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"); +}