diff --git a/Cargo.lock b/Cargo.lock index 6a92fdce..98008133 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2393,10 +2393,13 @@ dependencies = [ "libreplex_shared", "mocha", "mpl-token-metadata", + "nifty-asset", "solana-program", "solana-program-test", "solana-sdk", "spl-associated-token-account", + "spl-token", + "spl-token-2022 1.0.0", ] [[package]] @@ -2690,6 +2693,34 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nifty-asset" +version = "0.0.1-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99238aae879eafc74679bfa512af21b5694ebfb0c485d5f709c4e33b2dd463a9" +dependencies = [ + "borsh 0.10.3", + "kaigan", + "nifty-asset-types", + "num-derive 0.3.3", + "num-traits", + "solana-program", + "thiserror", +] + +[[package]] +name = "nifty-asset-types" +version = "0.0.1-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b94791938a72017c1cb3a95eef3b593d8871013d108463b2478cbaef5e6818" +dependencies = [ + "borsh 0.10.3", + "bytemuck", + "podded", + "solana-program", + "thiserror", +] + [[package]] name = "nix" version = "0.26.4" @@ -3091,6 +3122,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "podded" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ef03855395ea7838e038410cf2be5dd419fa65560c3c82dfcf1ee5b32cfb4e" +dependencies = [ + "bytemuck", +] + [[package]] name = "polyval" version = "0.5.3" diff --git a/programs/libreplex_monoswap/Cargo.toml b/programs/libreplex_monoswap/Cargo.toml index 20f97fb8..6b7307a3 100644 --- a/programs/libreplex_monoswap/Cargo.toml +++ b/programs/libreplex_monoswap/Cargo.toml @@ -16,18 +16,27 @@ no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] default = [] +test-bpf = [] [dependencies] -anchor-lang = {version = "~0.29", features = ["init-if-needed"]} -anchor-spl = {version = "~0.29"} -libreplex_fair_launch = {version = "*", path="../libreplex_fair_launch", features =["cpi", "no-entrypoint"]} +anchor-lang = { version = "~0.29", features = ["init-if-needed"] } +anchor-spl = { version = "~0.29" } +libreplex_fair_launch = { version = "*", path = "../libreplex_fair_launch", features = [ + "cpi", + "no-entrypoint", +] } spl-associated-token-account = "2.2.0" -solana-program = {version = "1.17.22"} -libreplex_shared = {version = "*", path = "../libreplex_shared", features=["cpi"]} -mpl-token-metadata = { version="~3" } +solana-program = { version = "1.17.22" } +libreplex_shared = { version = "*", path = "../libreplex_shared", features = [ + "cpi", +] } +mpl-token-metadata = { version = "~3" } +nifty-asset = "0.0.1-alpha.3" [dev-dependencies] -mocha = "0.1.1" assert_matches = "1.5.0" +mocha = "0.1.1" solana-program-test = "1.17.22" solana-sdk = "1.17.22" +spl-token-2022 = "1.0" +spl-token = "4" diff --git a/programs/libreplex_monoswap/src/errors.rs b/programs/libreplex_monoswap/src/errors.rs index c2d19db8..e28d4832 100644 --- a/programs/libreplex_monoswap/src/errors.rs +++ b/programs/libreplex_monoswap/src/errors.rs @@ -1,36 +1,7 @@ use anchor_lang::prelude::*; #[error_code] -pub enum LegacyInscriptionErrorCode { - - #[msg("Metadata has a bad mint")] - BadMint, - - #[msg("Cannot inscribe a fungible asset")] - CannotInscribeFungible, - - #[msg("Bad authority")] - BadAuthority, - - #[msg("Bad authority for holder inscription")] - BadAuthorityForHolderInscription, - - #[msg("Bad authority for update auth inscription")] - BadAuthorityForUpdateAuthInscription, - - #[msg("Multi Signature threshold must be one to create / edit inscriptions")] - MultiSigThresholdMustBeOne, - - #[msg("Not squads member")] - NotSquadsMember, - - #[msg("Inscription V2 key mismatch")] - Inscription2KeyMismatch, - - #[msg("Inscription V3 key mismatch")] - InscriptionV3KeyMismatch, - - #[msg("Metadata data missmatch")] - DataHashMismatch, - +pub enum MonoSwapError { + #[msg("Invalid Marker State")] + InvalidMarkerState, } diff --git a/programs/libreplex_monoswap/src/instructions/create_monoswap.rs b/programs/libreplex_monoswap/src/instructions/create_monoswap.rs index 6ba0e3f3..a0222e45 100644 --- a/programs/libreplex_monoswap/src/instructions/create_monoswap.rs +++ b/programs/libreplex_monoswap/src/instructions/create_monoswap.rs @@ -1,51 +1,51 @@ - - use anchor_lang::prelude::*; -use anchor_spl::{associated_token::AssociatedToken, token::spl_token, token_interface::{Mint,TokenAccount, spl_token_2022}}; +use anchor_spl::{ + associated_token::AssociatedToken, + token::spl_token, + token_interface::{spl_token_2022, Mint, TokenAccount}, +}; use libreplex_shared::operations::transfer_generic_spl; use crate::SwapMarker; - #[derive(Clone, AnchorDeserialize, AnchorSerialize)] pub struct CreateMonoSwapInput { pub mint_outgoing_amount: u64, - pub mint_incoming_amount: u64 + pub mint_incoming_amount: u64, } #[derive(Accounts)] pub struct CreateMonoSwapCtx<'info> { - #[account(init, - payer = payer, + payer = payer, space = SwapMarker::SIZE, - seeds = ["swap_marker".as_bytes(), + seeds = ["swap_marker".as_bytes(), namespace.key().as_ref(), mint_outgoing.key().as_ref(), mint_incoming.key().as_ref()], // always indexed by the incoming mint bump,)] pub swap_marker: Account<'info, SwapMarker>, - #[account(mut)] + #[account(mut)] pub mint_incoming: InterfaceAccount<'info, Mint>, - // each mint has to exist - there must be enough - pub mint_outgoing: InterfaceAccount<'info, Mint>, + // each mint has to exist - there must be enough + pub mint_outgoing: InterfaceAccount<'info, Mint>, // it is the responsibility of each swapper program to create enough - // of the outgoing mint so that the swap can happen. It is deposited + // of the outgoing mint so that the swap can happen. It is deposited // from this account #[account(mut, associated_token::mint = mint_outgoing, associated_token::authority = mint_outgoing_owner - )] + )] pub mint_outgoing_token_account_source: InterfaceAccount<'info, TokenAccount>, - - // escrow holders are organised by namespace + incoming mint - + + // escrow holders are organised by namespace + incoming mint - // that way you can get wallet contents to see what swaps are available to you /// CHECK: Checked in transfer logic #[account( - seeds = ["swap_escrow".as_bytes(), + seeds = ["swap_escrow".as_bytes(), namespace.key().as_ref(), mint_incoming.key().as_ref()], // always indexed by the incoming mint bump)] @@ -60,7 +60,6 @@ pub struct CreateMonoSwapCtx<'info> { )] pub mint_outgoing_token_account_escrow: InterfaceAccount<'info, TokenAccount>, - #[account(mut)] pub payer: Signer<'info>, @@ -68,7 +67,7 @@ pub struct CreateMonoSwapCtx<'info> { #[account(mut)] pub mint_outgoing_owner: Signer<'info>, - // any account that can sign this. this is useful for grouping swaps + // any account that can sign this. this is useful for grouping swaps pub namespace: Signer<'info>, /// CHECK: Checked in constraint @@ -81,14 +80,14 @@ pub struct CreateMonoSwapCtx<'info> { #[account()] pub system_program: Program<'info, System>, - } -pub fn create_swap(ctx: Context, input: CreateMonoSwapInput) -> Result<()> { - +pub fn process_create_swap( + ctx: Context, + input: CreateMonoSwapInput, +) -> Result<()> { let swap_marker = &mut ctx.accounts.swap_marker; let mint_outgoing = &mut ctx.accounts.mint_outgoing; - swap_marker.namespace = ctx.accounts.namespace.key(); swap_marker.mint_incoming = ctx.accounts.mint_incoming.key(); @@ -98,7 +97,7 @@ pub fn create_swap(ctx: Context, input: CreateMonoSwapInput) swap_marker.used = false; - // transfer the outgoing mint into escrow - + // transfer the outgoing mint into escrow - let token_program = &ctx.accounts.token_program; let mint_outgoing_token_account_source = &ctx.accounts.mint_outgoing_token_account_source; let mint_outgoing_token_account_escrow = &ctx.accounts.mint_outgoing_token_account_escrow; @@ -124,7 +123,6 @@ pub fn create_swap(ctx: Context, input: CreateMonoSwapInput) mint_outgoing.decimals, input.mint_outgoing_amount, )?; - Ok(()) } diff --git a/programs/libreplex_monoswap/src/instructions/create_nifty_swap.rs b/programs/libreplex_monoswap/src/instructions/create_nifty_swap.rs new file mode 100644 index 00000000..f6d2229d --- /dev/null +++ b/programs/libreplex_monoswap/src/instructions/create_nifty_swap.rs @@ -0,0 +1,110 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; +use libreplex_shared::operations::transfer_generic_spl; + +use crate::{MarkerState, NiftyMarker}; + +// Swaps are created by transferring a token in. +#[derive(Accounts)] +pub struct CreateNiftySwapCtx<'info> { + // any account that can sign this. this is useful for grouping swaps + pub namespace: Signer<'info>, + + #[account(mut)] + pub payer: Signer<'info>, + + #[account(init, + payer = payer, + space = 8 + NiftyMarker::INIT_SPACE, + seeds = [ + "nifty_marker".as_bytes(), + namespace.key().as_ref(), + asset.key().as_ref(), + mint.key().as_ref() + ], + bump, + )] + pub nifty_marker: Account<'info, NiftyMarker>, + + #[account( + constraint = asset.owner == nifty_program.key + )] + pub asset: UncheckedAccount<'info>, + + pub mint: InterfaceAccount<'info, Mint>, + + // escrow holders are organised by namespace + incoming mint - + // that way you can get wallet contents to see what swaps are available to you + /// CHECK: Checked in transfer logic + #[account( + seeds = [ + "nifty_escrow".as_bytes(), + namespace.key().as_ref(), + asset.key().as_ref(), + mint.key().as_ref(), + ], // always indexed by the incoming mint + bump + )] + pub escrow_owner: UncheckedAccount<'info>, + + #[account( + init, + payer = payer, + associated_token::mint = mint, + associated_token::authority = escrow_owner, + token::token_program = token_program, + )] + pub escrow_token_account: InterfaceAccount<'info, TokenAccount>, + + #[account( + mut, + token::mint = mint, + token::authority = payer, + token::token_program = token_program, + )] + pub source_token_account: InterfaceAccount<'info, TokenAccount>, + + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, + + #[account( + address = nifty_asset::ID, + )] + pub nifty_program: UncheckedAccount<'info>, +} + +pub fn process_create_nifty_swap(ctx: Context, amount: u64) -> Result<()> { + let swap_marker = &mut ctx.accounts.nifty_marker; + let mint = &ctx.accounts.mint; + + swap_marker.namespace = ctx.accounts.namespace.key(); + swap_marker.mint = ctx.accounts.mint.key(); + swap_marker.asset = ctx.accounts.asset.key(); + swap_marker.amount = amount; + swap_marker.state = MarkerState::FungibleEscrowed; + + // transfer the outgoing mint into escrow - + let token_program = &ctx.accounts.token_program; + let associated_token_program = &ctx.accounts.associated_token_program; + + transfer_generic_spl( + &token_program.to_account_info(), + &ctx.accounts.source_token_account.to_account_info(), + &ctx.accounts.escrow_token_account.to_account_info(), + &ctx.accounts.payer.to_account_info(), + &mint.to_account_info(), + &ctx.accounts.escrow_owner.to_account_info(), + &associated_token_program.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + None, // payer signs + &ctx.accounts.payer.to_account_info(), + mint.decimals, + amount, + )?; + + Ok(()) +} diff --git a/programs/libreplex_monoswap/src/instructions/mod.rs b/programs/libreplex_monoswap/src/instructions/mod.rs index d350e338..ce41c9c0 100644 --- a/programs/libreplex_monoswap/src/instructions/mod.rs +++ b/programs/libreplex_monoswap/src/instructions/mod.rs @@ -1,7 +1,11 @@ - pub mod create_monoswap; pub use create_monoswap::*; +pub mod create_nifty_swap; +pub use create_nifty_swap::*; pub mod swap; -pub use swap::*; \ No newline at end of file +pub use swap::*; + +pub mod nifty_swap; +pub use nifty_swap::*; diff --git a/programs/libreplex_monoswap/src/instructions/nifty_swap.rs b/programs/libreplex_monoswap/src/instructions/nifty_swap.rs new file mode 100644 index 00000000..c26e749d --- /dev/null +++ b/programs/libreplex_monoswap/src/instructions/nifty_swap.rs @@ -0,0 +1,168 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; +use libreplex_shared::operations::transfer_generic_spl; +use nifty_asset::instructions::TransferCpi; + +use crate::{MarkerState, MonoSwapError, NiftyMarker, SwapDirection}; + +// the swap endpoint is symmetrical. +// it can be used to swap back and forth +#[derive(Accounts)] +pub struct NiftySwapCtx<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + #[account(mut, + seeds = [ + "nifty_marker".as_bytes(), + nifty_marker.namespace.as_ref(), + asset.key().as_ref(), + mint.key().as_ref(), + ], + bump, + has_one = mint, + has_one = asset, + )] + pub nifty_marker: Account<'info, NiftyMarker>, + + #[account( + mut, + constraint = asset.owner == nifty_program.key + )] + pub asset: UncheckedAccount<'info>, + + // each mint has to exist - there must be enough + pub mint: InterfaceAccount<'info, Mint>, + + /// CHECK: Check in pda derivation + #[account( + seeds = [ + "nifty_escrow".as_bytes(), + nifty_marker.namespace.key().as_ref(), + asset.key().as_ref(), + mint.key().as_ref(), + ], // always indexed by the incoming mint + bump + )] + pub escrow_owner: UncheckedAccount<'info>, + + #[account( + mut, + associated_token::mint = mint, + associated_token::authority = escrow_owner + // token::token_program = token_program, + )] + pub escrow_token_account: InterfaceAccount<'info, TokenAccount>, + + // it is the responsibility of each swapper program to create enough + // of the outgoing mint so that the swap can happen. It is deposited + // from this account + #[account( + mut, + associated_token::mint = mint, + associated_token::authority = payer + // token::token_program = token_program, + )] + pub external_token_account: InterfaceAccount<'info, TokenAccount>, + + pub token_program: Interface<'info, TokenInterface>, + // pub token_program: Program<'info, Token>, + // pub token_program_2022: Program<'info, Token2022>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, + + #[account( + address = nifty_asset::ID, + )] + pub nifty_program: UncheckedAccount<'info>, +} + +pub fn process_nifty_swap(ctx: Context, direction: SwapDirection) -> Result<()> { + let nifty_marker = &mut ctx.accounts.nifty_marker; + let asset_pubkey = ctx.accounts.asset.key(); + let mint_pubkey = ctx.accounts.mint.key(); + + let authority_seeds: &[&[u8]] = &[ + "nifty_escrow".as_bytes(), + nifty_marker.namespace.as_ref(), + asset_pubkey.as_ref(), + mint_pubkey.as_ref(), + &[ctx.bumps.escrow_owner], + ]; + + // Determine the direction of the swap + match direction { + // Nifty asset comes in, fungibles go out + SwapDirection::AssetIn => { + require!( + nifty_marker.state == MarkerState::FungibleEscrowed, + MonoSwapError::InvalidMarkerState + ); + + // Nifty Transfer from payer to escrow_owner + TransferCpi { + __program: &ctx.accounts.nifty_program.to_account_info(), + asset: &ctx.accounts.asset.to_account_info(), + signer: &ctx.accounts.payer.to_account_info(), + recipient: &ctx.accounts.escrow_owner.to_account_info(), + group_asset: None, + } + .invoke()?; + + // Transfer fungible from escrow to payer + transfer_generic_spl( + &ctx.accounts.token_program.to_account_info(), + &ctx.accounts.escrow_token_account.to_account_info(), + &ctx.accounts.external_token_account.to_account_info(), + &ctx.accounts.escrow_owner.to_account_info(), + &ctx.accounts.mint.to_account_info(), + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.associated_token_program.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + Some(&[&authority_seeds]), // payer signs + &ctx.accounts.payer.to_account_info(), + ctx.accounts.mint.decimals, + nifty_marker.amount, + )?; + + // Change nifty marker state + nifty_marker.state = MarkerState::AssetEscrowed; + } + // Fungibles come in, nifty asset goes out + SwapDirection::AssetOut => { + // Nifty Transfer from escrow_owner to payer + TransferCpi { + __program: &ctx.accounts.nifty_program.to_account_info(), + asset: &ctx.accounts.asset.to_account_info(), + signer: &ctx.accounts.escrow_owner.to_account_info(), + recipient: &ctx.accounts.payer.to_account_info(), + group_asset: None, + } + .invoke_signed(&[authority_seeds])?; + + // Transfer fungible from payer to escrow + transfer_generic_spl( + &ctx.accounts.token_program.to_account_info(), + &ctx.accounts.external_token_account.to_account_info(), + &ctx.accounts.escrow_token_account.to_account_info(), + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.mint.to_account_info(), + &ctx.accounts.escrow_owner.to_account_info(), + &ctx.accounts.associated_token_program.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + None, // payer signs + &ctx.accounts.payer.to_account_info(), + ctx.accounts.mint.decimals, + nifty_marker.amount, + )?; + + // Change nifty marker state + nifty_marker.state = MarkerState::FungibleEscrowed; + } + } + + Ok(()) +} diff --git a/programs/libreplex_monoswap/src/instructions/swap.rs b/programs/libreplex_monoswap/src/instructions/swap.rs index 5834fafb..37132737 100644 --- a/programs/libreplex_monoswap/src/instructions/swap.rs +++ b/programs/libreplex_monoswap/src/instructions/swap.rs @@ -12,12 +12,12 @@ use crate::SwapMarker; // it can be used to swap back and forth #[derive(Accounts)] pub struct SwapCtx<'info> { - #[account(mut, + #[account(mut, close = payer, constraint = mint_incoming.key() == swap_marker.mint_incoming, constraint = mint_outgoing.key() == swap_marker.mint_outgoing, seeds = [ - "swap_marker".as_bytes(), + "swap_marker".as_bytes(), swap_marker.namespace.as_ref(), mint_outgoing.key().as_ref(), mint_incoming.key().as_ref()], @@ -26,9 +26,9 @@ pub struct SwapCtx<'info> { /// swapping always creates a symmetrical swap marker that enables a swap back #[account(init, - payer = payer, + payer = payer, space = SwapMarker::SIZE, - seeds = ["swap_marker".as_bytes(), + seeds = ["swap_marker".as_bytes(), swap_marker.namespace.as_ref(), mint_incoming.key().as_ref(), mint_outgoing.key().as_ref()], // always indexed by the incoming mint @@ -44,7 +44,7 @@ pub struct SwapCtx<'info> { #[account()] pub mint_outgoing: InterfaceAccount<'info, Mint>, - // it is the responsibility of each swapper program to create enough + // it is the responsibility of each swapper program to create enough // of the outgoing mint so that the swap can happen. It is deposited // from this account #[account(mut, @@ -55,26 +55,25 @@ pub struct SwapCtx<'info> { /// CHECK: Check in pda derivation #[account( - seeds = ["swap_escrow".as_bytes(), + seeds = ["swap_escrow".as_bytes(), swap_marker.namespace.as_ref(), mint_incoming.key().as_ref()], // always indexed by the incoming mint bump)] pub escrow_holder: UncheckedAccount<'info>, - /// CHECK: Check in pda derivation - #[account( - seeds = ["swap_escrow".as_bytes(), + /// CHECK: Check in pda derivation + #[account( + seeds = ["swap_escrow".as_bytes(), swap_marker.namespace.as_ref(), mint_outgoing.key().as_ref()], // always indexed by the incoming mint bump)] - pub escrow_holder_reverse: UncheckedAccount<'info>, + pub escrow_holder_reverse: UncheckedAccount<'info>, // ... into this escrow account /// CHECK: Checked in transfer logic #[account(mut)] pub mint_incoming_token_account_target: UncheckedAccount<'info>, - // it is the responsibility of each swapper program to create enough // of the outgoing mint so that the swap can happen. It is deposited // from this account @@ -100,28 +99,26 @@ pub struct SwapCtx<'info> { pub associated_token_program: Program<'info, AssociatedToken>, - #[account()] pub system_program: Program<'info, System>, } -pub fn swap(ctx: Context) -> Result<()> { - - +pub fn process_swap(ctx: Context) -> Result<()> { let swap_marker_reverse = &mut ctx.accounts.swap_marker_reverse; let mint_incoming = &mut ctx.accounts.mint_incoming; let mint_outgoing = &mut ctx.accounts.mint_outgoing; let escrow_holder_reverse = &ctx.accounts.escrow_holder_reverse; let swap_marker = &ctx.accounts.swap_marker; - swap_marker_reverse.set_inner(SwapMarker { - namespace: swap_marker.namespace.key(), - mint_incoming: mint_outgoing.key(), - mint_outgoing: mint_incoming.key(), - mint_incoming_amount: swap_marker.mint_outgoing_amount, - mint_outgoing_amount: swap_marker.mint_incoming_amount, - used: true }); - + swap_marker_reverse.set_inner(SwapMarker { + namespace: swap_marker.namespace.key(), + mint_incoming: mint_outgoing.key(), + mint_outgoing: mint_incoming.key(), + mint_incoming_amount: swap_marker.mint_outgoing_amount, + mint_outgoing_amount: swap_marker.mint_incoming_amount, + used: true, + }); + // transfer the outgoing mint into escrow - let token_program = &ctx.accounts.token_program; let mint_outgoing_token_account_source = &ctx.accounts.mint_outgoing_token_account_source; @@ -142,7 +139,6 @@ pub fn swap(ctx: Context) -> Result<()> { &[ctx.bumps.escrow_holder], ]; - let payer = &ctx.accounts.payer; // outgoing is going to the payer @@ -182,6 +178,5 @@ pub fn swap(ctx: Context) -> Result<()> { swap_marker.mint_incoming_amount, )?; - Ok(()) } diff --git a/programs/libreplex_monoswap/src/lib.rs b/programs/libreplex_monoswap/src/lib.rs index 7a5185a4..6013d9a0 100644 --- a/programs/libreplex_monoswap/src/lib.rs +++ b/programs/libreplex_monoswap/src/lib.rs @@ -9,25 +9,30 @@ pub mod state; pub use errors::*; pub use constants::*; -pub use state::*; pub use instructions::*; +pub use state::*; #[program] pub mod libreplex_monoswap { + use super::*; pub fn create_monoswap( ctx: Context, input: CreateMonoSwapInput, ) -> Result<()> { - instructions::create_monoswap::create_swap(ctx, input) + process_create_swap(ctx, input) } + pub fn swap(ctx: Context) -> Result<()> { + process_swap(ctx) + } - pub fn swap( - ctx: Context, - ) -> Result<()> { - instructions::swap::swap(ctx) + pub fn create_nifty_swap(ctx: Context, amount: u64) -> Result<()> { + process_create_nifty_swap(ctx, amount) } + pub fn nifty_swap(ctx: Context, direction: SwapDirection) -> Result<()> { + process_nifty_swap(ctx, direction) + } } diff --git a/programs/libreplex_monoswap/src/state/mod.rs b/programs/libreplex_monoswap/src/state/mod.rs index ffd9f08b..5fa44982 100644 --- a/programs/libreplex_monoswap/src/state/mod.rs +++ b/programs/libreplex_monoswap/src/state/mod.rs @@ -1,3 +1,5 @@ - pub use swap_marker::*; -pub mod swap_marker; \ No newline at end of file +pub mod swap_marker; + +pub mod nifty_marker; +pub use nifty_marker::*; diff --git a/programs/libreplex_monoswap/src/state/nifty_marker.rs b/programs/libreplex_monoswap/src/state/nifty_marker.rs new file mode 100644 index 00000000..6fa4ac61 --- /dev/null +++ b/programs/libreplex_monoswap/src/state/nifty_marker.rs @@ -0,0 +1,29 @@ +use anchor_lang::prelude::*; +use anchor_lang::{AnchorDeserialize, AnchorSerialize}; + +#[account] +#[derive(InitSpace)] +pub struct NiftyMarker { + pub namespace: Pubkey, + // The Nifty asset associated with this marker + pub asset: Pubkey, + // The mint associated with this marker + pub mint: Pubkey, + // The state of the marker indicating which type of asset is currently escrowed + pub state: MarkerState, + // The amount of the fungible token escrowed + pub amount: u64, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq, InitSpace)] +pub enum MarkerState { + Uninitialized, + AssetEscrowed, + FungibleEscrowed, +} + +#[derive(Clone, AnchorDeserialize, AnchorSerialize)] +pub enum SwapDirection { + AssetIn, + AssetOut, +} diff --git a/programs/libreplex_monoswap/src/state/swap_marker.rs b/programs/libreplex_monoswap/src/state/swap_marker.rs index 9fe00b68..c05cb1ee 100644 --- a/programs/libreplex_monoswap/src/state/swap_marker.rs +++ b/programs/libreplex_monoswap/src/state/swap_marker.rs @@ -11,7 +11,7 @@ pub struct SwapMarker { pub namespace: Pubkey, // allows slicing and dicing by incoming mint pub mint_incoming: Pubkey, - // allows slicing and dicing by outgoing mint + // allows slicing and dicing by outgoing mint pub mint_outgoing: Pubkey, pub mint_incoming_amount: u64, pub mint_outgoing_amount: u64, @@ -20,10 +20,9 @@ pub struct SwapMarker { // closed to avoid a situation where a // holder gets trapped into a crappy token // and cannot go back - pub used: bool + pub used: bool, } impl SwapMarker { pub const SIZE: usize = 8 + 32 + 32 + 32 + 8 + 8 + 1; } - diff --git a/programs/libreplex_monoswap/tests/create_nifty_swap_test.rs b/programs/libreplex_monoswap/tests/create_nifty_swap_test.rs new file mode 100644 index 00000000..6e5f6588 --- /dev/null +++ b/programs/libreplex_monoswap/tests/create_nifty_swap_test.rs @@ -0,0 +1,249 @@ +#![cfg(feature = "test-bpf")] + +use anchor_lang::{prelude::*, InstructionData}; +use libreplex_monoswap::{accounts::CreateNiftySwapCtx, instruction::CreateNiftySwap, NiftyMarker}; +use nifty_asset::{accounts::Asset, types::Standard}; +use solana_program::program_pack::Pack; +use solana_program_test::*; +use solana_sdk::{ + instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer, system_program, + transaction::Transaction, +}; +use spl_associated_token_account::get_associated_token_address_with_program_id; +use spl_token_2022::{ + instruction::{initialize_mint2, mint_to}, + state::Account as TokenAccount, +}; + +pub mod helpers; +use helpers::*; + +mod create_nifty_swap_tests { + use super::*; + + #[tokio::test] + async fn can_create_new_nifty_swap_with_legacy_token() { + let mut context = program_test().start_with_context().await; + + // **Set up keypairs and funding** + + // Authority creates the fungible and the nifty swap account + let authority_signer = Keypair::new(); + let authority = authority_signer.pubkey(); + + // User owns the Nifty asset and does the swapping + let user_signer = Keypair::new(); + let user = user_signer.pubkey(); + + // Fund the authority and user + airdrop(&mut context, &authority, 1_000_000_000) + .await + .unwrap(); + airdrop(&mut context, &user, 1_000_000_000).await.unwrap(); + + // **Create Nifty and fungible tokens** + + let FungibleTest { mint, ata } = + create_fungible_token(&mut context, &authority_signer, 10, TokenProgram::Legacy) + .await + .unwrap(); + + let AssetTest { asset } = create_nifty_asset(&mut context, &user_signer, user) + .await + .unwrap(); + + let asset_account = context + .banks_client + .get_account(asset) + .await + .expect("get_account") + .expect("asset_account not found"); + + let asset_data = Asset::deserialize(&mut asset_account.data.as_slice()).unwrap(); + assert_eq!(asset_data.owner, user); + assert_eq!(asset_data.authority, user); + assert_eq!(asset_data.standard, Standard::NonFungible); + assert_eq!(asset_data.mutable, true); + + let authority_token_account = context + .banks_client + .get_account(ata) + .await + .expect("get_account") + .expect("authority_token_account not found"); + + let ata_data = spl_token::state::Account::unpack(&authority_token_account.data).unwrap(); + assert_eq!(ata_data.amount, 10); + assert_eq!(ata_data.owner, authority); + assert_eq!(ata_data.mint, mint); + + context.warp_to_slot(100).unwrap(); + + let NiftySwapTest { + nifty_marker, + escrow_owner, + escrow_ata, + } = create_nifty_swap( + &mut context, + NiftySwapInput { + authority_signer: &authority_signer, + asset, + mint, + ata, + }, + ) + .await + .unwrap(); + + let nifty_marker_account = context + .banks_client + .get_account(nifty_marker) + .await + .expect("get_account") + .expect("nifty_marker_account not found"); + + let nifty_marker_data = + NiftyMarker::deserialize(&mut &nifty_marker_account.data.as_slice()[8..]).unwrap(); + + assert_eq!(nifty_marker_data.namespace, authority); + assert_eq!(nifty_marker_data.mint, mint); + assert_eq!(nifty_marker_data.amount, 10); + } + + #[tokio::test] + async fn can_create_new_nifty_swap_with_token_2022() { + let mut context = program_test().start_with_context().await; + + // **Set up keypairs and funding** + + // Authority creates the fungible and the nifty swap account + let authority_signer = Keypair::new(); + let authority = authority_signer.pubkey(); + + // User owns the Nifty asset and does the swapping + let user_signer = Keypair::new(); + let user = user_signer.pubkey(); + + // Fund the authority and user + airdrop(&mut context, &authority, 1_000_000_000) + .await + .unwrap(); + airdrop(&mut context, &user, 1_000_000_000).await.unwrap(); + + // **Create Nifty and fungible tokens** + + let FungibleTest { mint, ata } = + create_fungible_token(&mut context, &authority_signer, 10, TokenProgram::T22) + .await + .unwrap(); + + let AssetTest { asset } = create_nifty_asset(&mut context, &user_signer, user) + .await + .unwrap(); + + let asset_account = context + .banks_client + .get_account(asset) + .await + .expect("get_account") + .expect("asset_account not found"); + + let asset_data = Asset::deserialize(&mut asset_account.data.as_slice()).unwrap(); + assert_eq!(asset_data.owner, user); + assert_eq!(asset_data.authority, user); + assert_eq!(asset_data.standard, Standard::NonFungible); + assert_eq!(asset_data.mutable, true); + + let authority_token_account = context + .banks_client + .get_account(ata) + .await + .expect("get_account") + .expect("authority_token_account not found"); + + let ata_data = unpack::(&authority_token_account.data).unwrap(); + + assert_eq!(ata_data.amount, 10); + assert_eq!(ata_data.owner, authority); + assert_eq!(ata_data.mint, mint); + + context.warp_to_slot(100).unwrap(); + + let nifty_marker = Pubkey::find_program_address( + &[ + b"nifty_marker", + authority.as_ref(), + asset.as_ref(), + mint.as_ref(), + ], + &libreplex_monoswap::ID, + ) + .0; + + // Monoswap nifty escrow owner pda + let escrow_owner = Pubkey::find_program_address( + &[ + b"nifty_escrow", + authority.as_ref(), + asset.as_ref(), + mint.as_ref(), + ], + &libreplex_monoswap::ID, + ) + .0; + + // Associated token accounts for the authority and escrow + let ata_escrow = + get_associated_token_address_with_program_id(&escrow_owner, &mint, &spl_token_2022::ID); + + // **Create the Nifty swap account** + let create_swap_ix = Instruction { + program_id: libreplex_monoswap::ID, + accounts: CreateNiftySwapCtx { + namespace: authority, + payer: authority, + nifty_marker, + asset, + mint, + escrow_owner, + escrow_token_account: ata_escrow, + source_token_account: ata, + token_program: spl_token_2022::ID, + associated_token_program: spl_associated_token_account::ID, + system_program: system_program::ID, + nifty_program: nifty_asset::ID, + } + .to_account_metas(None), + data: CreateNiftySwap { amount: 10 }.data(), + }; + + println!("source ata: {:?}", ata); + println!("authority: {:?}", authority); + println!("mint: {:?}", mint); + + let blockhash = context.get_new_latest_blockhash().await.unwrap(); + + let tx = Transaction::new_signed_with_payer( + &[create_swap_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &authority_signer], + blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + let nifty_marker_account = context + .banks_client + .get_account(nifty_marker) + .await + .expect("get_account") + .expect("nifty_marker_account not found"); + + let nifty_marker_data = + NiftyMarker::deserialize(&mut &nifty_marker_account.data.as_slice()[8..]).unwrap(); + + assert_eq!(nifty_marker_data.namespace, authority); + assert_eq!(nifty_marker_data.mint, mint); + assert_eq!(nifty_marker_data.amount, 10); + } +} diff --git a/programs/libreplex_monoswap/tests/helpers.rs b/programs/libreplex_monoswap/tests/helpers.rs new file mode 100644 index 00000000..a0564b10 --- /dev/null +++ b/programs/libreplex_monoswap/tests/helpers.rs @@ -0,0 +1,255 @@ +#![cfg(feature = "test-bpf")] + +use anchor_lang::{prelude::*, InstructionData}; +use libreplex_monoswap::{accounts::CreateNiftySwapCtx, instruction::CreateNiftySwap}; +use nifty_asset::{accounts::Asset, instructions::CreateBuilder, types::Standard}; +use solana_program::program_pack::Pack; +use solana_program_test::*; +use solana_sdk::{ + instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer, + system_instruction, system_program, transaction::Transaction, +}; +use spl_associated_token_account::{ + get_associated_token_address_with_program_id, instruction::create_associated_token_account, +}; +use spl_token_2022::{ + extension::{BaseState, StateWithExtensions}, + instruction::{initialize_mint2, mint_to}, +}; + +pub const MINT_LAYOUT: u64 = 82; + +pub fn unpack(account_data: &[u8]) -> Result { + Ok(StateWithExtensions::::unpack(account_data)?.base) +} + +pub fn program_test() -> ProgramTest { + let mut test = ProgramTest::new("libreplex_monoswap", libreplex_monoswap::ID, None); + test.add_program("nifty_asset", nifty_asset::ID, None); + test.add_program("spl_token_2022", spl_token_2022::ID, None); + + test +} + +pub async fn airdrop( + context: &mut ProgramTestContext, + receiver: &Pubkey, + amount: u64, +) -> Result<()> { + let tx = Transaction::new_signed_with_payer( + &[system_instruction::transfer( + &context.payer.pubkey(), + receiver, + amount, + )], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + Ok(()) +} + +pub struct FungibleTest { + pub mint: Pubkey, + pub ata: Pubkey, +} + +pub struct AssetTest { + pub asset: Pubkey, +} + +pub enum TokenProgram { + Legacy, + T22, +} + +// Create a simple Nifty asset with no extensions. +pub async fn create_nifty_asset( + context: &mut ProgramTestContext, + authority_signer: &Keypair, + owner: Pubkey, +) -> Result { + let asset_signer = Keypair::new(); + let asset = asset_signer.pubkey(); + let authority = authority_signer.pubkey(); + + let nifty_ix = CreateBuilder::new() + .asset(asset) + .authority(authority, true) + .owner(owner) + .payer(Some(authority)) + .system_program(Some(system_program::ID)) + .name("TestNifty".to_string()) + .standard(Standard::NonFungible) + .mutable(true) + .instruction(); + + let signers = vec![&context.payer, &authority_signer, &asset_signer]; + + let tx = Transaction::new_signed_with_payer( + &[nifty_ix], + Some(&context.payer.pubkey()), + &signers, + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + return Ok(AssetTest { asset }); +} + +pub async fn create_fungible_token( + context: &mut ProgramTestContext, + authority_signer: &Keypair, + amount: u64, + token_program: TokenProgram, +) -> Result { + let mint_signer = Keypair::new(); + let mint = mint_signer.pubkey(); + + let rent = context.banks_client.get_rent().await.unwrap(); + let min_rent = rent.minimum_balance(MINT_LAYOUT as usize); + + let authority = authority_signer.pubkey(); + + let token_program = match token_program { + TokenProgram::Legacy => spl_token::ID, + TokenProgram::T22 => spl_token_2022::ID, + }; + + let ata = get_associated_token_address_with_program_id(&authority, &mint, &token_program); + + // Create mint account + let create_mint_account_ix = system_instruction::create_account( + &authority, + &mint, + min_rent, + MINT_LAYOUT, + &token_program, + ); + + // Initalize mint ix + let init_mint_ix = + initialize_mint2(&token_program, &mint, &authority, Some(&authority), 0).unwrap(); + + // Create associated account instruction + let create_assoc_account_ix = + create_associated_token_account(&authority, &authority, &mint, &token_program); + + // Mint to instruction + let mint_to_ix = mint_to(&token_program, &mint, &ata, &authority, &[], 10).unwrap(); + + // **Compose tranasaction, send it and assert the results** + + let instructions = vec![ + create_mint_account_ix, + init_mint_ix, + create_assoc_account_ix, + mint_to_ix, + ]; + + let signers = vec![&context.payer, &authority_signer, &mint_signer]; + + let tx = Transaction::new_signed_with_payer( + &instructions, + Some(&context.payer.pubkey()), + &signers, + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + return Ok(FungibleTest { mint, ata }); +} + +pub struct NiftySwapInput<'a> { + pub authority_signer: &'a Keypair, + pub asset: Pubkey, + pub mint: Pubkey, + pub ata: Pubkey, +} + +pub struct NiftySwapTest { + pub nifty_marker: Pubkey, + pub escrow_owner: Pubkey, + pub escrow_ata: Pubkey, +} + +pub async fn create_nifty_swap<'a>( + context: &mut ProgramTestContext, + input: NiftySwapInput<'a>, +) -> Result { + let NiftySwapInput { + authority_signer, + asset, + mint, + ata, + } = input; + + let authority = authority_signer.pubkey(); + + let nifty_marker = Pubkey::find_program_address( + &[ + b"nifty_marker", + authority.as_ref(), + asset.as_ref(), + mint.as_ref(), + ], + &libreplex_monoswap::ID, + ) + .0; + + // Monoswap nifty escrow owner pda + let escrow_owner = Pubkey::find_program_address( + &[ + b"nifty_escrow", + authority.as_ref(), + asset.as_ref(), + mint.as_ref(), + ], + &libreplex_monoswap::ID, + ) + .0; + + // Associated token accounts for the authority and escrow + let escrow_ata = + get_associated_token_address_with_program_id(&escrow_owner, &mint, &spl_token::ID); + + // **Create the Nifty swap account** + let create_swap_ix = Instruction { + program_id: libreplex_monoswap::ID, + accounts: CreateNiftySwapCtx { + namespace: authority, + payer: authority, + nifty_marker, + asset, + mint, + escrow_owner, + escrow_token_account: escrow_ata, + source_token_account: ata, + token_program: spl_token::ID, + associated_token_program: spl_associated_token_account::ID, + system_program: system_program::ID, + nifty_program: nifty_asset::ID, + } + .to_account_metas(None), + data: CreateNiftySwap { amount: 10 }.data(), + }; + + let blockhash = context.get_new_latest_blockhash().await.unwrap(); + + let tx = Transaction::new_signed_with_payer( + &[create_swap_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &authority_signer], + blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + Ok(NiftySwapTest { + nifty_marker, + escrow_owner, + escrow_ata, + }) +} diff --git a/programs/libreplex_monoswap/tests/nifty_swap_test.rs b/programs/libreplex_monoswap/tests/nifty_swap_test.rs new file mode 100644 index 00000000..c65e7d88 --- /dev/null +++ b/programs/libreplex_monoswap/tests/nifty_swap_test.rs @@ -0,0 +1,271 @@ +#![cfg(feature = "test-bpf")] + +use anchor_lang::{prelude::*, InstructionData}; +use libreplex_monoswap::{accounts::NiftySwapCtx, MarkerState, NiftyMarker, SwapDirection}; +use nifty_asset::{accounts::Asset, types::Standard}; +use solana_program::program_pack::Pack; +use solana_program_test::*; +use solana_sdk::{ + instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer, + system_instruction, system_program, transaction::Transaction, +}; +use spl_associated_token_account::{ + get_associated_token_address_with_program_id, instruction::create_associated_token_account, +}; +use spl_token_2022::{ + instruction::{initialize_mint2, mint_to}, + state::Account as TokenAccount, +}; + +pub mod helpers; +use helpers::*; + +mod nifty_swap_tests { + use super::*; + + #[tokio::test] + async fn can_swap_both_directions() { + let mut context = program_test().start_with_context().await; + + // **Set up keypairs and funding** + + // Authority creates the fungible and the nifty swap account + let authority_signer = Keypair::new(); + let authority = authority_signer.pubkey(); + + // User owns the Nifty asset and does the swapping + let user_signer = Keypair::new(); + let user = user_signer.pubkey(); + + // Fund the authority and user + airdrop(&mut context, &authority, 1_000_000_000) + .await + .unwrap(); + airdrop(&mut context, &user, 1_000_000_000).await.unwrap(); + + // **Create Nifty and fungible tokens** + + let FungibleTest { mint, ata } = + create_fungible_token(&mut context, &authority_signer, 10, TokenProgram::Legacy) + .await + .unwrap(); + + let AssetTest { asset } = create_nifty_asset(&mut context, &user_signer, user) + .await + .unwrap(); + + let asset_account = context + .banks_client + .get_account(asset) + .await + .expect("get_account") + .expect("asset_account not found"); + + let asset_data = Asset::deserialize(&mut asset_account.data.as_slice()).unwrap(); + assert_eq!(asset_data.owner, user); + assert_eq!(asset_data.authority, user); + assert_eq!(asset_data.standard, Standard::NonFungible); + assert_eq!(asset_data.mutable, true); + + let authority_token_account = context + .banks_client + .get_account(ata) + .await + .expect("get_account") + .expect("authority_token_account not found"); + + let ata_data = spl_token::state::Account::unpack(&authority_token_account.data).unwrap(); + assert_eq!(ata_data.amount, 10); + assert_eq!(ata_data.owner, authority); + assert_eq!(ata_data.mint, mint); + + context.warp_to_slot(100).unwrap(); + + let NiftySwapTest { + nifty_marker, + escrow_owner, + escrow_ata, + } = create_nifty_swap( + &mut context, + NiftySwapInput { + authority_signer: &authority_signer, + asset, + mint, + ata, + }, + ) + .await + .unwrap(); + + let nifty_marker_account = context + .banks_client + .get_account(nifty_marker) + .await + .expect("get_account") + .expect("nifty_marker_account not found"); + + let nifty_marker_data = + NiftyMarker::deserialize(&mut &nifty_marker_account.data.as_slice()[8..]).unwrap(); + + assert_eq!(nifty_marker_data.namespace, authority); + assert_eq!(nifty_marker_data.mint, mint); + assert_eq!(nifty_marker_data.asset, asset); + assert_eq!(nifty_marker_data.amount, 10); + assert_eq!(nifty_marker_data.state, MarkerState::FungibleEscrowed); + + context.warp_to_slot(200).unwrap(); + + // **Perform the swap** + + let external_ata = + get_associated_token_address_with_program_id(&user, &mint, &spl_token::ID); + + let create_external_ata_ix = + create_associated_token_account(&user, &user, &mint, &spl_token::ID); + + let swap_ix = Instruction { + program_id: libreplex_monoswap::ID, + accounts: NiftySwapCtx { + nifty_marker, + asset, + mint, + escrow_owner, + escrow_token_account: escrow_ata, + external_token_account: external_ata, + payer: user, + token_program: spl_token::ID, + associated_token_program: spl_associated_token_account::ID, + system_program: system_program::ID, + nifty_program: nifty_asset::ID, + } + .to_account_metas(None), + data: libreplex_monoswap::instruction::NiftySwap { + direction: SwapDirection::AssetIn, + } + .data(), + }; + + let blockhash = context.get_new_latest_blockhash().await.unwrap(); + + let tx = Transaction::new_signed_with_payer( + &[create_external_ata_ix, swap_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &user_signer], + blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // **Check the results** + + // The asset should be owned by escrow_owner. + let asset_account = context + .banks_client + .get_account(asset) + .await + .expect("get_account") + .expect("asset_account not found"); + + let asset_data = Asset::deserialize(&mut asset_account.data.as_slice()).unwrap(); + assert_eq!(asset_data.owner, escrow_owner); + + // The fungibles should be transfered to the user's external ATA. + let external_ata_account = context + .banks_client + .get_account(external_ata) + .await + .expect("get_account") + .expect("ata_account not found"); + + let external_ata_data = TokenAccount::unpack(&authority_token_account.data).unwrap(); + assert_eq!(external_ata_data.amount, 10); + assert_eq!(external_ata_data.owner, authority); + assert_eq!(external_ata_data.mint, mint); + + // The escrow ata should be empty. + let escrow_ata_account = context + .banks_client + .get_account(escrow_ata) + .await + .expect("get_account") + .expect("escrow_ata_account not found"); + + let escrow_ata_data = TokenAccount::unpack(&escrow_ata_account.data).unwrap(); + assert_eq!(escrow_ata_data.amount, 0); + + println!("escrow_ata_data: {:?}", escrow_ata); + println!("external_ata_data: {:?}", external_ata); + println!("asset_data: {:?}", asset); + println!("nifty marker: {:?}", nifty_marker); + println!("user: {:?}", user); + + // **Swap back** + let swap_ix = Instruction { + program_id: libreplex_monoswap::ID, + accounts: NiftySwapCtx { + nifty_marker, + asset, + mint, + escrow_owner, + escrow_token_account: escrow_ata, + external_token_account: external_ata, + payer: user, + token_program: spl_token::ID, + associated_token_program: spl_associated_token_account::ID, + system_program: system_program::ID, + nifty_program: nifty_asset::ID, + } + .to_account_metas(None), + data: libreplex_monoswap::instruction::NiftySwap { + direction: SwapDirection::AssetOut, + } + .data(), + }; + + let blockhash = context.get_new_latest_blockhash().await.unwrap(); + + let tx = Transaction::new_signed_with_payer( + &[swap_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &user_signer], + blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // **Check the results** + + // The asset should be owned by user. + let asset_account = context + .banks_client + .get_account(asset) + .await + .expect("get_account") + .expect("asset_account not found"); + + let asset_data = Asset::deserialize(&mut asset_account.data.as_slice()).unwrap(); + assert_eq!(asset_data.owner, user); + + // The fungibles should be transfered back to the escrow ATA. + let escrow_ata_account = context + .banks_client + .get_account(escrow_ata) + .await + .expect("get_account") + .expect("ata_account not found"); + + let escrow_ata_data = TokenAccount::unpack(&authority_token_account.data).unwrap(); + assert_eq!(escrow_ata_data.amount, 10); + + // The user's external ata should be empty. + let external_ata_account = context + .banks_client + .get_account(external_ata) + .await + .expect("get_account") + .expect("escrow_ata_account not found"); + + let external_ata_data = TokenAccount::unpack(&external_ata_account.data).unwrap(); + assert_eq!(external_ata_data.amount, 0); + } +}