diff --git a/basics/realloc/steel/.gitignore b/basics/realloc/steel/.gitignore new file mode 100644 index 000000000..cb95c0cdc --- /dev/null +++ b/basics/realloc/steel/.gitignore @@ -0,0 +1,2 @@ +target +test-ledger \ No newline at end of file diff --git a/basics/realloc/steel/Cargo.toml b/basics/realloc/steel/Cargo.toml new file mode 100644 index 000000000..72eddd3c3 --- /dev/null +++ b/basics/realloc/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 = "" +repository = "" +readme = "./README.md" +keywords = ["solana"] + +[workspace.dependencies] +realloc-api = { path = "./api", version = "0.1.0" } +bytemuck = "1.14" +num_enum = "0.7" +solana-program = "1.18" +steel = "2.0" +thiserror = "1.0" diff --git a/basics/realloc/steel/api/Cargo.toml b/basics/realloc/steel/api/Cargo.toml new file mode 100644 index 000000000..1e0002fb7 --- /dev/null +++ b/basics/realloc/steel/api/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "realloc-api" +description = "API for interacting with the Realloc program" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[dependencies] +bytemuck.workspace = true +num_enum.workspace = true +solana-program.workspace = true +steel.workspace = true +thiserror.workspace = true diff --git a/basics/realloc/steel/api/src/error.rs b/basics/realloc/steel/api/src/error.rs new file mode 100644 index 000000000..0e96dadbd --- /dev/null +++ b/basics/realloc/steel/api/src/error.rs @@ -0,0 +1,10 @@ +use steel::*; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] +#[repr(u32)] +pub enum ReallocError { + #[error("Invalid string length")] + InvalidStringLength = 0, +} + +error!(ReallocError); \ No newline at end of file diff --git a/basics/realloc/steel/api/src/instruction.rs b/basics/realloc/steel/api/src/instruction.rs new file mode 100644 index 000000000..424763c77 --- /dev/null +++ b/basics/realloc/steel/api/src/instruction.rs @@ -0,0 +1,25 @@ +use steel::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +pub enum ReallocInstruction { + Initialize = 0, + Update = 1, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Initialize { + pub message: [u8; 1024], + pub len: [u8; 4], // Store as bytes like in escrow +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct Update { + pub message: [u8; 1024], + pub len: [u8; 4], +} + +instruction!(ReallocInstruction, Initialize); +instruction!(ReallocInstruction, Update); \ No newline at end of file diff --git a/basics/realloc/steel/api/src/lib.rs b/basics/realloc/steel/api/src/lib.rs new file mode 100644 index 000000000..ae2810d71 --- /dev/null +++ b/basics/realloc/steel/api/src/lib.rs @@ -0,0 +1,24 @@ +pub mod error; +pub mod instruction; +pub mod state; +pub mod sdk; + +pub mod prelude { + pub use crate::error::*; + pub use crate::instruction::*; + pub use crate::state::*; + pub use crate::sdk::*; + pub use solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + msg, + pubkey::Pubkey, + rent::Rent, + system_program, + }; +} + +use steel::*; + +// TODO Set program id +declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35"); diff --git a/basics/realloc/steel/api/src/sdk.rs b/basics/realloc/steel/api/src/sdk.rs new file mode 100644 index 000000000..31c2cfb5b --- /dev/null +++ b/basics/realloc/steel/api/src/sdk.rs @@ -0,0 +1,46 @@ +use steel::*; +use crate::prelude::*; + +pub fn initialize( + payer: Pubkey, + message_account: Pubkey, + message: String, +) -> Instruction { + let mut message_bytes = [0u8; 1024]; + message_bytes[..message.len()].copy_from_slice(message.as_bytes()); + + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(payer, true), + AccountMeta::new(message_account, true), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: Initialize { + message: message_bytes, + len: (message.len() as u32).to_le_bytes(), + }.to_bytes(), + } +} + +pub fn update( + payer: Pubkey, + message_account: Pubkey, + message: String, +) -> Instruction { + let mut message_bytes = [0u8; 1024]; + message_bytes[..message.len()].copy_from_slice(message.as_bytes()); + + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(payer, true), + AccountMeta::new(message_account, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: Update { + message: message_bytes, + len: (message.len() as u32).to_le_bytes(), + }.to_bytes(), + } +} \ No newline at end of file diff --git a/basics/realloc/steel/api/src/state/message.rs b/basics/realloc/steel/api/src/state/message.rs new file mode 100644 index 000000000..c8b09d9e3 --- /dev/null +++ b/basics/realloc/steel/api/src/state/message.rs @@ -0,0 +1,17 @@ +use steel::*; +use super::ReallocAccount; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct Message { + pub message: [u8; 1024], // Max message size + pub len: u32, // Actual length of message +} + +impl Message { + pub fn required_space(message_len: usize) -> usize { + std::mem::size_of::() + } +} + +account!(ReallocAccount, Message); \ No newline at end of file diff --git a/basics/realloc/steel/api/src/state/mod.rs b/basics/realloc/steel/api/src/state/mod.rs new file mode 100644 index 000000000..b8e82b23c --- /dev/null +++ b/basics/realloc/steel/api/src/state/mod.rs @@ -0,0 +1,10 @@ +use steel::*; +mod message; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)] +pub enum ReallocAccount { + Message = 0, +} + +pub use message::*; \ No newline at end of file diff --git a/basics/realloc/steel/package.json b/basics/realloc/steel/package.json new file mode 100644 index 000000000..02381c42b --- /dev/null +++ b/basics/realloc/steel/package.json @@ -0,0 +1,14 @@ +{ + "name": "realloc-example", + "version": "1.0.0", + "description": "realloc basic example with steel framework for solana", + "scripts": { + "test": "cargo test-sbf", + "build-and-test": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./tests/fixtures && pnpm test", + "build": "cargo build-sbf --manifest-path=./program/Cargo.toml --sbf-out-dir=./program/target/so", + "deploy": "solana program deploy ./program/target/so/rent_example_program.so" + }, + "keywords": [], + "author": "Nithin", + "license": "ISC" +} diff --git a/basics/realloc/steel/program/Cargo.toml b/basics/realloc/steel/program/Cargo.toml new file mode 100644 index 000000000..ac2bae813 --- /dev/null +++ b/basics/realloc/steel/program/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "realloc-program" +description = "" +version.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +readme.workspace = true +keywords.workspace = true + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +realloc-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/realloc/steel/program/src/initialize.rs b/basics/realloc/steel/program/src/initialize.rs new file mode 100644 index 000000000..c7ef727a0 --- /dev/null +++ b/basics/realloc/steel/program/src/initialize.rs @@ -0,0 +1,32 @@ +use realloc_api::prelude::*; +use steel::*; + +pub fn process_initialize(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let args = Initialize::try_from_bytes(data)?; + let len = u32::from_le_bytes(args.len); + + let [payer_info, message_account_info, system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + payer_info.is_signer()?; + message_account_info.is_signer()?; + + // Create the account + let space = Message::required_space(len as usize); + + create_account::( + message_account_info, + system_program, + payer_info, + &realloc_api::ID, + &[], + )?; + + // Initialize the message + let message = message_account_info.as_account_mut::(&realloc_api::ID)?; + message.message = args.message; + message.len = len; + + Ok(()) +} diff --git a/basics/realloc/steel/program/src/lib.rs b/basics/realloc/steel/program/src/lib.rs new file mode 100644 index 000000000..c8189fbf8 --- /dev/null +++ b/basics/realloc/steel/program/src/lib.rs @@ -0,0 +1,29 @@ +use realloc_api::prelude::*; +use steel::*; + +mod initialize; +mod update; + +use initialize::process_initialize; +use update::process_update; + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (ix, data) = parse_instruction::( + &realloc_api::ID, + program_id, + data + )?; + + match ix { + ReallocInstruction::Initialize => process_initialize(accounts, data)?, + ReallocInstruction::Update => process_update(accounts, data)?, + } + + Ok(()) +} + +entrypoint!(process_instruction); \ No newline at end of file diff --git a/basics/realloc/steel/program/src/update.rs b/basics/realloc/steel/program/src/update.rs new file mode 100644 index 000000000..38f6a08d0 --- /dev/null +++ b/basics/realloc/steel/program/src/update.rs @@ -0,0 +1,20 @@ +use realloc_api::prelude::*; +use steel::*; + +pub fn process_update(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + let args = Update::try_from_bytes(data)?; + let len = u32::from_le_bytes(args.len); + + let [payer_info, message_account_info, system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + payer_info.is_signer()?; + + // Update the message + let message = message_account_info.as_account_mut::(&realloc_api::ID)?; + message.message = args.message; + message.len = len; + + Ok(()) +} \ No newline at end of file diff --git a/basics/realloc/steel/program/tests/test.rs b/basics/realloc/steel/program/tests/test.rs new file mode 100644 index 000000000..53faa4451 --- /dev/null +++ b/basics/realloc/steel/program/tests/test.rs @@ -0,0 +1,125 @@ +use realloc_api::prelude::*; +use solana_program::hash::Hash; +use solana_program_test::{processor, BanksClient, ProgramTest}; +use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; +use steel::*; + +async fn setup() -> (BanksClient, Keypair, Hash) { + let mut program_test = ProgramTest::new( + "realloc_program", + realloc_api::ID, + processor!(realloc_program::process_instruction), + ); + program_test.prefer_bpf(true); + program_test.start().await +} + +async fn check_account( + banks_client: &mut BanksClient, + pubkey: &Pubkey, + expected_message: &str +) { + let account = banks_client + .get_account(*pubkey) + .await + .expect("get_account") + .expect("account not found"); + + let message_account = Message::try_from_bytes(&account.data).unwrap(); + + // Convert stored message to string + let stored_message = String::from_utf8( + message_account.message[..message_account.len as usize] + .to_vec() + ).unwrap(); + + assert_eq!(stored_message, expected_message); + println!("Account Data Length: {}", account.data.len()); + println!("Message: {}", stored_message); +} + +#[tokio::test] +async fn test_initialize() { + let (mut banks_client, payer, recent_blockhash) = setup().await; + let message_account = Keypair::new(); + + let input = "hello"; + + let ix = initialize( + payer.pubkey(), + message_account.pubkey(), + input.to_string(), + ); + + let mut transaction = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &message_account], + recent_blockhash, + ); + + banks_client.process_transaction(transaction).await.unwrap(); + + check_account(&mut banks_client, &message_account.pubkey(), input).await; +} + +#[tokio::test] +async fn test_update() { + let (mut banks_client, payer, recent_blockhash) = setup().await; + let message_account = Keypair::new(); + + // First initialize + let init_message = "hello"; + let ix = initialize( + payer.pubkey(), + message_account.pubkey(), + init_message.to_string(), + ); + + let mut transaction = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &message_account], + recent_blockhash, + ); + + banks_client.process_transaction(transaction).await.unwrap(); + + // Then update + let update_message = "hello world"; + let ix = update( + payer.pubkey(), + message_account.pubkey(), + update_message.to_string(), + ); + + let mut transaction = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + + banks_client.process_transaction(transaction).await.unwrap(); + + check_account(&mut banks_client, &message_account.pubkey(), update_message).await; + + // Update with shorter message + let short_message = "hi"; + let ix = update( + payer.pubkey(), + message_account.pubkey(), + short_message.to_string(), + ); + + let mut transaction = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + + banks_client.process_transaction(transaction).await.unwrap(); + + check_account(&mut banks_client, &message_account.pubkey(), short_message).await; +} \ No newline at end of file