From ea4b76d1481399e25e83ae0915340e35509c527b Mon Sep 17 00:00:00 2001 From: Perelyn <64838956+Perelyn-sama@users.noreply.github.com> Date: Sun, 27 Oct 2024 02:35:57 +0100 Subject: [PATCH 1/2] add steel escrow example --- tokens/escrow/steel/.gitignore | 2 + tokens/escrow/steel/Cargo.toml | 25 ++++++ tokens/escrow/steel/README.md | 28 +++++++ tokens/escrow/steel/api/Cargo.toml | 20 +++++ tokens/escrow/steel/api/src/consts.rs | 2 + tokens/escrow/steel/api/src/error.rs | 10 +++ tokens/escrow/steel/api/src/instruction.rs | 23 ++++++ tokens/escrow/steel/api/src/lib.rs | 18 +++++ tokens/escrow/steel/api/src/sdk.rs | 66 ++++++++++++++++ tokens/escrow/steel/api/src/state/mod.rs | 21 +++++ tokens/escrow/steel/api/src/state/offer.rs | 18 +++++ tokens/escrow/steel/program/Cargo.toml | 28 +++++++ tokens/escrow/steel/program/src/lib.rs | 25 ++++++ tokens/escrow/steel/program/src/make_offer.rs | 77 +++++++++++++++++++ tokens/escrow/steel/program/src/take_offer.rs | 15 ++++ tokens/escrow/steel/program/tests/test.rs | 45 +++++++++++ 16 files changed, 423 insertions(+) create mode 100644 tokens/escrow/steel/.gitignore create mode 100644 tokens/escrow/steel/Cargo.toml create mode 100644 tokens/escrow/steel/README.md create mode 100644 tokens/escrow/steel/api/Cargo.toml create mode 100644 tokens/escrow/steel/api/src/consts.rs create mode 100644 tokens/escrow/steel/api/src/error.rs create mode 100644 tokens/escrow/steel/api/src/instruction.rs create mode 100644 tokens/escrow/steel/api/src/lib.rs create mode 100644 tokens/escrow/steel/api/src/sdk.rs create mode 100644 tokens/escrow/steel/api/src/state/mod.rs create mode 100644 tokens/escrow/steel/api/src/state/offer.rs create mode 100644 tokens/escrow/steel/program/Cargo.toml create mode 100644 tokens/escrow/steel/program/src/lib.rs create mode 100644 tokens/escrow/steel/program/src/make_offer.rs create mode 100644 tokens/escrow/steel/program/src/take_offer.rs create mode 100644 tokens/escrow/steel/program/tests/test.rs diff --git a/tokens/escrow/steel/.gitignore b/tokens/escrow/steel/.gitignore new file mode 100644 index 000000000..052739dbc --- /dev/null +++ b/tokens/escrow/steel/.gitignore @@ -0,0 +1,2 @@ +target +test-ledger diff --git a/tokens/escrow/steel/Cargo.toml b/tokens/escrow/steel/Cargo.toml new file mode 100644 index 000000000..6b2dd8610 --- /dev/null +++ b/tokens/escrow/steel/Cargo.toml @@ -0,0 +1,25 @@ +[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] +escrow-api = { path = "./api", version = "0.1.0" } +bytemuck = "1.14" +num_enum = "0.7" +solana-program = "1.18" +steel = { features = ["spl"], version = "2.0" } +thiserror = "1.0" +spl-token = { version = "^4", features = ["no-entrypoint"] } +spl-associated-token-account = { version = "^2.3", features = [ + "no-entrypoint", +] } diff --git a/tokens/escrow/steel/README.md b/tokens/escrow/steel/README.md new file mode 100644 index 000000000..a316ec09f --- /dev/null +++ b/tokens/escrow/steel/README.md @@ -0,0 +1,28 @@ +# Escrow + +**Escrow** 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 +- [`Add`](program/src/add.rs) – Add ... +- [`Initialize`](program/src/initialize.rs) – Initialize ... + +## State +- [`Counter`](api/src/state/counter.rs) – Counter ... + +## Get started + +Compile your program: +```sh +steel build +``` + +Run unit and integration tests: +```sh +steel test +``` diff --git a/tokens/escrow/steel/api/Cargo.toml b/tokens/escrow/steel/api/Cargo.toml new file mode 100644 index 000000000..7dd1dac0d --- /dev/null +++ b/tokens/escrow/steel/api/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "escrow-api" +description = "API for interacting with the Escrow 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 +spl-token.workspace = true +spl-associated-token-account.workspace = true diff --git a/tokens/escrow/steel/api/src/consts.rs b/tokens/escrow/steel/api/src/consts.rs new file mode 100644 index 000000000..d91efc1f5 --- /dev/null +++ b/tokens/escrow/steel/api/src/consts.rs @@ -0,0 +1,2 @@ +/// Seed of the offer account PDA. +pub const OFFER: &[u8] = b"offer"; diff --git a/tokens/escrow/steel/api/src/error.rs b/tokens/escrow/steel/api/src/error.rs new file mode 100644 index 000000000..5ae4ab897 --- /dev/null +++ b/tokens/escrow/steel/api/src/error.rs @@ -0,0 +1,10 @@ +use steel::*; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] +#[repr(u32)] +pub enum EscrowError { + #[error("This is a dummy error")] + Dummy = 0, +} + +error!(EscrowError); diff --git a/tokens/escrow/steel/api/src/instruction.rs b/tokens/escrow/steel/api/src/instruction.rs new file mode 100644 index 000000000..51ce42f89 --- /dev/null +++ b/tokens/escrow/steel/api/src/instruction.rs @@ -0,0 +1,23 @@ +use steel::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] +pub enum EscrowInstruction { + MakeOffer = 0, + TakeOffer = 1, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct MakeOffer { + pub token_a_offered_amount: [u8; 8], + pub id: [u8; 8], + pub token_b_wanted_amount: [u8; 8], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct TakeOffer {} + +instruction!(EscrowInstruction, MakeOffer); +instruction!(EscrowInstruction, TakeOffer); diff --git a/tokens/escrow/steel/api/src/lib.rs b/tokens/escrow/steel/api/src/lib.rs new file mode 100644 index 000000000..aed2ed84f --- /dev/null +++ b/tokens/escrow/steel/api/src/lib.rs @@ -0,0 +1,18 @@ +pub mod consts; +pub mod error; +pub mod instruction; +pub mod sdk; +pub mod state; + +pub mod prelude { + pub use crate::consts::*; + pub use crate::error::*; + pub use crate::instruction::*; + pub use crate::sdk::*; + pub use crate::state::*; +} + +use steel::*; + +// TODO Set program id +declare_id!("z7msBPQHDJjTvdQRoEcKyENgXDhSRYeHieN1ZMTqo35"); diff --git a/tokens/escrow/steel/api/src/sdk.rs b/tokens/escrow/steel/api/src/sdk.rs new file mode 100644 index 000000000..58f39ce96 --- /dev/null +++ b/tokens/escrow/steel/api/src/sdk.rs @@ -0,0 +1,66 @@ +use steel::*; + +use crate::prelude::*; + +pub fn make_offer( + maker: Pubkey, + token_mint_a: Pubkey, + token_mint_b: Pubkey, + maker_token_account_a: Pubkey, + vault: Pubkey, + id: u64, + token_a_offered_amount: u64, + token_b_wanted_amount: u64, +) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(maker, true), + AccountMeta::new_readonly(token_mint_a, false), + AccountMeta::new_readonly(token_mint_b, false), + AccountMeta::new_readonly(maker_token_account_a, false), + AccountMeta::new(offer_pda(maker, id).0, false), + AccountMeta::new(vault, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token::ID, false), + AccountMeta::new_readonly(spl_associated_token_account::ID, false), + ], + data: MakeOffer { + token_a_offered_amount: token_a_offered_amount.to_le_bytes(), + id: id.to_le_bytes(), + token_b_wanted_amount: token_b_wanted_amount.to_le_bytes(), + } + .to_bytes(), + } +} + +pub fn take_offer( + taker: Pubkey, + maker: Pubkey, + token_mint_a: Pubkey, + token_mint_b: Pubkey, + taker_token_account_a: Pubkey, + taker_token_account_b: Pubkey, + maker_token_account_b: Pubkey, + vault: Pubkey, + id: u64, +) -> Instruction { + Instruction { + program_id: crate::ID, + accounts: vec![ + AccountMeta::new(taker, true), + AccountMeta::new(maker, false), + AccountMeta::new_readonly(token_mint_a, false), + AccountMeta::new_readonly(token_mint_b, false), + AccountMeta::new_readonly(taker_token_account_a, false), + AccountMeta::new_readonly(taker_token_account_b, false), + AccountMeta::new_readonly(maker_token_account_b, false), + AccountMeta::new(offer_pda(maker, id).0, false), + AccountMeta::new(vault, false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(spl_token::ID, false), + AccountMeta::new_readonly(spl_associated_token_account::ID, false), + ], + data: TakeOffer {}.to_bytes(), + } +} diff --git a/tokens/escrow/steel/api/src/state/mod.rs b/tokens/escrow/steel/api/src/state/mod.rs new file mode 100644 index 000000000..0e7c98a12 --- /dev/null +++ b/tokens/escrow/steel/api/src/state/mod.rs @@ -0,0 +1,21 @@ +mod offer; + +pub use offer::*; + +use steel::*; + +use crate::consts::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)] +pub enum EscrowAccount { + Offer = 0, +} + +/// Fetch PDA of the counter account. +pub fn offer_pda(maker: Pubkey, id: u64) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[OFFER, maker.as_ref(), id.to_le_bytes().as_ref()], + &crate::id(), + ) +} diff --git a/tokens/escrow/steel/api/src/state/offer.rs b/tokens/escrow/steel/api/src/state/offer.rs new file mode 100644 index 000000000..787c9c9bc --- /dev/null +++ b/tokens/escrow/steel/api/src/state/offer.rs @@ -0,0 +1,18 @@ +use steel::*; + +use super::EscrowAccount; + +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] +pub struct Offer { + pub id: u64, + pub maker: Pubkey, + pub token_mint_a: Pubkey, + pub token_mint_b: Pubkey, + pub token_b_wanted_amount: u64, + pub bump: u8, + + pub _padding: [u8; 7], // Explicit padding to match 960 bits +} + +account!(EscrowAccount, Offer); diff --git a/tokens/escrow/steel/program/Cargo.toml b/tokens/escrow/steel/program/Cargo.toml new file mode 100644 index 000000000..23795bb09 --- /dev/null +++ b/tokens/escrow/steel/program/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "escrow-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] +escrow-api.workspace = true +solana-program.workspace = true +steel.workspace = true +spl-token.workspace = true +spl-associated-token-account.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/tokens/escrow/steel/program/src/lib.rs b/tokens/escrow/steel/program/src/lib.rs new file mode 100644 index 000000000..ce5b8fd05 --- /dev/null +++ b/tokens/escrow/steel/program/src/lib.rs @@ -0,0 +1,25 @@ +mod make_offer; +mod take_offer; + +use make_offer::*; +use take_offer::*; + +use escrow_api::prelude::*; +use steel::*; + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let (ix, data) = parse_instruction(&escrow_api::ID, program_id, data)?; + + match ix { + EscrowInstruction::MakeOffer => process_make_offer(accounts, data)?, + EscrowInstruction::TakeOffer => process_take_offer(accounts, data)?, + } + + Ok(()) +} + +entrypoint!(process_instruction); diff --git a/tokens/escrow/steel/program/src/make_offer.rs b/tokens/escrow/steel/program/src/make_offer.rs new file mode 100644 index 000000000..8981700b0 --- /dev/null +++ b/tokens/escrow/steel/program/src/make_offer.rs @@ -0,0 +1,77 @@ +use escrow_api::prelude::*; +use steel::{transfer as transfer_spl_tokens, *}; + +pub fn process_make_offer(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult { + // Parse args. + let args = MakeOffer::try_from_bytes(data)?; + let token_a_offered_amount = u64::from_le_bytes(args.token_a_offered_amount); + let id = u64::from_le_bytes(args.id); + let token_b_wanted_amount = u64::from_le_bytes(args.token_b_wanted_amount); + + // Load accounts. + let [maker_info, token_mint_a_info, token_mint_b_info, maker_token_account_a_info, offer_info, vault_info, system_program, token_program, associated_token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + maker_info.is_signer()?; + + token_mint_a_info + .has_address(&token_mint_a_info.key)? + .is_writable()? + .as_mint()?; + + token_mint_b_info + .has_address(&token_mint_b_info.key)? + .is_writable()? + .as_mint()?; + + maker_token_account_a_info + .as_associated_token_account(maker_info.key, token_mint_a_info.key)?; + + offer_info.is_empty()?.is_writable()?.has_seeds( + &[OFFER, maker_info.key.as_ref(), id.to_le_bytes().as_ref()], + &escrow_api::ID, + )?; + + vault_info.is_empty()?.is_writable()?; + + // Initialize offer. + create_account::( + offer_info, + system_program, + maker_info, + &escrow_api::ID, + &[OFFER, maker_info.key.as_ref(), id.to_le_bytes().as_ref()], + )?; + + // create valut + create_associated_token_account( + maker_info, + offer_info, + vault_info, + token_mint_a_info, + system_program, + token_program, + associated_token_program, + )?; + + transfer_spl_tokens( + maker_info, + maker_token_account_a_info, + vault_info, + token_program, + token_a_offered_amount, + )?; + + let offer = offer_info.as_account_mut::(&escrow_api::ID)?; + offer.id = id; + offer.maker = *maker_info.key; + offer.token_mint_a = *token_mint_a_info.key; + offer.token_mint_b = *token_mint_b_info.key; + offer.token_b_wanted_amount = token_b_wanted_amount; + offer.bump = offer_pda(*maker_info.key, id).1; + + Ok(()) +} diff --git a/tokens/escrow/steel/program/src/take_offer.rs b/tokens/escrow/steel/program/src/take_offer.rs new file mode 100644 index 000000000..b18654a6f --- /dev/null +++ b/tokens/escrow/steel/program/src/take_offer.rs @@ -0,0 +1,15 @@ +use escrow_api::prelude::*; +use steel::*; + +pub fn process_take_offer(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { + // Load accounts. + let [taker_info, maker_info, token_mint_a_info, token_mint_b_info, taker_token_account_a_info, taker_token_account_b_info, maker_token_account_b_info, system_program, token_program, associated_token_program] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + taker_info.is_signer()?; + + Ok(()) +} diff --git a/tokens/escrow/steel/program/tests/test.rs b/tokens/escrow/steel/program/tests/test.rs new file mode 100644 index 000000000..4a0d357d8 --- /dev/null +++ b/tokens/escrow/steel/program/tests/test.rs @@ -0,0 +1,45 @@ +// use escrow_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( +// "escrow_program", +// escrow_api::ID, +// processor!(escrow_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; + +// // Submit initialize transaction. +// let ix = initialize(payer.pubkey()); +// 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 counter was initialized. +// let counter_address = counter_pda().0; +// let counter_account = banks.get_account(counter_address).await.unwrap().unwrap(); +// let counter = Counter::try_from_bytes(&counter_account.data).unwrap(); +// assert_eq!(counter_account.owner, escrow_api::ID); +// assert_eq!(counter.value, 0); + +// // Submit add transaction. +// let ix = add(payer.pubkey(), 42); +// 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 counter was incremented. +// let counter_account = banks.get_account(counter_address).await.unwrap().unwrap(); +// let counter = Counter::try_from_bytes(&counter_account.data).unwrap(); +// assert_eq!(counter.value, 42); +// } From b0cbe0427ee192d8b488b8c7b9b1ca7212a66553 Mon Sep 17 00:00:00 2001 From: Perelyn <64838956+Perelyn-sama@users.noreply.github.com> Date: Tue, 29 Oct 2024 02:06:38 +0100 Subject: [PATCH 2/2] Update take_offer.rs --- tokens/escrow/steel/program/src/take_offer.rs | 113 +++++++++++++++++- 1 file changed, 111 insertions(+), 2 deletions(-) diff --git a/tokens/escrow/steel/program/src/take_offer.rs b/tokens/escrow/steel/program/src/take_offer.rs index b18654a6f..8348f1c0f 100644 --- a/tokens/escrow/steel/program/src/take_offer.rs +++ b/tokens/escrow/steel/program/src/take_offer.rs @@ -1,15 +1,124 @@ use escrow_api::prelude::*; -use steel::*; +use steel::{transfer as transfer_spl_tokens, *}; pub fn process_take_offer(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { // Load accounts. - let [taker_info, maker_info, token_mint_a_info, token_mint_b_info, taker_token_account_a_info, taker_token_account_b_info, maker_token_account_b_info, system_program, token_program, associated_token_program] = + let [taker_info, maker_info, token_mint_a_info, token_mint_b_info, taker_token_account_a_info, taker_token_account_b_info, maker_token_account_b_info, offer_info, vault_info, system_program, token_program, associated_token_program] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; taker_info.is_signer()?; + maker_info.is_writable()?; + + let token_mint_a = token_mint_a_info + .has_address(&token_mint_a_info.key)? + .is_writable()? + .as_mint()?; + + token_mint_b_info + .has_address(&token_mint_b_info.key)? + .is_writable()? + .as_mint()?; + + taker_token_account_a_info.is_empty()?.is_writable()?; + + // create taker token account a + create_associated_token_account( + taker_info, + taker_info, + taker_token_account_a_info, + token_mint_a_info, + system_program, + token_program, + associated_token_program, + )?; + + taker_token_account_b_info + .as_associated_token_account(taker_info.key, token_mint_b_info.key)?; + + maker_token_account_b_info.is_empty()?.is_writable()?; + + // create maker token account b + create_associated_token_account( + taker_info, + maker_info, + maker_token_account_b_info, + token_mint_b_info, + system_program, + token_program, + associated_token_program, + )?; + + let offer = offer_info.as_account_mut::(&escrow_api::ID)?; + offer_info + .is_writable()? + .has_seeds( + &[ + OFFER, + maker_info.key.as_ref(), + offer.id.to_le_bytes().as_ref(), + ], + &escrow_api::ID, + )? + .is_type::(&escrow_api::ID)?; + + let vault = vault_info.as_associated_token_account(offer_info.key, token_mint_a_info.key)?; + + transfer_spl_tokens( + taker_info, + taker_token_account_b_info, + maker_token_account_b_info, + token_program, + offer.clone().token_b_wanted_amount, + )?; + + solana_program::program::invoke_signed( + &spl_token::instruction::transfer_checked( + token_program.key, + vault_info.key, + token_mint_a_info.key, + taker_token_account_a_info.key, + offer_info.key, + &[offer_info.key], + vault.amount, + token_mint_a.decimals, + )?, + &[ + vault_info.clone(), + token_mint_a_info.clone(), + taker_token_account_a_info.clone(), + offer_info.clone(), + ], + &[&[ + OFFER, + maker_info.key.as_ref(), + offer.id.to_le_bytes().as_ref(), + &[offer.bump], + ]], + )?; + + // close vault account + solana_program::program::invoke_signed( + &spl_token::instruction::close_account( + &spl_token::ID, + vault_info.key, + taker_info.key, + offer_info.key, + &[&offer_info.key], + )?, + &[vault_info.clone(), taker_info.clone(), offer_info.clone()], + &[&[ + OFFER, + maker_info.key.as_ref(), + offer.id.to_le_bytes().as_ref(), + &[offer.bump], + ]], + )?; + + // close offer account. + close_account(offer_info, maker_info)?; Ok(()) }