From 81d5424f8d4147cc4ccf3be35302749d1526ab2d Mon Sep 17 00:00:00 2001
From: Perelyn <64838956+Perelyn-sama@users.noreply.github.com>
Date: Thu, 2 Jan 2025 14:27:22 +0100
Subject: [PATCH] add basics/favorites/steel (#192)

---
 basics/favorites/steel/.gitignore             |   2 +
 basics/favorites/steel/Cargo.toml             |  21 +++
 basics/favorites/steel/README.md              |  22 +++
 basics/favorites/steel/api/Cargo.toml         |  11 ++
 basics/favorites/steel/api/src/consts.rs      |   2 +
 basics/favorites/steel/api/src/error.rs       |  10 ++
 basics/favorites/steel/api/src/instruction.rs |  19 +++
 basics/favorites/steel/api/src/lib.rs         |  20 +++
 basics/favorites/steel/api/src/sdk.rs         |  24 +++
 .../steel/api/src/state/favorites.rs          |  15 ++
 basics/favorites/steel/api/src/state/mod.rs   |  18 +++
 basics/favorites/steel/api/src/utils.rs       |  85 +++++++++++
 basics/favorites/steel/program/Cargo.toml     |  19 +++
 basics/favorites/steel/program/src/lib.rs     |  22 +++
 .../steel/program/src/set_favorites.rs        |  49 +++++++
 basics/favorites/steel/program/src/utils.rs   | 137 ++++++++++++++++++
 basics/favorites/steel/program/tests/test.rs  |  53 +++++++
 17 files changed, 529 insertions(+)
 create mode 100644 basics/favorites/steel/.gitignore
 create mode 100644 basics/favorites/steel/Cargo.toml
 create mode 100644 basics/favorites/steel/README.md
 create mode 100644 basics/favorites/steel/api/Cargo.toml
 create mode 100644 basics/favorites/steel/api/src/consts.rs
 create mode 100644 basics/favorites/steel/api/src/error.rs
 create mode 100644 basics/favorites/steel/api/src/instruction.rs
 create mode 100644 basics/favorites/steel/api/src/lib.rs
 create mode 100644 basics/favorites/steel/api/src/sdk.rs
 create mode 100644 basics/favorites/steel/api/src/state/favorites.rs
 create mode 100644 basics/favorites/steel/api/src/state/mod.rs
 create mode 100644 basics/favorites/steel/api/src/utils.rs
 create mode 100644 basics/favorites/steel/program/Cargo.toml
 create mode 100644 basics/favorites/steel/program/src/lib.rs
 create mode 100644 basics/favorites/steel/program/src/set_favorites.rs
 create mode 100644 basics/favorites/steel/program/src/utils.rs
 create mode 100644 basics/favorites/steel/program/tests/test.rs

diff --git a/basics/favorites/steel/.gitignore b/basics/favorites/steel/.gitignore
new file mode 100644
index 000000000..052739dbc
--- /dev/null
+++ b/basics/favorites/steel/.gitignore
@@ -0,0 +1,2 @@
+target
+test-ledger
diff --git a/basics/favorites/steel/Cargo.toml b/basics/favorites/steel/Cargo.toml
new file mode 100644
index 000000000..203056488
--- /dev/null
+++ b/basics/favorites/steel/Cargo.toml
@@ -0,0 +1,21 @@
+[workspace]
+resolver = "2"
+members = ["api", "program"]
+
+[workspace.package]
+version = "0.1.0"
+edition = "2021"
+license = "Apache-2.0"
+homepage = ""
+documentation = ""
+respository = ""
+readme = "./README.md"
+keywords = ["solana"]
+
+[workspace.dependencies]
+steel-api = { path = "./api", version = "0.1.0" }
+bytemuck = "1.14"
+num_enum = "0.7"
+solana-program = "1.18"
+steel = "1.3"
+thiserror = "1.0"
diff --git a/basics/favorites/steel/README.md b/basics/favorites/steel/README.md
new file mode 100644
index 000000000..4f4fe2a54
--- /dev/null
+++ b/basics/favorites/steel/README.md
@@ -0,0 +1,22 @@
+# Steel
+
+**Steel** is a ...
+        
+## API
+- [`Consts`](api/src/consts.rs) – Program constants.
+- [`Error`](api/src/error.rs) – Custom program errors.
+- [`Event`](api/src/event.rs) – Custom program events.
+- [`Instruction`](api/src/instruction.rs) – Declared instructions.
+
+## Instructions
+- [`Hello`](program/src/hello.rs) – Hello ...
+
+## State
+- [`User`](api/src/state/user.rs) – User ...
+
+## Tests
+
+To run the test suit, use the Solana toolchain:
+```
+cargo test-sbf         
+```
diff --git a/basics/favorites/steel/api/Cargo.toml b/basics/favorites/steel/api/Cargo.toml
new file mode 100644
index 000000000..34b563782
--- /dev/null
+++ b/basics/favorites/steel/api/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "steel-api"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+bytemuck.workspace = true
+num_enum.workspace = true
+solana-program.workspace = true
+steel.workspace = true
+thiserror.workspace = true
diff --git a/basics/favorites/steel/api/src/consts.rs b/basics/favorites/steel/api/src/consts.rs
new file mode 100644
index 000000000..c263203b1
--- /dev/null
+++ b/basics/favorites/steel/api/src/consts.rs
@@ -0,0 +1,2 @@
+/// Seed of the favorites account PDA.
+pub const FAVORITES: &[u8] = b"favorites";
diff --git a/basics/favorites/steel/api/src/error.rs b/basics/favorites/steel/api/src/error.rs
new file mode 100644
index 000000000..96e84da4d
--- /dev/null
+++ b/basics/favorites/steel/api/src/error.rs
@@ -0,0 +1,10 @@
+use steel::*;
+
+#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)]
+#[repr(u32)]
+pub enum SteelError {
+    #[error("This is a dummy error")]
+    Dummy = 0,
+}
+
+error!(SteelError);
diff --git a/basics/favorites/steel/api/src/instruction.rs b/basics/favorites/steel/api/src/instruction.rs
new file mode 100644
index 000000000..539638583
--- /dev/null
+++ b/basics/favorites/steel/api/src/instruction.rs
@@ -0,0 +1,19 @@
+use steel::*;
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
+pub enum SteelInstruction {
+    SetFavorites = 0,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Pod, Zeroable)]
+pub struct SetFavorites {
+    pub number: [u8; 8],
+
+    pub color: [u8; 32],
+
+    pub hobbies: [[u8; 32]; 3],
+}
+
+instruction!(SteelInstruction, SetFavorites);
diff --git a/basics/favorites/steel/api/src/lib.rs b/basics/favorites/steel/api/src/lib.rs
new file mode 100644
index 000000000..76867dc34
--- /dev/null
+++ b/basics/favorites/steel/api/src/lib.rs
@@ -0,0 +1,20 @@
+pub mod consts;
+pub mod error;
+pub mod instruction;
+pub mod sdk;
+pub mod state;
+pub mod utils;
+
+pub mod prelude {
+    pub use crate::consts::*;
+    pub use crate::error::*;
+    pub use crate::instruction::*;
+    pub use crate::sdk::*;
+    pub use crate::state::*;
+    pub use crate::utils::*;
+}
+
+use steel::*;
+
+// TODO Set program id
+declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35");
diff --git a/basics/favorites/steel/api/src/sdk.rs b/basics/favorites/steel/api/src/sdk.rs
new file mode 100644
index 000000000..6f5d875bd
--- /dev/null
+++ b/basics/favorites/steel/api/src/sdk.rs
@@ -0,0 +1,24 @@
+use steel::*;
+
+use crate::prelude::*;
+
+pub fn set_favorites(signer: Pubkey, number: u64, color: &str, hobbies: Vec<&str>) -> Instruction {
+    let color_bytes: [u8; 32] = string_to_bytes32_padded(color).unwrap();
+
+    let hobbies_bytes = strings_to_bytes32_array_padded(hobbies).unwrap();
+
+    Instruction {
+        program_id: crate::ID,
+        accounts: vec![
+            AccountMeta::new(signer, true),
+            AccountMeta::new(favorites_pda().0, false),
+            AccountMeta::new_readonly(system_program::ID, false),
+        ],
+        data: SetFavorites {
+            number: number.to_le_bytes(),
+            color: color_bytes,
+            hobbies: hobbies_bytes,
+        }
+        .to_bytes(),
+    }
+}
diff --git a/basics/favorites/steel/api/src/state/favorites.rs b/basics/favorites/steel/api/src/state/favorites.rs
new file mode 100644
index 000000000..6f912c586
--- /dev/null
+++ b/basics/favorites/steel/api/src/state/favorites.rs
@@ -0,0 +1,15 @@
+use steel::*;
+
+use super::SteelAccount;
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
+pub struct Favorites {
+    pub number: u64,
+
+    pub color: [u8; 32],
+
+    pub hobbies: [[u8; 32]; 3],
+}
+
+account!(SteelAccount, Favorites);
diff --git a/basics/favorites/steel/api/src/state/mod.rs b/basics/favorites/steel/api/src/state/mod.rs
new file mode 100644
index 000000000..72eb8dd98
--- /dev/null
+++ b/basics/favorites/steel/api/src/state/mod.rs
@@ -0,0 +1,18 @@
+mod favorites;
+
+pub use favorites::*;
+
+use steel::*;
+
+use crate::consts::*;
+
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)]
+pub enum SteelAccount {
+    Favorites = 0,
+}
+
+/// Fetch PDA of the favorites account.
+pub fn favorites_pda() -> (Pubkey, u8) {
+    Pubkey::find_program_address(&[FAVORITES], &crate::id())
+}
diff --git a/basics/favorites/steel/api/src/utils.rs b/basics/favorites/steel/api/src/utils.rs
new file mode 100644
index 000000000..a590bf2f9
--- /dev/null
+++ b/basics/favorites/steel/api/src/utils.rs
@@ -0,0 +1,85 @@
+use std::error::Error;
+use std::fmt;
+
+#[derive(Debug)]
+pub enum ConversionError {
+    StringTooLong(usize),
+    StringTooShort(usize),
+    VecLengthMismatch { expected: usize, actual: usize },
+    InvalidUtf8,
+}
+
+impl fmt::Display for ConversionError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            ConversionError::StringTooLong(len) => {
+                write!(f, "String length {} exceeds 32 bytes", len)
+            }
+            ConversionError::StringTooShort(len) => {
+                write!(f, "String length {} is less than 32 bytes", len)
+            }
+            ConversionError::VecLengthMismatch { expected, actual } => {
+                write!(
+                    f,
+                    "Vector length mismatch: expected {}, got {}",
+                    expected, actual
+                )
+            }
+            ConversionError::InvalidUtf8 => write!(f, "Invalid UTF-8 sequence in bytes"),
+        }
+    }
+}
+
+impl Error for ConversionError {}
+
+// Convert string to bytes with padding
+pub fn string_to_bytes32_padded(input: &str) -> Result<[u8; 32], ConversionError> {
+    let bytes = input.as_bytes();
+    let len = bytes.len();
+
+    if len > 32 {
+        return Err(ConversionError::StringTooLong(len));
+    }
+
+    let mut result = [0u8; 32];
+    result[..len].copy_from_slice(bytes);
+    Ok(result)
+}
+
+// Convert bytes back to string, trimming trailing zeros
+pub fn bytes32_to_string(bytes: &[u8; 32]) -> Result<String, ConversionError> {
+    // Find the actual length by looking for the first zero or taking full length
+    let actual_len = bytes.iter().position(|&b| b == 0).unwrap_or(32);
+
+    // Convert the slice up to actual_len to a string
+    String::from_utf8(bytes[..actual_len].to_vec()).map_err(|_| ConversionError::InvalidUtf8)
+}
+
+// Convert vec of strings to byte arrays with padding
+pub fn strings_to_bytes32_array_padded<const N: usize>(
+    inputs: Vec<&str>,
+) -> Result<[[u8; 32]; N], ConversionError> {
+    if inputs.len() != N {
+        return Err(ConversionError::VecLengthMismatch {
+            expected: N,
+            actual: inputs.len(),
+        });
+    }
+
+    let mut result = [[0u8; 32]; N];
+    for (i, input) in inputs.iter().enumerate() {
+        result[i] = string_to_bytes32_padded(input)?;
+    }
+    Ok(result)
+}
+
+// Convert array of byte arrays back to vec of strings
+pub fn bytes32_array_to_strings<const N: usize>(
+    bytes_array: &[[u8; 32]; N],
+) -> Result<Vec<String>, ConversionError> {
+    let mut result = Vec::with_capacity(N);
+    for bytes in bytes_array.iter() {
+        result.push(bytes32_to_string(bytes)?);
+    }
+    Ok(result)
+}
diff --git a/basics/favorites/steel/program/Cargo.toml b/basics/favorites/steel/program/Cargo.toml
new file mode 100644
index 000000000..db99b68cc
--- /dev/null
+++ b/basics/favorites/steel/program/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "steel-program"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+
+[dependencies]
+steel-api.workspace = true
+solana-program.workspace = true
+steel.workspace = true
+
+[dev-dependencies]
+base64 = "0.21"
+rand = "0.8.5"
+solana-program-test = "1.18"
+solana-sdk = "1.18"
+tokio = { version = "1.35", features = ["full"] }
diff --git a/basics/favorites/steel/program/src/lib.rs b/basics/favorites/steel/program/src/lib.rs
new file mode 100644
index 000000000..a6b8cd616
--- /dev/null
+++ b/basics/favorites/steel/program/src/lib.rs
@@ -0,0 +1,22 @@
+mod set_favorites;
+
+pub use set_favorites::*;
+
+use steel::*;
+use steel_api::prelude::*;
+
+pub fn process_instruction(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    data: &[u8],
+) -> ProgramResult {
+    let (ix, data) = parse_instruction(&steel_api::ID, program_id, data)?;
+
+    match ix {
+        SteelInstruction::SetFavorites => process_set_favorites(accounts, data)?,
+    }
+
+    Ok(())
+}
+
+entrypoint!(process_instruction);
diff --git a/basics/favorites/steel/program/src/set_favorites.rs b/basics/favorites/steel/program/src/set_favorites.rs
new file mode 100644
index 000000000..cc86aef40
--- /dev/null
+++ b/basics/favorites/steel/program/src/set_favorites.rs
@@ -0,0 +1,49 @@
+use solana_program::msg;
+use steel::*;
+use steel_api::prelude::*;
+
+pub fn process_set_favorites(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
+    // Parse args.
+    let args = SetFavorites::try_from_bytes(data)?;
+    let number = u64::from_le_bytes(args.number);
+    let color = bytes32_to_string(&args.color).unwrap();
+    let hobbies = bytes32_array_to_strings(&args.hobbies).unwrap();
+
+    // Get expected pda bump.
+    let favorites_bump = favorites_pda().1;
+
+    // Load accounts.
+    let [user_info, favorites_info, system_program] = accounts else {
+        return Err(ProgramError::NotEnoughAccountKeys);
+    };
+    user_info.is_signer()?;
+    favorites_info.is_empty()?.is_writable()?.has_seeds(
+        &[FAVORITES],
+        favorites_bump,
+        &steel_api::ID,
+    )?;
+    system_program.is_program(&system_program::ID)?;
+
+    // Initialize favorites.
+    create_account::<Favorites>(
+        favorites_info,
+        &steel_api::ID,
+        &[FAVORITES, &[favorites_bump]],
+        system_program,
+        user_info,
+    )?;
+
+    msg!("Greetings from {}", &steel_api::ID);
+    let user_public_key = user_info.key;
+
+    msg!(
+              "User {user_public_key}'s favorite number is {number}, favorite color is: {color}, and their hobbies are {hobbies:?}",
+          );
+
+    let favorites = favorites_info.to_account_mut::<Favorites>(&steel_api::ID)?;
+    favorites.number = number;
+    favorites.color = args.color;
+    favorites.hobbies = args.hobbies;
+
+    Ok(())
+}
diff --git a/basics/favorites/steel/program/src/utils.rs b/basics/favorites/steel/program/src/utils.rs
new file mode 100644
index 000000000..2af1c9fd1
--- /dev/null
+++ b/basics/favorites/steel/program/src/utils.rs
@@ -0,0 +1,137 @@
+use std::error::Error;
+use std::fmt;
+
+#[derive(Debug)]
+pub enum ConversionError {
+    StringTooLong(usize),
+    StringTooShort(usize),
+    VecLengthMismatch { expected: usize, actual: usize },
+    InvalidUtf8,
+}
+
+impl fmt::Display for ConversionError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            ConversionError::StringTooLong(len) => {
+                write!(f, "String length {} exceeds 32 bytes", len)
+            }
+            ConversionError::StringTooShort(len) => {
+                write!(f, "String length {} is less than 32 bytes", len)
+            }
+            ConversionError::VecLengthMismatch { expected, actual } => {
+                write!(
+                    f,
+                    "Vector length mismatch: expected {}, got {}",
+                    expected, actual
+                )
+            }
+            ConversionError::InvalidUtf8 => write!(f, "Invalid UTF-8 sequence in bytes"),
+        }
+    }
+}
+
+impl Error for ConversionError {}
+
+// Convert string to bytes with padding
+pub fn string_to_bytes32_padded(input: &str) -> Result<[u8; 32], ConversionError> {
+    let bytes = input.as_bytes();
+    let len = bytes.len();
+
+    if len > 32 {
+        return Err(ConversionError::StringTooLong(len));
+    }
+
+    let mut result = [0u8; 32];
+    result[..len].copy_from_slice(bytes);
+    Ok(result)
+}
+
+// NEW: Convert bytes back to string, trimming trailing zeros
+pub fn bytes32_to_string(bytes: &[u8; 32]) -> Result<String, ConversionError> {
+    // Find the actual length by looking for the first zero or taking full length
+    let actual_len = bytes.iter().position(|&b| b == 0).unwrap_or(32);
+
+    // Convert the slice up to actual_len to a string
+    String::from_utf8(bytes[..actual_len].to_vec()).map_err(|_| ConversionError::InvalidUtf8)
+}
+
+// Convert vec of strings to byte arrays with padding
+pub fn strings_to_bytes32_array_padded<const N: usize>(
+    inputs: Vec<&str>,
+) -> Result<[[u8; 32]; N], ConversionError> {
+    if inputs.len() != N {
+        return Err(ConversionError::VecLengthMismatch {
+            expected: N,
+            actual: inputs.len(),
+        });
+    }
+
+    let mut result = [[0u8; 32]; N];
+    for (i, input) in inputs.iter().enumerate() {
+        result[i] = string_to_bytes32_padded(input)?;
+    }
+    Ok(result)
+}
+
+// NEW: Convert array of byte arrays back to vec of strings
+pub fn bytes32_array_to_strings<const N: usize>(
+    bytes_array: &[[u8; 32]; N],
+) -> Result<Vec<String>, ConversionError> {
+    let mut result = Vec::with_capacity(N);
+    for bytes in bytes_array.iter() {
+        result.push(bytes32_to_string(bytes)?);
+    }
+    Ok(result)
+}
+
+// // Convert string to bytes with padding
+// pub fn string_to_bytes32_padded(input: &str) -> Result<[u8; 32], ConversionError> {
+//     let bytes = input.as_bytes();
+//     let len = bytes.len();
+
+//     if len > 32 {
+//         return Err(ConversionError::StringTooLong(len));
+//     }
+
+//     let mut result = [0u8; 32];
+//     result[..len].copy_from_slice(bytes);
+//     Ok(result)
+// }
+
+// // NEW: Convert bytes back to string, trimming trailing zeros
+// pub fn bytes32_to_string(bytes: &[u8; 32]) -> Result<String, ConversionError> {
+//     // Find the actual length by looking for the first zero or taking full length
+//     let actual_len = bytes.iter().position(|&b| b == 0).unwrap_or(32);
+
+//     // Convert the slice up to actual_len to a string
+//     String::from_utf8(bytes[..actual_len].to_vec()).map_err(|_| ConversionError::InvalidUtf8)
+// }
+
+// // Convert vec of strings to byte arrays with padding
+// pub fn strings_to_bytes32_array_padded<const N: usize>(
+//     inputs: Vec<&str>,
+// ) -> Result<[[u8; 32]; N], ConversionError> {
+//     if inputs.len() != N {
+//         return Err(ConversionError::VecLengthMismatch {
+//             expected: N,
+//             actual: inputs.len(),
+//         });
+//     }
+
+//     let mut result = [[0u8; 32]; N];
+//     for (i, input) in inputs.iter().enumerate() {
+//         result[i] = string_to_bytes32_padded(input)?;
+//     }
+//     Ok(result)
+// }
+
+// // NEW: Convert array of byte arrays back to vec of strings
+// pub fn bytes32_array_to_strings<const N: usize>(
+//     bytes_array: &[[u8; 32]; N],
+// ) -> Result<Vec<String>, ConversionError> {
+//     let mut result = Vec::with_capacity(N);
+//     for bytes in bytes_array.iter() {
+//         result.push(bytes32_to_string(bytes)?);
+//     }
+//     Ok(result)
+// }
diff --git a/basics/favorites/steel/program/tests/test.rs b/basics/favorites/steel/program/tests/test.rs
new file mode 100644
index 000000000..b501372a6
--- /dev/null
+++ b/basics/favorites/steel/program/tests/test.rs
@@ -0,0 +1,53 @@
+use solana_program::hash::Hash;
+use solana_program_test::{processor, BanksClient, ProgramTest};
+use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction};
+use steel::*;
+use steel_api::prelude::*;
+
+async fn setup() -> (BanksClient, Keypair, Hash) {
+    let mut program_test = ProgramTest::new(
+        "steel_program",
+        steel_api::ID,
+        processor!(steel_program::process_instruction),
+    );
+    program_test.prefer_bpf(true);
+    program_test.start().await
+}
+
+#[tokio::test]
+async fn run_test() {
+    // Setup test
+    let (mut banks, payer, blockhash) = setup().await;
+
+    let favorite_number: u64 = 23;
+    let favorite_color: &str = "purple";
+    let mut favorite_hobbies: Vec<&str> = Vec::new();
+    favorite_hobbies.push("skiing");
+    favorite_hobbies.push("skydiving");
+    favorite_hobbies.push("biking");
+
+    // Submit set favorites transaction.
+    let ix = set_favorites(
+        payer.pubkey(),
+        favorite_number,
+        favorite_color,
+        favorite_hobbies,
+    );
+    let tx = Transaction::new_signed_with_payer(&[ix], Some(&payer.pubkey()), &[&payer], blockhash);
+    let res = banks.process_transaction(tx).await;
+    assert!(res.is_ok());
+
+    // Verify favorites was updated.
+    let favorites_address = favorites_pda().0;
+    let favorites_account = banks.get_account(favorites_address).await.unwrap().unwrap();
+    let favorites = Favorites::try_from_bytes(&favorites_account.data).unwrap();
+
+    let favorites_number = favorites.number;
+    let favorites_color = bytes32_to_string(&favorites.color).unwrap();
+    let favorites_hobbies = bytes32_array_to_strings(&favorites.hobbies).unwrap();
+
+    assert_eq!(favorites_account.owner, steel_api::ID);
+    assert_eq!(favorites_number, 23);
+    assert_eq!(favorites_color, "purple");
+    assert_eq!(favorites_hobbies[0], "skiing");
+}