diff --git a/Anchor.toml b/Anchor.toml index ecd5baf..ae507ce 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -30,6 +30,10 @@ address = "ocp4vWUzA2z2XMYJ3QhM9vWdyoyoQwAFJhRdVTbvo9E" # ocp: open_creator_prot [[test.validator.clone]] address = "6Huqrb4xxmmNA4NufYdgpmspoLmjXFd3qEfteCddLgSz" # ocp: policy (allow all) +[[test.genesis]] +address = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb" +program = "./tests/deps/spl_token_2022.so" + [programs.localnet] mmm = "mmm3XBJg5gk8XJxEKBvdgptZz6SgK4tXvn36sodowMc" diff --git a/Cargo.lock b/Cargo.lock index 87928b3..0b283a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,6 +1313,8 @@ dependencies = [ "spl-associated-token-account", "spl-token", "spl-token-2022 1.0.0", + "spl-token-group-interface", + "spl-token-metadata-interface", ] [[package]] diff --git a/package.json b/package.json index 91ace37..1a3dc79 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "@metaplex-foundation/umi-bundle-tests": "^0.8.2", "@metaplex-foundation/umi-web3js-adapters": "^0.8.2", "@project-serum/anchor": "^0.26.0", - "@solana/spl-token": "^0.3.5", + "@solana/spl-token": "^0.4.1", + "@solana/spl-token-group": "^0.0.1", "@solana/web3.js": "^1.65.0", "borsh": "^0.7.0", "old-mpl-token-metadata": "npm:@metaplex-foundation/mpl-token-metadata@2.12.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1178e8..5996a39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,11 @@ dependencies: specifier: ^0.26.0 version: 0.26.0 '@solana/spl-token': - specifier: ^0.3.5 - version: 0.3.11(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) + specifier: ^0.4.1 + version: 0.4.1(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/spl-token-group': + specifier: ^0.0.1 + version: 0.0.1(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) '@solana/web3.js': specifier: ^1.65.0 version: 1.89.1 @@ -802,7 +805,7 @@ packages: /@metaplex-foundation/mpl-candy-machine@5.1.0(fastestsmallesttextencoderdecoder@1.0.22): resolution: {integrity: sha512-pjHpUpWVOCDxK3l6dXxfmJKNQmbjBqnm5ElOl1mJAygnzO8NIPQvrP89y6xSNyo8qZsJyt4ZMYUyD0TdbtKZXQ==} dependencies: - '@metaplex-foundation/beet': 0.7.1 + '@metaplex-foundation/beet': 0.7.2 '@metaplex-foundation/beet-solana': 0.4.1 '@metaplex-foundation/cusper': 0.0.2 '@solana/spl-token': 0.3.11(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) @@ -1290,17 +1293,34 @@ packages: /@solana/codecs-core@2.0.0-experimental.8618508: resolution: {integrity: sha512-JCz7mKjVKtfZxkuDtwMAUgA7YvJcA2BwpZaA1NOLcted4OMC4Prwa3DUe3f3181ixPYaRyptbF0Ikq2MbDkYEA==} + /@solana/codecs-core@2.0.0-experimental.9741939: + resolution: {integrity: sha512-7E51aEwLW+1ta6VWbEq0CbvwSUOcIwmaQCLpkOKsF6cwSNqE5GDv/jw9zKuiYOynlBFQO1Ws59a/hlb2wwwWKg==} + dev: false + /@solana/codecs-data-structures@2.0.0-experimental.8618508: resolution: {integrity: sha512-sLpjL9sqzaDdkloBPV61Rht1tgaKq98BCtIKRuyscIrmVPu3wu0Bavk2n/QekmUzaTsj7K1pVSniM0YqCdnEBw==} dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 '@solana/codecs-numbers': 2.0.0-experimental.8618508 + /@solana/codecs-data-structures@2.0.0-experimental.9741939: + resolution: {integrity: sha512-Ghjx0pFEA22T/cpqg3zXHYlnxqKytjTr1QjgfhWvBrlVFkqr3ZQL4av9/V6sHKvpoi+JNkEwM1wE+tlpt/54CA==} + dependencies: + '@solana/codecs-core': 2.0.0-experimental.9741939 + '@solana/codecs-numbers': 2.0.0-experimental.9741939 + dev: false + /@solana/codecs-numbers@2.0.0-experimental.8618508: resolution: {integrity: sha512-EXQKfzFr3CkKKNzKSZPOOOzchXsFe90TVONWsSnVkonO9z+nGKALE0/L9uBmIFGgdzhhU9QQVFvxBMclIDJo2Q==} dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 + /@solana/codecs-numbers@2.0.0-experimental.9741939: + resolution: {integrity: sha512-VXBvw8LZdGUJGuC33EFzvaUxCvgNtr9y9Td8zjK9wJDqKSzR0+G53CJv5K4AF25LUu8du8XHBK3VEz3YHl44nQ==} + dependencies: + '@solana/codecs-core': 2.0.0-experimental.9741939 + dev: false + /@solana/codecs-strings@2.0.0-experimental.8618508(fastestsmallesttextencoderdecoder@1.0.22): resolution: {integrity: sha512-b2yhinr1+oe+JDmnnsV0641KQqqDG8AQ16Z/x7GVWO+AWHMpRlHWVXOq8U1yhPMA4VXxl7i+D+C6ql0VGFp0GA==} peerDependencies: @@ -1310,19 +1330,36 @@ packages: '@solana/codecs-numbers': 2.0.0-experimental.8618508 fastestsmallesttextencoderdecoder: 1.0.22 + /@solana/codecs-strings@2.0.0-experimental.9741939(fastestsmallesttextencoderdecoder@1.0.22): + resolution: {integrity: sha512-J1DCTJsAMhFcIIvys/yNWM2Vr4HEWi+LzFB9xxdH+FHTynApjTMd4vI7frgnckuWH7ynp8EZFPnBTje1BphQrw==} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + dependencies: + '@solana/codecs-core': 2.0.0-experimental.9741939 + '@solana/codecs-numbers': 2.0.0-experimental.9741939 + fastestsmallesttextencoderdecoder: 1.0.22 + dev: false + /@solana/options@2.0.0-experimental.8618508: resolution: {integrity: sha512-fy/nIRAMC3QHvnKi63KEd86Xr/zFBVxNW4nEpVEU2OT0gCEKwHY4Z55YHf7XujhyuM3PNpiBKg/YYw5QlRU4vg==} dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 '@solana/codecs-numbers': 2.0.0-experimental.8618508 + /@solana/options@2.0.0-experimental.9741939: + resolution: {integrity: sha512-Fj76WDb+SWEEN3i0gEVQHGPHR2v54ECHILluQ5r18deLHjtZpD48a0dZepf0YcyAG7OHofkRdPxInZ3YYDuZeQ==} + dependencies: + '@solana/codecs-core': 2.0.0-experimental.9741939 + '@solana/codecs-numbers': 2.0.0-experimental.9741939 + dev: false + /@solana/spl-account-compression@0.1.10(@solana/web3.js@1.89.1): resolution: {integrity: sha512-IQAOJrVOUo6LCgeWW9lHuXo6JDbi4g3/RkQtvY0SyalvSWk9BIkHHe4IkAzaQw8q/BxEVBIjz8e9bNYWIAESNw==} engines: {node: '>=16'} peerDependencies: '@solana/web3.js': ^1.50.1 dependencies: - '@metaplex-foundation/beet': 0.7.1 + '@metaplex-foundation/beet': 0.7.2 '@metaplex-foundation/beet-solana': 0.4.1 '@solana/web3.js': 1.89.1 bn.js: 5.2.1 @@ -1336,6 +1373,22 @@ packages: - utf-8-validate dev: false + /@solana/spl-token-group@0.0.1(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22): + resolution: {integrity: sha512-dnBpjFhAskL+LzLfZhgeZVpDoO5ialtwear3vcHy3f+WNzJn/HITCmf341kekMHcLEeVaJts+4i/daVsfjqq7A==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.87.6 + dependencies: + '@solana/codecs-data-structures': 2.0.0-experimental.9741939 + '@solana/codecs-numbers': 2.0.0-experimental.9741939 + '@solana/codecs-strings': 2.0.0-experimental.9741939(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/options': 2.0.0-experimental.9741939 + '@solana/spl-type-length-value': 0.1.0 + '@solana/web3.js': 1.89.1 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + dev: false + /@solana/spl-token-metadata@0.1.2(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22): resolution: {integrity: sha512-hJYnAJNkDrtkE2Q41YZhCpeOGU/0JgRFXbtrtOuGGeKc3pkEUHB9DDoxZAxx+XRno13GozUleyBi0qypz4c3bw==} engines: {node: '>=16'} @@ -1398,6 +1451,24 @@ packages: - fastestsmallesttextencoderdecoder - utf-8-validate + /@solana/spl-token@0.4.1(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22): + resolution: {integrity: sha512-DEe15GI0l+XLHwtau/3GUwGQJ9YY/VWNE0k/QuXaaGKo4adMZLEAIQUktRc/S2sRqPjvUdR5anZGxQ9p5khWZw==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.90.0 + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/buffer-layout-utils': 0.2.0 + '@solana/spl-token-metadata': 0.1.2(@solana/web3.js@1.89.1)(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/web3.js': 1.89.1 + buffer: 6.0.3 + transitivePeerDependencies: + - bufferutil + - encoding + - fastestsmallesttextencoderdecoder + - utf-8-validate + dev: false + /@solana/spl-type-length-value@0.1.0: resolution: {integrity: sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==} engines: {node: '>=16'} diff --git a/programs/mmm/Cargo.toml b/programs/mmm/Cargo.toml index add245b..ac3ee6d 100644 --- a/programs/mmm/Cargo.toml +++ b/programs/mmm/Cargo.toml @@ -22,6 +22,8 @@ community-managed-token = { version = "0.3.1", features = ["no-entrypoint"] } mpl-token-metadata = { version = "4.0.0" } open_creator_protocol = { version = "0.4.2", features = ["cpi"] } solana-program = "~1.17" +spl-token-group-interface = "0.1.0" +spl-token-metadata-interface = "0.2.0" spl-token = { version = "4.0.0", features = ["no-entrypoint"] } spl-associated-token-account = { version = "2.2.0", features = [ "no-entrypoint", diff --git a/programs/mmm/src/errors.rs b/programs/mmm/src/errors.rs index 4061e99..56c1ac0 100644 --- a/programs/mmm/src/errors.rs +++ b/programs/mmm/src/errors.rs @@ -62,4 +62,8 @@ pub enum MMMErrorCode { UnexpectedMetadataUri, // 0x178c #[msg("Invalid remaining accounts")] InvalidRemainingAccounts, // 0x178d + #[msg("Invalid token metadata extensions")] + InvalidTokenMetadataExtension, // 0x178e + #[msg("Invalid token member extensions")] + InvalidTokenMemberExtension, // 0x178f } diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs new file mode 100644 index 0000000..b414760 --- /dev/null +++ b/programs/mmm/src/instructions/ext_vanilla/ext_deposit_sell.rs @@ -0,0 +1,134 @@ +use anchor_lang::{prelude::*, AnchorDeserialize}; +use anchor_spl::{ + associated_token::AssociatedToken, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; +use solana_program::program::invoke; +use spl_token_2022::onchain::invoke_transfer_checked; + +use crate::{ + constants::*, + errors::MMMErrorCode, + state::{Pool, SellState}, + util::{check_allowlists_for_mint_ext, log_pool}, + DepositSellArgs, +}; + +#[derive(Accounts)] +#[instruction(args: DepositSellArgs)] +pub struct ExtDepositeSell<'info> { + #[account(mut)] + pub owner: Signer<'info>, + pub cosigner: Signer<'info>, + #[account( + mut, + seeds = [POOL_PREFIX.as_bytes(), owner.key().as_ref(), pool.uuid.as_ref()], + has_one = owner @ MMMErrorCode::InvalidOwner, + has_one = cosigner @ MMMErrorCode::InvalidCosigner, + bump + )] + pub pool: Box>, + #[account( + mint::token_program = token_program, + constraint = asset_mint.supply == 1 && asset_mint.decimals == 0 @ MMMErrorCode::InvalidTokenMint, + )] + pub asset_mint: Box>, + #[account( + mut, + associated_token::mint = asset_mint, + associated_token::authority = owner, + associated_token::token_program = token_program, + )] + pub asset_token_account: Box>, + #[account( + init_if_needed, + payer = owner, + associated_token::mint = asset_mint, + associated_token::authority = pool, + associated_token::token_program = token_program, + )] + pub sellside_escrow_token_account: Box>, + #[account( + init_if_needed, + payer = owner, + seeds = [ + SELL_STATE_PREFIX.as_bytes(), + pool.key().as_ref(), + asset_mint.key().as_ref(), + ], + space = SellState::LEN, + bump + )] + pub sell_state: Account<'info, SellState>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, +} + +pub fn handler<'info>( + ctx: Context<'_, '_, '_, 'info, ExtDepositeSell<'info>>, + args: DepositSellArgs, +) -> Result<()> { + let owner = &ctx.accounts.owner; + let asset_token_account = &ctx.accounts.asset_token_account; + let asset_mint = &ctx.accounts.asset_mint; + let sellside_escrow_token_account = &ctx.accounts.sellside_escrow_token_account; + let token_program = &ctx.accounts.token_program; + let pool = &mut ctx.accounts.pool; + let sell_state = &mut ctx.accounts.sell_state; + + if pool.using_shared_escrow() { + return Err(MMMErrorCode::InvalidAccountState.into()); + } + + check_allowlists_for_mint_ext( + &pool.allowlists, + &asset_mint.to_account_info(), + args.allowlist_aux, + )?; + + invoke_transfer_checked( + token_program.key, + asset_token_account.to_account_info(), + asset_mint.to_account_info(), + sellside_escrow_token_account.to_account_info(), + owner.to_account_info(), + ctx.remaining_accounts, + args.asset_amount, + 0, + &[], + )?; + + if asset_token_account.amount == args.asset_amount { + invoke( + &spl_token_2022::instruction::close_account( + token_program.key, + &asset_token_account.key(), + &owner.key(), + &owner.key(), + &[], + )?, + &[ + asset_token_account.to_account_info(), + owner.to_account_info(), + ], + )?; + } + + pool.sellside_asset_amount = pool + .sellside_asset_amount + .checked_add(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + + sell_state.pool = pool.key(); + sell_state.pool_owner = owner.key(); + sell_state.asset_mint = asset_mint.key(); + sell_state.cosigner_annotation = pool.cosigner_annotation; + sell_state.asset_amount = sell_state + .asset_amount + .checked_add(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + log_pool("post_ext_deposit_sell", pool)?; + + Ok(()) +} diff --git a/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs b/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs new file mode 100644 index 0000000..e9f12d4 --- /dev/null +++ b/programs/mmm/src/instructions/ext_vanilla/ext_withdraw_sell.rs @@ -0,0 +1,139 @@ +use anchor_lang::{prelude::*, AnchorDeserialize}; +use anchor_spl::{ + associated_token::AssociatedToken, + token_2022::{close_account, CloseAccount}, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; +use spl_token_2022::onchain::invoke_transfer_checked; + +use crate::{ + constants::*, + errors::MMMErrorCode, + state::{Pool, SellState}, + util::{log_pool, try_close_pool, try_close_sell_state}, + WithdrawSellArgs, +}; + +#[derive(Accounts)] +#[instruction(args:WithdrawSellArgs)] +pub struct ExtWithdrawSell<'info> { + #[account(mut)] + pub owner: Signer<'info>, + pub cosigner: Signer<'info>, + #[account( + mut, + seeds = [POOL_PREFIX.as_bytes(), owner.key().as_ref(), pool.uuid.as_ref()], + has_one = owner @ MMMErrorCode::InvalidOwner, + has_one = cosigner @ MMMErrorCode::InvalidCosigner, + bump + )] + pub pool: Box>, + #[account( + mint::token_program = token_program, + constraint = asset_mint.supply == 1 && asset_mint.decimals == 0 @ MMMErrorCode::InvalidTokenMint, + )] + pub asset_mint: Box>, + #[account( + init_if_needed, + payer = owner, + associated_token::mint = asset_mint, + associated_token::authority = owner, + associated_token::token_program = token_program + )] + pub asset_token_account: Box>, + #[account( + mut, + associated_token::mint = asset_mint, + associated_token::authority = pool, + associated_token::token_program = token_program + )] + pub sellside_escrow_token_account: Box>, + /// CHECK: it's a pda, and the private key is owned by the seeds + #[account( + mut, + seeds = [BUYSIDE_SOL_ESCROW_ACCOUNT_PREFIX.as_bytes(), pool.key().as_ref()], + bump, + )] + pub buyside_sol_escrow_account: UncheckedAccount<'info>, + /// CHECK: will be used for allowlist checks + pub allowlist_aux_account: UncheckedAccount<'info>, + #[account( + mut, + seeds = [ + SELL_STATE_PREFIX.as_bytes(), + pool.key().as_ref(), + asset_mint.key().as_ref(), + ], + bump + )] + pub sell_state: Account<'info, SellState>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, +} + +pub fn handler(ctx: Context, args: WithdrawSellArgs) -> Result<()> { + let owner = &ctx.accounts.owner; + let asset_token_account = &ctx.accounts.asset_token_account; + let sellside_escrow_token_account = &ctx.accounts.sellside_escrow_token_account; + let token_program = &ctx.accounts.token_program; + let buyside_sol_escrow_account = &ctx.accounts.buyside_sol_escrow_account; + let pool = &mut ctx.accounts.pool; + let sell_state = &mut ctx.accounts.sell_state; + let asset_mint = &ctx.accounts.asset_mint; + + let pool_uuid_key = pool.uuid.key(); + let owner_key = owner.key(); + let pool_seeds: &[&[&[u8]]] = &[&[ + POOL_PREFIX.as_bytes(), + owner_key.as_ref(), + pool_uuid_key.as_ref(), + &[ctx.bumps.pool], + ]]; + + // Note that check_allowlists_for_mint_ext is optional for withdraw_sell + // because sometimes the nft might be moved out of the collection + // and we'd still like to enable the withdraw of those items for the pool owner. + + invoke_transfer_checked( + token_program.key, + sellside_escrow_token_account.to_account_info(), + asset_mint.to_account_info(), + asset_token_account.to_account_info(), + pool.to_account_info(), + &[], // additional_accounts + args.asset_amount, + 0, // decimals + pool_seeds, + )?; + + // we can close the sellside_escrow_token_account if no amount left + if sellside_escrow_token_account.amount == args.asset_amount { + close_account(CpiContext::new_with_signer( + token_program.to_account_info(), + CloseAccount { + account: sellside_escrow_token_account.to_account_info(), + destination: owner.to_account_info(), + authority: pool.to_account_info(), + }, + // seeds should be the PDA of 'pool' + pool_seeds, + ))?; + } + + pool.sellside_asset_amount = pool + .sellside_asset_amount + .checked_sub(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + sell_state.asset_amount = sell_state + .asset_amount + .checked_sub(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + try_close_sell_state(sell_state, owner.to_account_info())?; + + pool.buyside_payment_amount = buyside_sol_escrow_account.lamports(); + log_pool("post_ext_withdraw_sell", pool)?; + try_close_pool(pool, owner.to_account_info())?; + + Ok(()) +} diff --git a/programs/mmm/src/instructions/ext_vanilla/mod.rs b/programs/mmm/src/instructions/ext_vanilla/mod.rs new file mode 100644 index 0000000..132eb09 --- /dev/null +++ b/programs/mmm/src/instructions/ext_vanilla/mod.rs @@ -0,0 +1,9 @@ +pub mod ext_deposit_sell; +pub mod ext_withdraw_sell; +pub mod sol_ext_fulfill_buy; +pub mod sol_ext_fulfill_sell; + +pub use ext_deposit_sell::*; +pub use ext_withdraw_sell::*; +pub use sol_ext_fulfill_buy::*; +pub use sol_ext_fulfill_sell::*; diff --git a/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs new file mode 100644 index 0000000..12a6b03 --- /dev/null +++ b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_buy.rs @@ -0,0 +1,359 @@ +use anchor_lang::{prelude::*, AnchorDeserialize}; +use anchor_spl::{ + associated_token::AssociatedToken, + token_2022::close_account, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; +use solana_program::{program::invoke_signed, system_instruction}; +use spl_token_2022::onchain::invoke_transfer_checked; +use std::convert::TryFrom; + +use crate::{ + ata::init_if_needed_ata, + constants::*, + errors::MMMErrorCode, + index_ra, + instructions::{check_remaining_accounts_for_m2, log_pool, try_close_pool, withdraw_m2}, + state::{Pool, SellState}, + util::{ + assert_valid_fees_bp, check_allowlists_for_mint_ext, get_buyside_seller_receives, + get_lp_fee_bp, get_sol_fee, get_sol_lp_fee, get_sol_total_price_and_next_price, + try_close_escrow, try_close_sell_state, + }, + SolFulfillBuyArgs, +}; + +// ExtSolFulfillBuy means a seller wants to sell NFT into the pool +// where the pool has some buyside payment liquidity. Therefore, +// the seller expects a min_payment_amount that goes back to the +// seller's wallet for the asset_amount that the seller wants to sell. +// This is mainly used for Token22 extension +#[derive(Accounts)] +#[instruction(args:SolFulfillBuyArgs)] +pub struct ExtSolFulfillBuy<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: we will check the owner field that matches the pool owner + #[account(mut)] + pub owner: UncheckedAccount<'info>, + pub cosigner: Signer<'info>, + #[account(mut)] + /// CHECK: we will check that the referral matches the pool's referral + pub referral: UncheckedAccount<'info>, + #[account( + mut, + seeds = [POOL_PREFIX.as_bytes(), owner.key().as_ref(), pool.uuid.as_ref()], + has_one = owner @ MMMErrorCode::InvalidOwner, + has_one = referral @ MMMErrorCode::InvalidReferral, + has_one = cosigner @ MMMErrorCode::InvalidCosigner, + constraint = pool.payment_mint.eq(&Pubkey::default()) @ MMMErrorCode::InvalidPaymentMint, + constraint = pool.expiry == 0 || pool.expiry > Clock::get().unwrap().unix_timestamp @ MMMErrorCode::Expired, + bump + )] + pub pool: Box>, + /// CHECK: it's a pda, and the private key is owned by the seeds + #[account( + mut, + seeds = [BUYSIDE_SOL_ESCROW_ACCOUNT_PREFIX.as_bytes(), pool.key().as_ref()], + bump, + )] + pub buyside_sol_escrow_account: UncheckedAccount<'info>, + #[account( + mint::token_program = token_program, + constraint = asset_mint.supply == 1 && asset_mint.decimals == 0 @ MMMErrorCode::InvalidTokenMint, + )] + pub asset_mint: Box>, + #[account( + mut, + token::mint = asset_mint, + token::authority = payer, + token::token_program = token_program, + )] + pub payer_asset_account: Box>, + /// CHECK: check in init_if_needed_ata + #[account(mut)] + pub sellside_escrow_token_account: UncheckedAccount<'info>, + /// CHECK: check in init_if_needed_ata + #[account(mut)] + pub owner_token_account: UncheckedAccount<'info>, + /// CHECK: will be used for allowlist checks + pub allowlist_aux_account: UncheckedAccount<'info>, + #[account( + init_if_needed, + payer = payer, + seeds = [ + SELL_STATE_PREFIX.as_bytes(), + pool.key().as_ref(), + asset_mint.key().as_ref(), + ], + space = SellState::LEN, + bump + )] + pub sell_state: Account<'info, SellState>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub rent: Sysvar<'info, Rent>, + // Remaining accounts + // Branch: using shared escrow accounts + // 0: m2_program + // 1: shared_escrow_account + // 2+: transfer hook accounts + // Branch: not using shared escrow accounts + // 0+: transfer hook accounts +} + +pub fn handler<'info>( + ctx: Context<'_, '_, '_, 'info, ExtSolFulfillBuy<'info>>, + args: SolFulfillBuyArgs, +) -> Result<()> { + let token_program = &ctx.accounts.token_program; + let system_program = &ctx.accounts.system_program; + let associated_token_program = &ctx.accounts.associated_token_program; + let rent = &ctx.accounts.rent; + let pool = &mut ctx.accounts.pool; + let sell_state = &mut ctx.accounts.sell_state; + let owner = &ctx.accounts.owner; + let referral = &ctx.accounts.referral; + let payer = &ctx.accounts.payer; + let payer_asset_account = &ctx.accounts.payer_asset_account; + let asset_mint = &ctx.accounts.asset_mint; + let buyside_sol_escrow_account = &ctx.accounts.buyside_sol_escrow_account; + let pool_key = pool.key(); + let buyside_sol_escrow_account_seeds: &[&[&[u8]]] = &[&[ + BUYSIDE_SOL_ESCROW_ACCOUNT_PREFIX.as_bytes(), + pool_key.as_ref(), + &[ctx.bumps.buyside_sol_escrow_account], + ]]; + let remaining_accounts = ctx.remaining_accounts; + + check_allowlists_for_mint_ext( + &pool.allowlists, + &asset_mint.to_account_info(), + args.allowlist_aux, + )?; + + let (total_price, next_price) = + get_sol_total_price_and_next_price(pool, args.asset_amount, true)?; + // TODO: update lp_fee_bp when shared escrow for both side is enabled + let seller_receives = { + let lp_fee_bp = get_lp_fee_bp(pool, buyside_sol_escrow_account.lamports()); + get_buyside_seller_receives( + total_price, + lp_fee_bp, + 0, // metadata_royalty_bp + 0, // buyside_creator_royalty_bp, + ) + }?; + + assert_valid_fees_bp(args.maker_fee_bp, args.taker_fee_bp)?; + let maker_fee = get_sol_fee(seller_receives, args.maker_fee_bp)?; + let taker_fee = get_sol_fee(seller_receives, args.taker_fee_bp)?; + let referral_fee = u64::try_from( + maker_fee + .checked_add(taker_fee) + .ok_or(MMMErrorCode::NumericOverflow)?, + ) + .map_err(|_| MMMErrorCode::NumericOverflow)?; + + // TODO: update lp_fee when shared escrow for both side is enabled + let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), seller_receives)?; + + // withdraw sol from M2 first if shared escrow is enabled + let remaining_account_without_m2 = if pool.using_shared_escrow() { + check_remaining_accounts_for_m2(remaining_accounts, &pool.owner.key())?; + + let amount: u64 = (total_price as i64 + maker_fee) as u64; + withdraw_m2( + pool, + ctx.bumps.pool, + buyside_sol_escrow_account, + index_ra!(remaining_accounts, 1), + system_program, + index_ra!(remaining_accounts, 0), + pool.owner, + amount, + )?; + pool.shared_escrow_count = pool + .shared_escrow_count + .checked_sub(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + &remaining_accounts[2..] + } else { + remaining_accounts + }; + + if pool.reinvest_fulfill_buy { + if pool.using_shared_escrow() { + return Err(MMMErrorCode::InvalidAccountState.into()); + } + let sellside_escrow_token_account = + ctx.accounts.sellside_escrow_token_account.to_account_info(); + init_if_needed_ata( + sellside_escrow_token_account.to_account_info(), + payer.to_account_info(), + pool.to_account_info(), + asset_mint.to_account_info(), + associated_token_program.to_account_info(), + token_program.to_account_info(), + system_program.to_account_info(), + rent.to_account_info(), + )?; + invoke_transfer_checked( + token_program.key, + payer_asset_account.to_account_info(), + asset_mint.to_account_info(), + sellside_escrow_token_account.to_account_info(), + payer.to_account_info(), + remaining_account_without_m2, + args.asset_amount, + 0, // decimals + &[], // seeds + )?; + + pool.sellside_asset_amount = pool + .sellside_asset_amount + .checked_add(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + sell_state.pool = pool.key(); + sell_state.pool_owner = owner.key(); + sell_state.asset_mint = asset_mint.key(); + sell_state.cosigner_annotation = pool.cosigner_annotation; + sell_state.asset_amount = sell_state + .asset_amount + .checked_add(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + } else { + let owner_token_account = ctx.accounts.owner_token_account.to_account_info(); + init_if_needed_ata( + owner_token_account.to_account_info(), + payer.to_account_info(), + owner.to_account_info(), + asset_mint.to_account_info(), + associated_token_program.to_account_info(), + token_program.to_account_info(), + system_program.to_account_info(), + rent.to_account_info(), + )?; + + invoke_transfer_checked( + token_program.key, + payer_asset_account.to_account_info(), + asset_mint.to_account_info(), + owner_token_account.to_account_info(), + payer.to_account_info(), + remaining_account_without_m2, + args.asset_amount, + 0, // decimals + &[], // seeds + )?; + } + + // we can close the payer_asset_account if no amount left + if payer_asset_account.amount == args.asset_amount { + close_account(CpiContext::new( + token_program.to_account_info(), + anchor_spl::token_2022::CloseAccount { + account: payer_asset_account.to_account_info(), + destination: payer.to_account_info(), + authority: payer.to_account_info(), + }, + ))?; + } + + // prevent frontrun by pool config changes + let payment_amount = total_price + .checked_sub(lp_fee) + .ok_or(MMMErrorCode::NumericOverflow)? + .checked_sub(taker_fee as u64) + .ok_or(MMMErrorCode::NumericOverflow)?; + + if payment_amount < args.min_payment_amount { + return Err(MMMErrorCode::InvalidRequestedPrice.into()); + } + + invoke_signed( + &system_instruction::transfer(buyside_sol_escrow_account.key, payer.key, payment_amount), + &[ + buyside_sol_escrow_account.to_account_info(), + payer.to_account_info(), + ], + buyside_sol_escrow_account_seeds, + )?; + + if lp_fee > 0 { + invoke_signed( + &system_instruction::transfer(buyside_sol_escrow_account.key, owner.key, lp_fee), + &[ + buyside_sol_escrow_account.to_account_info(), + owner.to_account_info(), + ], + buyside_sol_escrow_account_seeds, + )?; + } + if referral_fee > 0 { + invoke_signed( + &system_instruction::transfer( + buyside_sol_escrow_account.key, + referral.key, + referral_fee, + ), + &[ + buyside_sol_escrow_account.to_account_info(), + referral.to_account_info(), + ], + buyside_sol_escrow_account_seeds, + )?; + } + + pool.lp_fee_earned = pool + .lp_fee_earned + .checked_add(lp_fee) + .ok_or(MMMErrorCode::NumericOverflow)?; + pool.spot_price = next_price; + + try_close_escrow( + &buyside_sol_escrow_account.to_account_info(), + pool, + system_program, + buyside_sol_escrow_account_seeds, + )?; + try_close_sell_state(sell_state, payer.to_account_info())?; + + // return the remaining per pool escrow balance to the shared escrow account + if pool.using_shared_escrow() { + let min_rent = Rent::get()?.minimum_balance(0); + let shared_escrow_account = index_ra!(remaining_accounts, 1).to_account_info(); + if shared_escrow_account.lamports() + buyside_sol_escrow_account.lamports() > min_rent + && buyside_sol_escrow_account.lamports() > 0 + { + invoke_signed( + &system_instruction::transfer( + buyside_sol_escrow_account.key, + shared_escrow_account.key, + buyside_sol_escrow_account.lamports(), + ), + &[ + buyside_sol_escrow_account.to_account_info(), + shared_escrow_account, + ], + buyside_sol_escrow_account_seeds, + )?; + } else { + try_close_escrow( + buyside_sol_escrow_account, + pool, + system_program, + buyside_sol_escrow_account_seeds, + )?; + } + } + pool.buyside_payment_amount = buyside_sol_escrow_account.lamports(); + + log_pool("post_ext_sol_fulfill_buy", pool)?; + try_close_pool(pool, owner.to_account_info())?; + + msg!("{{\"lp_fee\":{},\"total_price\":{}}}", lp_fee, total_price,); + + Ok(()) +} diff --git a/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_sell.rs b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_sell.rs new file mode 100644 index 0000000..f9b1e23 --- /dev/null +++ b/programs/mmm/src/instructions/ext_vanilla/sol_ext_fulfill_sell.rs @@ -0,0 +1,242 @@ +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token_2022::close_account, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; +use solana_program::{program::invoke, system_instruction}; +use spl_token_2022::onchain::invoke_transfer_checked; +use std::convert::TryFrom; + +use crate::{ + constants::*, + errors::MMMErrorCode, + instructions::{ + get_sell_fulfill_pool_price_info, log_pool, try_close_pool, try_close_sell_state, + PoolPriceInfo, + }, + state::{Pool, SellState}, + util::check_allowlists_for_mint_ext, + SolFulfillSellArgs, +}; + +// ExtSolFulfillSell means a buyer wants to buy NFTs from the pool +// where the pool has some sellside asset liquidity. Therefore, +// the buyer expects to pay a max_payment_amount for the asset_amount +// that the buyer wants to buy. +// This is mainly used for Token22 extension +#[derive(Accounts)] +#[instruction(args:SolFulfillSellArgs)] +pub struct ExtSolFulfillSell<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: we will check the owner field that matches the pool owner + #[account(mut)] + pub owner: UncheckedAccount<'info>, + pub cosigner: Signer<'info>, + /// CHECK: we will check that the referral matches the pool's referral + #[account(mut)] + pub referral: UncheckedAccount<'info>, + #[account( + mut, + seeds = [POOL_PREFIX.as_bytes(), owner.key().as_ref(), pool.uuid.as_ref()], + has_one = owner @ MMMErrorCode::InvalidOwner, + has_one = referral @ MMMErrorCode::InvalidReferral, + has_one = cosigner @ MMMErrorCode::InvalidCosigner, + constraint = pool.payment_mint.eq(&Pubkey::default()) @ MMMErrorCode::InvalidPaymentMint, + constraint = pool.expiry == 0 || pool.expiry > Clock::get().unwrap().unix_timestamp @ MMMErrorCode::Expired, + constraint = args.buyside_creator_royalty_bp <= 10000 @ MMMErrorCode::InvalidBP, + bump + )] + pub pool: Box>, + /// CHECK: it's a pda, and the private key is owned by the seeds + #[account( + mut, + seeds = [BUYSIDE_SOL_ESCROW_ACCOUNT_PREFIX.as_bytes(), pool.key().as_ref()], + bump, + )] + pub buyside_sol_escrow_account: AccountInfo<'info>, + /// CHECK: check_allowlists_for_mint_ext + #[account( + mint::token_program = token_program, + constraint = asset_mint.supply == 1 && asset_mint.decimals == 0 @ MMMErrorCode::InvalidTokenMint, + )] + pub asset_mint: Box>, + #[account( + mut, + associated_token::mint = asset_mint, + associated_token::authority = pool, + associated_token::token_program = token_program, + )] + pub sellside_escrow_token_account: Box>, + #[account( + init_if_needed, + payer = payer, + associated_token::mint = asset_mint, + associated_token::authority = payer, + associated_token::token_program = token_program, + )] + pub payer_asset_account: Box>, + #[account( + mut, + seeds = [ + SELL_STATE_PREFIX.as_bytes(), + pool.key().as_ref(), + asset_mint.key().as_ref(), + ], + bump + )] + pub sell_state: Account<'info, SellState>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, +} + +pub fn handler<'info>( + ctx: Context<'_, '_, '_, 'info, ExtSolFulfillSell<'info>>, + args: SolFulfillSellArgs, +) -> Result<()> { + let token_program = &ctx.accounts.token_program; + let system_program = &ctx.accounts.system_program; + let owner = &ctx.accounts.owner; + let referral = &ctx.accounts.referral; + let pool = &mut ctx.accounts.pool; + let sell_state = &mut ctx.accounts.sell_state; + + let payer = &ctx.accounts.payer; + let payer_asset_account = &ctx.accounts.payer_asset_account; + let asset_mint = &ctx.accounts.asset_mint; + + let sellside_escrow_token_account = &ctx.accounts.sellside_escrow_token_account; + let buyside_sol_escrow_account = &ctx.accounts.buyside_sol_escrow_account; + let pool_seeds: &[&[&[u8]]] = &[&[ + POOL_PREFIX.as_bytes(), + pool.owner.as_ref(), + pool.uuid.as_ref(), + &[ctx.bumps.pool], + ]]; + + check_allowlists_for_mint_ext( + &pool.allowlists, + &asset_mint.to_account_info(), + args.allowlist_aux, + )?; + + let PoolPriceInfo { + total_price, + next_price, + lp_fee, + maker_fee, + taker_fee, + referral_fee, + transfer_sol_to, + } = get_sell_fulfill_pool_price_info( + pool, + owner, + buyside_sol_escrow_account, + args.asset_amount, + args.maker_fee_bp, + args.taker_fee_bp, + )?; + + // TODO: make sure that the lp fee is paid with the correct amount + invoke( + &system_instruction::transfer( + payer.key, + transfer_sol_to.key, + u64::try_from( + i64::try_from(total_price) + .map_err(|_| MMMErrorCode::NumericOverflow)? + .checked_sub(maker_fee) + .ok_or(MMMErrorCode::NumericOverflow)?, + ) + .map_err(|_| MMMErrorCode::NumericOverflow)?, + ), + &[ + payer.to_account_info(), + transfer_sol_to, + system_program.to_account_info(), + ], + )?; + + invoke_transfer_checked( + token_program.key, + sellside_escrow_token_account.to_account_info(), + asset_mint.to_account_info(), + payer_asset_account.to_account_info(), + pool.to_account_info(), + &[], + args.asset_amount, + 0, + pool_seeds, + )?; + + // we can close the sellside_escrow_token_account if no amount left + if sellside_escrow_token_account.amount == args.asset_amount { + close_account(CpiContext::new_with_signer( + token_program.to_account_info(), + anchor_spl::token_2022::CloseAccount { + account: sellside_escrow_token_account.to_account_info(), + destination: owner.to_account_info(), + authority: pool.to_account_info(), + }, + pool_seeds, + ))?; + } + + if lp_fee > 0 { + invoke( + &system_instruction::transfer(payer.key, owner.key, lp_fee), + &[ + payer.to_account_info(), + owner.to_account_info(), + system_program.to_account_info(), + ], + )?; + } + + if referral_fee > 0 { + invoke( + &system_instruction::transfer(payer.key, referral.key, referral_fee), + &[ + payer.to_account_info(), + referral.to_account_info(), + system_program.to_account_info(), + ], + )?; + } + + pool.spot_price = next_price; + pool.sellside_asset_amount = pool + .sellside_asset_amount + .checked_sub(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + pool.lp_fee_earned = pool + .lp_fee_earned + .checked_add(lp_fee) + .ok_or(MMMErrorCode::NumericOverflow)?; + + // prevent frontrun by pool config changes + let payment_amount = total_price + .checked_add(lp_fee) + .ok_or(MMMErrorCode::NumericOverflow)? + .checked_add(taker_fee as u64) + .ok_or(MMMErrorCode::NumericOverflow)?; + if payment_amount > args.max_payment_amount { + return Err(MMMErrorCode::InvalidRequestedPrice.into()); + } + + sell_state.asset_amount = sell_state + .asset_amount + .checked_sub(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + try_close_sell_state(sell_state, owner.to_account_info())?; + + pool.buyside_payment_amount = buyside_sol_escrow_account.lamports(); + log_pool("post_ext_sol_fulfill_sell", pool)?; + try_close_pool(pool, owner.to_account_info())?; + + msg!("{{\"lp_fee\":{},\"total_price\":{}}}", lp_fee, total_price); + + Ok(()) +} diff --git a/programs/mmm/src/instructions/mod.rs b/programs/mmm/src/instructions/mod.rs index b81d88e..9c1b5a9 100644 --- a/programs/mmm/src/instructions/mod.rs +++ b/programs/mmm/src/instructions/mod.rs @@ -1,11 +1,13 @@ #![allow(missing_docs)] pub mod admin; +pub mod ext_vanilla; pub mod mip1; pub mod ocp; pub mod vanilla; pub use admin::*; +pub use ext_vanilla::*; pub use mip1::*; pub use ocp::*; pub use vanilla::*; diff --git a/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs b/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs index 38a2c20..c9927e4 100644 --- a/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs +++ b/programs/mmm/src/instructions/vanilla/sol_fulfill_buy.rs @@ -155,6 +155,7 @@ pub fn handler<'info>( let (total_price, next_price) = get_sol_total_price_and_next_price(pool, args.asset_amount, true)?; let metadata_royalty_bp = get_metadata_royalty_bp(total_price, &parsed_metadata, None); + // TODO: update lp_fee_bp when shared escrow for both side is enabled let seller_receives = { let lp_fee_bp = get_lp_fee_bp(pool, buyside_sol_escrow_account.lamports()); get_buyside_seller_receives( @@ -164,6 +165,8 @@ pub fn handler<'info>( pool.buyside_creator_royalty_bp, ) }?; + + // TODO: update lp_fee when shared escrow for both side is enabled let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), seller_receives)?; assert_valid_fees_bp(args.maker_fee_bp, args.taker_fee_bp)?; diff --git a/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs b/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs index c1c08e0..c136b4e 100644 --- a/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs +++ b/programs/mmm/src/instructions/vanilla/sol_fulfill_sell.rs @@ -8,10 +8,10 @@ use std::convert::TryFrom; use crate::{ constants::*, errors::MMMErrorCode, + instructions::{get_sell_fulfill_pool_price_info, PoolPriceInfo}, state::{Pool, SellState}, util::{ - assert_valid_fees_bp, check_allowlists_for_mint, get_metadata_royalty_bp, get_sol_fee, - get_sol_lp_fee, get_sol_total_price_and_next_price, log_pool, pay_creator_fees_in_sol, + check_allowlists_for_mint, get_metadata_royalty_bp, log_pool, pay_creator_fees_in_sol, try_close_pool, try_close_sell_state, }, }; @@ -143,25 +143,22 @@ pub fn handler<'info>( args.allowlist_aux, )?; - let (total_price, next_price) = - get_sol_total_price_and_next_price(pool, args.asset_amount, false)?; - let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), total_price)?; - - assert_valid_fees_bp(args.maker_fee_bp, args.taker_fee_bp)?; - let maker_fee = get_sol_fee(total_price, args.maker_fee_bp)?; - let taker_fee = get_sol_fee(total_price, args.taker_fee_bp)?; - let referral_fee = u64::try_from( - maker_fee - .checked_add(taker_fee) - .ok_or(MMMErrorCode::NumericOverflow)?, - ) - .map_err(|_| MMMErrorCode::NumericOverflow)?; - - let transfer_sol_to = if pool.reinvest_fulfill_sell { - buyside_sol_escrow_account.to_account_info() - } else { - owner.to_account_info() - }; + let PoolPriceInfo { + total_price, + next_price, + lp_fee, + maker_fee, + taker_fee, + referral_fee, + transfer_sol_to, + } = get_sell_fulfill_pool_price_info( + pool, + owner, + buyside_sol_escrow_account, + args.asset_amount, + args.maker_fee_bp, + args.taker_fee_bp, + )?; // TODO: make sure that the lp fee is paid with the correct amount anchor_lang::solana_program::program::invoke( diff --git a/programs/mmm/src/lib.rs b/programs/mmm/src/lib.rs index 8a1f76e..7a8f890 100644 --- a/programs/mmm/src/lib.rs +++ b/programs/mmm/src/lib.rs @@ -124,4 +124,29 @@ pub mod mmm { ) -> Result<()> { instructions::set_shared_escrow::handler(ctx, args) } + + pub fn ext_deposit_sell<'info>( + ctx: Context<'_, '_, '_, 'info, ExtDepositeSell<'info>>, + args: DepositSellArgs, + ) -> Result<()> { + instructions::ext_deposit_sell::handler(ctx, args) + } + + pub fn sol_ext_fulfill_sell<'info>( + ctx: Context<'_, '_, '_, 'info, ExtSolFulfillSell<'info>>, + args: SolFulfillSellArgs, + ) -> Result<()> { + instructions::sol_ext_fulfill_sell::handler(ctx, args) + } + + pub fn sol_ext_fulfill_buy<'info>( + ctx: Context<'_, '_, '_, 'info, ExtSolFulfillBuy<'info>>, + args: SolFulfillBuyArgs, + ) -> Result<()> { + instructions::sol_ext_fulfill_buy::handler(ctx, args) + } + + pub fn ext_withdraw_sell(ctx: Context, args: WithdrawSellArgs) -> Result<()> { + instructions::ext_withdraw_sell::handler(ctx, args) + } } diff --git a/programs/mmm/src/state.rs b/programs/mmm/src/state.rs index 569fdd2..043d2c1 100644 --- a/programs/mmm/src/state.rs +++ b/programs/mmm/src/state.rs @@ -10,6 +10,7 @@ pub const ALLOWLIST_KIND_FVCA: u8 = 1; pub const ALLOWLIST_KIND_MINT: u8 = 2; pub const ALLOWLIST_KIND_MCC: u8 = 3; pub const ALLOWLIST_KIND_METADATA: u8 = 4; +pub const ALLOWLIST_KIND_GROUP: u8 = 5; // ANY nft will pass the allowlist check, please make sure to use cosigner to check NFT validity pub const ALLOWLIST_KIND_ANY: u8 = u8::MAX; @@ -25,10 +26,11 @@ impl Allowlist { // kind == 2: single mint, useful for SFT // kind == 3: verified MCC // kind == 4: metadata - // kind == 5,6,... will be supported in the future + // kind == 5: group extension + // kind == 6,7,... will be supported in the future // kind == 255: any pub fn valid(&self) -> bool { - if self.kind > ALLOWLIST_KIND_METADATA && self.kind != ALLOWLIST_KIND_ANY { + if self.kind > ALLOWLIST_KIND_GROUP && self.kind != ALLOWLIST_KIND_ANY { return false; } if self.kind != 0 && self.kind != ALLOWLIST_KIND_ANY { diff --git a/programs/mmm/src/util.rs b/programs/mmm/src/util.rs index 6884288..0c26abd 100644 --- a/programs/mmm/src/util.rs +++ b/programs/mmm/src/util.rs @@ -1,12 +1,10 @@ use crate::{ constants::{ - BUYSIDE_SOL_ESCROW_ACCOUNT_PREFIX, M2_AUCTION_HOUSE, M2_PREFIX, M2_PROGRAM, - MAX_METADATA_CREATOR_ROYALTY_BP, MAX_REFERRAL_FEE_BP, MAX_TOTAL_PRICE, - MIN_SOL_ESCROW_BALANCE_BP, POOL_PREFIX, + M2_AUCTION_HOUSE, M2_PREFIX, M2_PROGRAM, MAX_METADATA_CREATOR_ROYALTY_BP, + MAX_REFERRAL_FEE_BP, MAX_TOTAL_PRICE, MIN_SOL_ESCROW_BALANCE_BP, POOL_PREFIX, }, errors::MMMErrorCode, state::*, - ID, }; use anchor_lang::{prelude::*, solana_program::log::sol_log_data}; use anchor_spl::token_interface::Mint; @@ -19,6 +17,15 @@ use mpl_token_metadata::{ }; use open_creator_protocol::state::Policy; use solana_program::program::invoke_signed; +use spl_token_2022::{ + extension::{ + group_member_pointer::GroupMemberPointer, metadata_pointer::MetadataPointer, + BaseStateWithExtensions, StateWithExtensions, + }, + state::Mint as Token22Mint, +}; +use spl_token_group_interface::state::TokenGroupMember; +use spl_token_metadata_interface::state::TokenMetadata; use std::convert::TryFrom; #[macro_export] @@ -649,3 +656,158 @@ pub fn check_remaining_accounts_for_m2( Ok(()) } + +pub fn check_allowlists_for_mint_ext( + allowlists: &[Allowlist], + token_mint: &AccountInfo, + allowlist_aux: Option, +) -> Result { + if token_mint.owner != &spl_token_2022::ID || token_mint.data_is_empty() { + return Err(MMMErrorCode::InvalidTokenMint.into()); + } + let borrowed_data = token_mint.data.borrow(); + let mint_deserialized = StateWithExtensions::::unpack(&borrowed_data)?; + if !mint_deserialized.base.is_initialized { + return Err(MMMErrorCode::InvalidTokenMint.into()); + } + + // verify metadata extension + if let Ok(metadata_ptr) = mint_deserialized.get_extension::() { + if Option::::from(metadata_ptr.metadata_address) != Some(*token_mint.key) { + return Err(MMMErrorCode::InvalidTokenMetadataExtension.into()); + } + } + let parsed_metadata = mint_deserialized + .get_variable_len_extension::() + .unwrap(); + + if allowlists + .iter() + .any(|&val| val.kind == ALLOWLIST_KIND_METADATA) + { + // If allowlist_aux is not passed in, do not validate URI. + if let Some(ref aux_key) = allowlist_aux { + // Handle URI padding. + if !parsed_metadata.uri.trim().starts_with(aux_key) { + msg!( + "Failed metadata validation. Expected URI: |{}| but got |{}|", + *aux_key, + parsed_metadata.uri + ); + return Err(MMMErrorCode::UnexpectedMetadataUri.into()); + } + } + } + + // verify group member extension + if let Ok(group_member_ptr) = mint_deserialized.get_extension::() { + if Some(*token_mint.key) != Option::::from(group_member_ptr.member_address) { + msg!("group member pointer does not point to itself"); + return Err(MMMErrorCode::InvalidTokenMemberExtension.into()); + } + } + + for allowlist_val in allowlists.iter() { + match allowlist_val.kind { + ALLOWLIST_KIND_EMPTY => {} + ALLOWLIST_KIND_ANY => { + // any is a special case, we don't need to check anything else + return Ok(parsed_metadata); + } + ALLOWLIST_KIND_FVCA => { + return Err(MMMErrorCode::InvalidAllowLists.into()); + } + ALLOWLIST_KIND_MINT => { + if token_mint.key() == allowlist_val.value { + return Ok(parsed_metadata); + } + } + ALLOWLIST_KIND_MCC => { + return Err(MMMErrorCode::InvalidAllowLists.into()); + } + ALLOWLIST_KIND_GROUP => { + let group_address = assert_valid_group(&mint_deserialized, token_mint)?; + if group_address != Some(allowlist_val.value) { + msg!("group address |{}| is not allowed", group_address.unwrap()); + return Err(MMMErrorCode::InvalidAllowLists.into()); + } + return Ok(parsed_metadata); + } + ALLOWLIST_KIND_METADATA => { + // Do not validate URI here, as we already did it above. + // Group is validated in a separate function. + // These checks are separate since allowlist values are unioned together. + continue; + } + _ => { + return Err(MMMErrorCode::InvalidAllowLists.into()); + } + } + } + + // at the end, we didn't find a match, thus return err + Err(MMMErrorCode::InvalidAllowLists.into()) +} + +pub fn assert_valid_group( + mint_deserialized: &StateWithExtensions<'_, Token22Mint>, + token_mint: &AccountInfo, +) -> Result> { + if let Ok(group_member) = mint_deserialized.get_extension::() { + // counter spoof check + if group_member.mint != *token_mint.key { + msg!("group member mint does not match the token mint"); + return Err(MMMErrorCode::InvalidTokenMemberExtension.into()); + } + return Ok(Some(group_member.group)); + } + Err(MMMErrorCode::InvalidTokenMemberExtension.into()) +} + +pub struct PoolPriceInfo<'info> { + pub total_price: u64, + pub next_price: u64, + pub lp_fee: u64, + pub maker_fee: i64, + pub taker_fee: i64, + pub referral_fee: u64, + pub transfer_sol_to: AccountInfo<'info>, +} + +pub fn get_sell_fulfill_pool_price_info<'info>( + pool: &Pool, + owner: &UncheckedAccount<'info>, + buyside_sol_escrow_account: &AccountInfo<'info>, + asset_amount: u64, + maker_fee_bp: i16, + taker_fee_bp: i16, +) -> Result> { + let (total_price, next_price) = get_sol_total_price_and_next_price(pool, asset_amount, false)?; + let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), total_price)?; + + assert_valid_fees_bp(maker_fee_bp, taker_fee_bp)?; + let maker_fee = get_sol_fee(total_price, maker_fee_bp)?; + let taker_fee = get_sol_fee(total_price, taker_fee_bp)?; + let referral_fee = u64::try_from( + maker_fee + .checked_add(taker_fee) + .ok_or(MMMErrorCode::NumericOverflow)?, + ) + .map_err(|_| MMMErrorCode::NumericOverflow)?; + + let transfer_sol_to = if pool.reinvest_fulfill_sell { + buyside_sol_escrow_account.to_account_info() + } else { + owner.to_account_info() + }; + + Ok(PoolPriceInfo { + total_price, + next_price, + lp_fee, + maker_fee, + taker_fee, + referral_fee, + transfer_sol_to, + }) +} diff --git a/sdk/src/constants.ts b/sdk/src/constants.ts index 6d4fc0b..162c385 100644 --- a/sdk/src/constants.ts +++ b/sdk/src/constants.ts @@ -15,5 +15,6 @@ export enum AllowlistKind { mint = 2, mcc = 3, metadata = 4, + group = 5, any = 255, } diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index d590021..89e04b2 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -1601,6 +1601,313 @@ export type Mmm = { } } ] + }, + { + "name": "extDepositSell", + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "assetTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "DepositSellArgs" + } + } + ] + }, + { + "name": "solExtFulfillSell", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "payerAssetAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SolFulfillSellArgs" + } + } + ] + }, + { + "name": "solExtFulfillBuy", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "payerAssetAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "ownerTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "allowlistAuxAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SolFulfillBuyArgs" + } + } + ] + }, + { + "name": "extWithdrawSell", + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "assetTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "allowlistAuxAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "WithdrawSellArgs" + } + } + ] } ], "accounts": [ @@ -2250,6 +2557,16 @@ export type Mmm = { "code": 6029, "name": "InvalidRemainingAccounts", "msg": "Invalid remaining accounts" + }, + { + "code": 6030, + "name": "InvalidTokenMetadataExtension", + "msg": "Invalid token metadata extensions" + }, + { + "code": 6031, + "name": "InvalidTokenMemberExtension", + "msg": "Invalid token member extensions" } ] }; @@ -2333,8 +2650,218 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "pool", - "isMut": true, + "name": "pool", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "UpdateAllowlistsArgs" + } + } + ] + }, + { + "name": "solClosePool", + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "solDepositBuy", + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SolDepositBuyArgs" + } + } + ] + }, + { + "name": "solWithdrawBuy", + "accounts": [ + { + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SolWithdrawBuyArgs" + } + } + ] + }, + { + "name": "solFulfillBuy", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMetadata", + "isMut": false, + "isSigner": false + }, + { + "name": "assetMasterEdition", + "isMut": false, + "isSigner": false + }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "payerAssetAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "ownerTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "allowlistAuxAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, "isSigner": false } ], @@ -2342,24 +2869,34 @@ export const IDL: Mmm = { { "name": "args", "type": { - "defined": "UpdateAllowlistsArgs" + "defined": "SolFulfillBuyArgs" } } ] }, { - "name": "solClosePool", + "name": "solFulfillSell", "accounts": [ { - "name": "owner", + "name": "payer", "isMut": true, "isSigner": true }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, { "name": "cosigner", "isMut": false, "isSigner": true }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, { "name": "pool", "isMut": true, @@ -2367,37 +2904,41 @@ export const IDL: Mmm = { }, { "name": "buysideSolEscrowAccount", - "isMut": false, + "isMut": true, "isSigner": false }, { - "name": "systemProgram", + "name": "assetMetadata", "isMut": false, "isSigner": false - } - ], - "args": [] - }, - { - "name": "solDepositBuy", - "accounts": [ + }, { - "name": "owner", - "isMut": true, - "isSigner": true + "name": "assetMasterEdition", + "isMut": false, + "isSigner": false }, { - "name": "cosigner", + "name": "assetMint", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "pool", + "name": "sellsideEscrowTokenAccount", "isMut": true, "isSigner": false }, { - "name": "buysideSolEscrowAccount", + "name": "payerAssetAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "allowlistAuxAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "sellState", "isMut": true, "isSigner": false }, @@ -2405,19 +2946,34 @@ export const IDL: Mmm = { "name": "systemProgram", "isMut": false, "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false } ], "args": [ { "name": "args", "type": { - "defined": "SolDepositBuyArgs" + "defined": "SolFulfillSellArgs" } } ] }, { - "name": "solWithdrawBuy", + "name": "withdrawSell", "accounts": [ { "name": "owner", @@ -2434,59 +2990,84 @@ export const IDL: Mmm = { "isMut": true, "isSigner": false }, + { + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "assetTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", + "isMut": true, + "isSigner": false + }, { "name": "buysideSolEscrowAccount", "isMut": true, "isSigner": false }, + { + "name": "allowlistAuxAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, { "name": "systemProgram", "isMut": false, "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false } ], "args": [ { "name": "args", "type": { - "defined": "SolWithdrawBuyArgs" + "defined": "WithdrawSellArgs" } } ] }, { - "name": "solFulfillBuy", + "name": "depositSell", "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, { "name": "owner", "isMut": true, - "isSigner": false + "isSigner": true }, { "name": "cosigner", "isMut": false, "isSigner": true }, - { - "name": "referral", - "isMut": true, - "isSigner": false - }, { "name": "pool", "isMut": true, "isSigner": false }, - { - "name": "buysideSolEscrowAccount", - "isMut": true, - "isSigner": false - }, { "name": "assetMetadata", "isMut": false, @@ -2503,7 +3084,7 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "payerAssetAccount", + "name": "assetTokenAccount", "isMut": true, "isSigner": false }, @@ -2513,7 +3094,7 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "ownerTokenAccount", + "name": "sellState", "isMut": true, "isSigner": false }, @@ -2522,11 +3103,6 @@ export const IDL: Mmm = { "isMut": false, "isSigner": false }, - { - "name": "sellState", - "isMut": true, - "isSigner": false - }, { "name": "systemProgram", "isMut": false, @@ -2552,77 +3128,87 @@ export const IDL: Mmm = { { "name": "args", "type": { - "defined": "SolFulfillBuyArgs" + "defined": "DepositSellArgs" } } ] }, { - "name": "solFulfillSell", + "name": "ocpDepositSell", "accounts": [ { - "name": "payer", + "name": "owner", "isMut": true, "isSigner": true }, { - "name": "owner", + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "pool", "isMut": true, "isSigner": false }, { - "name": "cosigner", + "name": "assetMetadata", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "referral", + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "assetTokenAccount", "isMut": true, "isSigner": false }, { - "name": "pool", + "name": "sellsideEscrowTokenAccount", "isMut": true, "isSigner": false }, { - "name": "buysideSolEscrowAccount", + "name": "sellState", "isMut": true, "isSigner": false }, { - "name": "assetMetadata", + "name": "allowlistAuxAccount", "isMut": false, "isSigner": false }, { - "name": "assetMasterEdition", - "isMut": false, + "name": "ocpMintState", + "isMut": true, "isSigner": false }, { - "name": "assetMint", + "name": "ocpPolicy", "isMut": false, "isSigner": false }, { - "name": "sellsideEscrowTokenAccount", - "isMut": true, + "name": "ocpFreezeAuthority", + "isMut": false, "isSigner": false }, { - "name": "payerAssetAccount", - "isMut": true, + "name": "ocpProgram", + "isMut": false, "isSigner": false }, { - "name": "allowlistAuxAccount", + "name": "cmtProgram", "isMut": false, "isSigner": false }, { - "name": "sellState", - "isMut": true, + "name": "instructions", + "isMut": false, "isSigner": false }, { @@ -2650,36 +3236,56 @@ export const IDL: Mmm = { { "name": "args", "type": { - "defined": "SolFulfillSellArgs" + "defined": "DepositSellArgs" } } ] }, { - "name": "withdrawSell", + "name": "solOcpFulfillBuy", "accounts": [ { - "name": "owner", + "name": "payer", "isMut": true, "isSigner": true }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, { "name": "cosigner", "isMut": false, "isSigner": true }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, { "name": "pool", "isMut": true, "isSigner": false }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMetadata", + "isMut": false, + "isSigner": false + }, { "name": "assetMint", "isMut": false, "isSigner": false }, { - "name": "assetTokenAccount", + "name": "payerAssetAccount", "isMut": true, "isSigner": false }, @@ -2689,7 +3295,7 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "buysideSolEscrowAccount", + "name": "ownerTokenAccount", "isMut": true, "isSigner": false }, @@ -2699,8 +3305,38 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "sellState", - "isMut": true, + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "ocpMintState", + "isMut": true, + "isSigner": false + }, + { + "name": "ocpPolicy", + "isMut": false, + "isSigner": false + }, + { + "name": "ocpFreezeAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "ocpProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "cmtProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, "isSigner": false }, { @@ -2728,36 +3364,46 @@ export const IDL: Mmm = { { "name": "args", "type": { - "defined": "WithdrawSellArgs" + "defined": "SolFulfillBuyArgs" } } ] }, { - "name": "depositSell", + "name": "solOcpFulfillSell", "accounts": [ { - "name": "owner", + "name": "payer", "isMut": true, "isSigner": true }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, { "name": "cosigner", "isMut": false, "isSigner": true }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, { "name": "pool", "isMut": true, "isSigner": false }, { - "name": "assetMetadata", - "isMut": false, + "name": "buysideSolEscrowAccount", + "isMut": true, "isSigner": false }, { - "name": "assetMasterEdition", + "name": "assetMetadata", "isMut": false, "isSigner": false }, @@ -2767,22 +3413,52 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "assetTokenAccount", + "name": "sellsideEscrowTokenAccount", "isMut": true, "isSigner": false }, { - "name": "sellsideEscrowTokenAccount", + "name": "payerAssetAccount", "isMut": true, "isSigner": false }, + { + "name": "allowlistAuxAccount", + "isMut": false, + "isSigner": false + }, { "name": "sellState", "isMut": true, "isSigner": false }, { - "name": "allowlistAuxAccount", + "name": "ocpMintState", + "isMut": true, + "isSigner": false + }, + { + "name": "ocpPolicy", + "isMut": false, + "isSigner": false + }, + { + "name": "ocpFreezeAuthority", + "isMut": false, + "isSigner": false + }, + { + "name": "ocpProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "cmtProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", "isMut": false, "isSigner": false }, @@ -2811,13 +3487,13 @@ export const IDL: Mmm = { { "name": "args", "type": { - "defined": "DepositSellArgs" + "defined": "SolOcpFulfillSellArgs" } } ] }, { - "name": "ocpDepositSell", + "name": "ocpWithdrawSell", "accounts": [ { "name": "owner", @@ -2835,12 +3511,12 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "assetMetadata", + "name": "assetMint", "isMut": false, "isSigner": false }, { - "name": "assetMint", + "name": "assetMetadata", "isMut": false, "isSigner": false }, @@ -2855,7 +3531,7 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "sellState", + "name": "buysideSolEscrowAccount", "isMut": true, "isSigner": false }, @@ -2864,6 +3540,11 @@ export const IDL: Mmm = { "isMut": false, "isSigner": false }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, { "name": "ocpMintState", "isMut": true, @@ -2919,56 +3600,46 @@ export const IDL: Mmm = { { "name": "args", "type": { - "defined": "DepositSellArgs" + "defined": "WithdrawSellArgs" } } ] }, { - "name": "solOcpFulfillBuy", + "name": "mip1DepositSell", "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, { "name": "owner", "isMut": true, - "isSigner": false + "isSigner": true }, { "name": "cosigner", "isMut": false, "isSigner": true }, - { - "name": "referral", - "isMut": true, - "isSigner": false - }, { "name": "pool", "isMut": true, "isSigner": false }, { - "name": "buysideSolEscrowAccount", + "name": "assetMetadata", "isMut": true, "isSigner": false }, { - "name": "assetMetadata", + "name": "assetMint", "isMut": false, "isSigner": false }, { - "name": "assetMint", + "name": "assetMasterEdition", "isMut": false, "isSigner": false }, { - "name": "payerAssetAccount", + "name": "assetTokenAccount", "isMut": true, "isSigner": false }, @@ -2978,7 +3649,7 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "ownerTokenAccount", + "name": "sellState", "isMut": true, "isSigner": false }, @@ -2988,32 +3659,27 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "sellState", + "name": "ownerTokenRecord", "isMut": true, "isSigner": false }, { - "name": "ocpMintState", + "name": "destinationTokenRecord", "isMut": true, "isSigner": false }, { - "name": "ocpPolicy", - "isMut": false, - "isSigner": false - }, - { - "name": "ocpFreezeAuthority", + "name": "authorizationRules", "isMut": false, "isSigner": false }, { - "name": "ocpProgram", + "name": "tokenMetadataProgram", "isMut": false, "isSigner": false }, { - "name": "cmtProgram", + "name": "authorizationRulesProgram", "isMut": false, "isSigner": false }, @@ -3047,23 +3713,18 @@ export const IDL: Mmm = { { "name": "args", "type": { - "defined": "SolFulfillBuyArgs" + "defined": "DepositSellArgs" } } ] }, { - "name": "solOcpFulfillSell", + "name": "mip1WithdrawSell", "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, { "name": "owner", "isMut": true, - "isSigner": false + "isSigner": true }, { "name": "cosigner", @@ -3071,28 +3732,28 @@ export const IDL: Mmm = { "isSigner": true }, { - "name": "referral", + "name": "pool", "isMut": true, "isSigner": false }, { - "name": "pool", - "isMut": true, + "name": "assetMint", + "isMut": false, "isSigner": false }, { - "name": "buysideSolEscrowAccount", - "isMut": true, + "name": "assetMasterEdition", + "isMut": false, "isSigner": false }, { "name": "assetMetadata", - "isMut": false, + "isMut": true, "isSigner": false }, { - "name": "assetMint", - "isMut": false, + "name": "assetTokenAccount", + "isMut": true, "isSigner": false }, { @@ -3101,7 +3762,7 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "payerAssetAccount", + "name": "buysideSolEscrowAccount", "isMut": true, "isSigner": false }, @@ -3116,27 +3777,27 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "ocpMintState", + "name": "ownerTokenRecord", "isMut": true, "isSigner": false }, { - "name": "ocpPolicy", - "isMut": false, + "name": "destinationTokenRecord", + "isMut": true, "isSigner": false }, { - "name": "ocpFreezeAuthority", + "name": "authorizationRules", "isMut": false, "isSigner": false }, { - "name": "ocpProgram", + "name": "tokenMetadataProgram", "isMut": false, "isSigner": false }, { - "name": "cmtProgram", + "name": "authorizationRulesProgram", "isMut": false, "isSigner": false }, @@ -3170,42 +3831,57 @@ export const IDL: Mmm = { { "name": "args", "type": { - "defined": "SolOcpFulfillSellArgs" + "defined": "WithdrawSellArgs" } } ] }, { - "name": "ocpWithdrawSell", + "name": "solMip1FulfillSell", "accounts": [ { - "name": "owner", + "name": "payer", "isMut": true, "isSigner": true }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, { "name": "cosigner", "isMut": false, "isSigner": true }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, { "name": "pool", "isMut": true, "isSigner": false }, { - "name": "assetMint", - "isMut": false, + "name": "buysideSolEscrowAccount", + "isMut": true, "isSigner": false }, { "name": "assetMetadata", + "isMut": true, + "isSigner": false + }, + { + "name": "assetMint", "isMut": false, "isSigner": false }, { - "name": "assetTokenAccount", - "isMut": true, + "name": "assetMasterEdition", + "isMut": false, "isSigner": false }, { @@ -3214,7 +3890,7 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "buysideSolEscrowAccount", + "name": "payerAssetAccount", "isMut": true, "isSigner": false }, @@ -3229,27 +3905,27 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "ocpMintState", + "name": "ownerTokenRecord", "isMut": true, "isSigner": false }, { - "name": "ocpPolicy", - "isMut": false, + "name": "destinationTokenRecord", + "isMut": true, "isSigner": false }, { - "name": "ocpFreezeAuthority", + "name": "authorizationRules", "isMut": false, "isSigner": false }, { - "name": "ocpProgram", + "name": "tokenMetadataProgram", "isMut": false, "isSigner": false }, { - "name": "cmtProgram", + "name": "authorizationRulesProgram", "isMut": false, "isSigner": false }, @@ -3283,29 +3959,44 @@ export const IDL: Mmm = { { "name": "args", "type": { - "defined": "WithdrawSellArgs" + "defined": "SolMip1FulfillSellArgs" } } ] }, { - "name": "mip1DepositSell", + "name": "solMip1FulfillBuy", "accounts": [ { - "name": "owner", + "name": "payer", "isMut": true, "isSigner": true }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, { "name": "cosigner", "isMut": false, "isSigner": true }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, { "name": "pool", "isMut": true, "isSigner": false }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, { "name": "assetMetadata", "isMut": true, @@ -3322,7 +4013,7 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "assetTokenAccount", + "name": "payerAssetAccount", "isMut": true, "isSigner": false }, @@ -3332,7 +4023,7 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "sellState", + "name": "ownerTokenAccount", "isMut": true, "isSigner": false }, @@ -3342,27 +4033,46 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "ownerTokenRecord", + "name": "sellState", "isMut": true, "isSigner": false }, { - "name": "destinationTokenRecord", + "name": "tokenOwnerTokenRecord", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "This is the token record for the seller" + ] }, { - "name": "authorizationRules", + "name": "poolTokenRecord", + "isMut": true, + "isSigner": false, + "docs": [ + "This is the token record for the pool - will always be required" + ] + }, + { + "name": "poolOwnerTokenRecord", + "isMut": true, + "isSigner": false, + "docs": [ + "This is the token record for the pool owner - will be required if reinvest = true" + ] + }, + { + "name": "tokenMetadataProgram", "isMut": false, "isSigner": false }, { - "name": "tokenMetadataProgram", + "name": "authorizationRulesProgram", "isMut": false, "isSigner": false }, { - "name": "authorizationRulesProgram", + "name": "authorizationRules", "isMut": false, "isSigner": false }, @@ -3396,97 +4106,111 @@ export const IDL: Mmm = { { "name": "args", "type": { - "defined": "DepositSellArgs" + "defined": "SolFulfillBuyArgs" } } ] }, { - "name": "mip1WithdrawSell", + "name": "closeIfBalanceInvalid", "accounts": [ { - "name": "owner", - "isMut": true, - "isSigner": true - }, - { - "name": "cosigner", + "name": "authority", "isMut": false, "isSigner": true }, { - "name": "pool", + "name": "owner", "isMut": true, "isSigner": false }, { - "name": "assetMint", - "isMut": false, + "name": "pool", + "isMut": true, "isSigner": false }, { - "name": "assetMasterEdition", - "isMut": false, + "name": "buysideSolEscrowAccount", + "isMut": true, "isSigner": false }, { - "name": "assetMetadata", - "isMut": true, + "name": "systemProgram", + "isMut": false, "isSigner": false - }, + } + ], + "args": [] + }, + { + "name": "setSharedEscrow", + "accounts": [ { - "name": "assetTokenAccount", + "name": "owner", "isMut": true, - "isSigner": false + "isSigner": true }, { - "name": "sellsideEscrowTokenAccount", - "isMut": true, - "isSigner": false + "name": "cosigner", + "isMut": false, + "isSigner": true }, { - "name": "buysideSolEscrowAccount", + "name": "pool", "isMut": true, "isSigner": false }, { - "name": "allowlistAuxAccount", + "name": "sharedEscrowAccount", "isMut": false, "isSigner": false - }, + } + ], + "args": [ { - "name": "sellState", + "name": "args", + "type": { + "defined": "SetSharedEscrowArgs" + } + } + ] + }, + { + "name": "extDepositSell", + "accounts": [ + { + "name": "owner", "isMut": true, - "isSigner": false + "isSigner": true }, { - "name": "ownerTokenRecord", - "isMut": true, - "isSigner": false + "name": "cosigner", + "isMut": false, + "isSigner": true }, { - "name": "destinationTokenRecord", + "name": "pool", "isMut": true, "isSigner": false }, { - "name": "authorizationRules", + "name": "assetMint", "isMut": false, "isSigner": false }, { - "name": "tokenMetadataProgram", - "isMut": false, + "name": "assetTokenAccount", + "isMut": true, "isSigner": false }, { - "name": "authorizationRulesProgram", - "isMut": false, + "name": "sellsideEscrowTokenAccount", + "isMut": true, "isSigner": false }, { - "name": "instructions", - "isMut": false, + "name": "sellState", + "isMut": true, "isSigner": false }, { @@ -3503,24 +4227,19 @@ export const IDL: Mmm = { "name": "associatedTokenProgram", "isMut": false, "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false } ], "args": [ { "name": "args", "type": { - "defined": "WithdrawSellArgs" + "defined": "DepositSellArgs" } } ] }, { - "name": "solMip1FulfillSell", + "name": "solExtFulfillSell", "accounts": [ { "name": "payer", @@ -3552,21 +4271,11 @@ export const IDL: Mmm = { "isMut": true, "isSigner": false }, - { - "name": "assetMetadata", - "isMut": true, - "isSigner": false - }, { "name": "assetMint", "isMut": false, "isSigner": false }, - { - "name": "assetMasterEdition", - "isMut": false, - "isSigner": false - }, { "name": "sellsideEscrowTokenAccount", "isMut": true, @@ -3577,46 +4286,11 @@ export const IDL: Mmm = { "isMut": true, "isSigner": false }, - { - "name": "allowlistAuxAccount", - "isMut": false, - "isSigner": false - }, { "name": "sellState", "isMut": true, "isSigner": false }, - { - "name": "ownerTokenRecord", - "isMut": true, - "isSigner": false - }, - { - "name": "destinationTokenRecord", - "isMut": true, - "isSigner": false - }, - { - "name": "authorizationRules", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenMetadataProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "authorizationRulesProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "instructions", - "isMut": false, - "isSigner": false - }, { "name": "systemProgram", "isMut": false, @@ -3631,24 +4305,19 @@ export const IDL: Mmm = { "name": "associatedTokenProgram", "isMut": false, "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false } ], "args": [ { "name": "args", "type": { - "defined": "SolMip1FulfillSellArgs" + "defined": "SolFulfillSellArgs" } } ] }, { - "name": "solMip1FulfillBuy", + "name": "solExtFulfillBuy", "accounts": [ { "name": "payer", @@ -3680,21 +4349,11 @@ export const IDL: Mmm = { "isMut": true, "isSigner": false }, - { - "name": "assetMetadata", - "isMut": true, - "isSigner": false - }, { "name": "assetMint", "isMut": false, "isSigner": false }, - { - "name": "assetMasterEdition", - "isMut": false, - "isSigner": false - }, { "name": "payerAssetAccount", "isMut": true, @@ -3720,50 +4379,6 @@ export const IDL: Mmm = { "isMut": true, "isSigner": false }, - { - "name": "tokenOwnerTokenRecord", - "isMut": true, - "isSigner": false, - "docs": [ - "This is the token record for the seller" - ] - }, - { - "name": "poolTokenRecord", - "isMut": true, - "isSigner": false, - "docs": [ - "This is the token record for the pool - will always be required" - ] - }, - { - "name": "poolOwnerTokenRecord", - "isMut": true, - "isSigner": false, - "docs": [ - "This is the token record for the pool owner - will be required if reinvest = true" - ] - }, - { - "name": "tokenMetadataProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "authorizationRulesProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "authorizationRules", - "isMut": false, - "isSigner": false - }, - { - "name": "instructions", - "isMut": false, - "isSigner": false - }, { "name": "systemProgram", "isMut": false, @@ -3795,20 +4410,35 @@ export const IDL: Mmm = { ] }, { - "name": "closeIfBalanceInvalid", + "name": "extWithdrawSell", "accounts": [ { - "name": "authority", + "name": "owner", + "isMut": true, + "isSigner": true + }, + { + "name": "cosigner", "isMut": false, "isSigner": true }, { - "name": "owner", + "name": "pool", "isMut": true, "isSigner": false }, { - "name": "pool", + "name": "assetMint", + "isMut": false, + "isSigner": false + }, + { + "name": "assetTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "sellsideEscrowTokenAccount", "isMut": true, "isSigner": false }, @@ -3818,33 +4448,27 @@ export const IDL: Mmm = { "isSigner": false }, { - "name": "systemProgram", + "name": "allowlistAuxAccount", "isMut": false, "isSigner": false - } - ], - "args": [] - }, - { - "name": "setSharedEscrow", - "accounts": [ + }, { - "name": "owner", + "name": "sellState", "isMut": true, - "isSigner": true + "isSigner": false }, { - "name": "cosigner", + "name": "systemProgram", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "pool", - "isMut": true, + "name": "tokenProgram", + "isMut": false, "isSigner": false }, { - "name": "sharedEscrowAccount", + "name": "associatedTokenProgram", "isMut": false, "isSigner": false } @@ -3853,7 +4477,7 @@ export const IDL: Mmm = { { "name": "args", "type": { - "defined": "SetSharedEscrowArgs" + "defined": "WithdrawSellArgs" } } ] @@ -4506,6 +5130,16 @@ export const IDL: Mmm = { "code": 6029, "name": "InvalidRemainingAccounts", "msg": "Invalid remaining accounts" + }, + { + "code": 6030, + "name": "InvalidTokenMetadataExtension", + "msg": "Invalid token metadata extensions" + }, + { + "code": 6031, + "name": "InvalidTokenMemberExtension", + "msg": "Invalid token member extensions" } ] }; diff --git a/tests/deps/spl_token_2022.so b/tests/deps/spl_token_2022.so new file mode 100644 index 0000000..5c4c3cc Binary files /dev/null and b/tests/deps/spl_token_2022.so differ diff --git a/tests/mmm-ext-deposit.spec.ts b/tests/mmm-ext-deposit.spec.ts new file mode 100644 index 0000000..9040028 --- /dev/null +++ b/tests/mmm-ext-deposit.spec.ts @@ -0,0 +1,571 @@ +import * as anchor from '@project-serum/anchor'; +import { + getAssociatedTokenAddress, + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, +} from '@solana/spl-token'; +import { Keypair, PublicKey, SystemProgram } from '@solana/web3.js'; +import { assert, expect } from 'chai'; +import { + Mmm, + AllowlistKind, + getMMMSellStatePDA, + IDL, + MMMProgramID, +} from '../sdk/src'; +import { + airdrop, + createPool, + createPoolWithExampleT22ExtDeposits, + createTestGroupMemberMint, + createTestGroupMintExt, + createTestMintAndTokenT22VanillaExt, + getEmptyAllowLists, + getTokenAccount2022, +} from './utils'; + +describe('mmm-ext-deposit', () => { + const { connection } = anchor.AnchorProvider.env(); + const wallet = new anchor.Wallet(Keypair.generate()); + const provider = new anchor.AnchorProvider(connection, wallet, { + commitment: 'processed', + }); + const program = new anchor.Program( + IDL, + MMMProgramID, + provider, + ) as anchor.Program; + const cosigner = Keypair.generate(); + + beforeEach(async () => { + await airdrop(connection, wallet.publicKey, 50); + }); + + describe('ext_deposit_sell', () => { + it('correctly verifies depositing nfts with group allowlist', async () => { + const { + mint, + recipientTokenAccount, + poolData, + poolAta, + sellState, + groupAddress, + } = await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'none', + { + owner: wallet.publicKey, + cosigner, + }, + ); + + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: 'example.com', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + + let nftEscrow = await getTokenAccount2022( + connection, + poolAta, + TOKEN_2022_PROGRAM_ID, + ); + assert.equal(Number(nftEscrow.amount), 1); + assert.equal(nftEscrow.owner.toBase58(), poolData.poolKey.toBase58()); + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 1); + assert.equal(await connection.getBalance(recipientTokenAccount), 0); + + const sellStateAccountInfo = await program.account.sellState.fetch( + sellState, + ); + assert.equal( + sellStateAccountInfo.pool.toBase58(), + poolData.poolKey.toBase58(), + ); + assert.equal( + sellStateAccountInfo.poolOwner.toBase58(), + wallet.publicKey.toBase58(), + ); + assert.equal(sellStateAccountInfo.assetMint.toBase58(), mint.toBase58()); + assert.equal(sellStateAccountInfo.assetAmount.toNumber(), 1); + assert.deepEqual( + sellStateAccountInfo.cosignerAnnotation, + new Array(32).fill(0), + ); + + const { mint: mint2, recipientTokenAccount: recipientTokenAccount2 } = + await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + undefined, + groupAddress, + ); + const poolAta2 = await getAssociatedTokenAddress( + mint2, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + let { key: sellState2 } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + mint2, + ); + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: '', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint2, + assetTokenAccount: recipientTokenAccount2, + sellsideEscrowTokenAccount: poolAta2, + sellState: sellState2, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + + let nftEscrow2 = await getTokenAccount2022( + connection, + poolAta2, + TOKEN_2022_PROGRAM_ID, + ); + assert.equal(Number(nftEscrow2.amount), 1); + assert.equal(nftEscrow2.owner.toBase58(), poolData.poolKey.toBase58()); + poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + // should increment by 1 + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 2); + assert.equal(await connection.getBalance(recipientTokenAccount2), 0); + + const sellStateAccountInfo2 = await program.account.sellState.fetch( + sellState2, + ); + assert.equal( + sellStateAccountInfo2.pool.toBase58(), + poolData.poolKey.toBase58(), + ); + assert.equal( + sellStateAccountInfo2.poolOwner.toBase58(), + wallet.publicKey.toBase58(), + ); + assert.equal( + sellStateAccountInfo2.assetMint.toBase58(), + mint2.toBase58(), + ); + assert.equal(sellStateAccountInfo2.assetAmount.toNumber(), 1); + assert.deepEqual( + sellStateAccountInfo2.cosignerAnnotation, + new Array(32).fill(0), + ); + }); + + it('correctly verifies depositing nfts with ANY allowlist', async () => { + const { + mint, + recipientTokenAccount, + poolData, + poolAta, + sellState, + groupAddress, + } = await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'none', + { + owner: wallet.publicKey, + cosigner, + allowlists: [ + { + kind: AllowlistKind.any, + value: PublicKey.default, + }, + ...getEmptyAllowLists(5), + ], + }, + ); + + assert.equal(await connection.getBalance(poolAta), 0); + assert.equal(await connection.getBalance(sellState), 0); + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: '', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + + let nftEscrow = await getTokenAccount2022( + connection, + poolAta, + TOKEN_2022_PROGRAM_ID, + ); + assert.equal(Number(nftEscrow.amount), 1); + assert.equal(nftEscrow.owner.toBase58(), poolData.poolKey.toBase58()); + poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 1); + assert.equal(await connection.getBalance(recipientTokenAccount), 0); + + const sellStateAccountInfo = await program.account.sellState.fetch( + sellState, + ); + assert.equal( + sellStateAccountInfo.pool.toBase58(), + poolData.poolKey.toBase58(), + ); + assert.equal( + sellStateAccountInfo.poolOwner.toBase58(), + wallet.publicKey.toBase58(), + ); + assert.equal(sellStateAccountInfo.assetMint.toBase58(), mint.toBase58()); + assert.equal(sellStateAccountInfo.assetAmount.toNumber(), 1); + assert.deepEqual( + sellStateAccountInfo.cosignerAnnotation, + new Array(32).fill(0), + ); + + const { mint: mint2, recipientTokenAccount: recipientTokenAccount2 } = + await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + undefined, + groupAddress, + ); + const poolAta2 = await getAssociatedTokenAddress( + mint2, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + let { key: sellState2 } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + mint2, + ); + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: '', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint2, + assetTokenAccount: recipientTokenAccount2, + sellsideEscrowTokenAccount: poolAta2, + sellState: sellState2, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + + let nftEscrow2 = await getTokenAccount2022( + connection, + poolAta2, + TOKEN_2022_PROGRAM_ID, + ); + assert.equal(Number(nftEscrow2.amount), 1); + assert.equal(nftEscrow2.owner.toBase58(), poolData.poolKey.toBase58()); + poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + // should increment by 1 + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 2); + assert.equal(await connection.getBalance(recipientTokenAccount2), 0); + + const sellStateAccountInfo2 = await program.account.sellState.fetch( + sellState2, + ); + assert.equal( + sellStateAccountInfo2.pool.toBase58(), + poolData.poolKey.toBase58(), + ); + assert.equal( + sellStateAccountInfo2.poolOwner.toBase58(), + wallet.publicKey.toBase58(), + ); + assert.equal( + sellStateAccountInfo2.assetMint.toBase58(), + mint2.toBase58(), + ); + assert.equal(sellStateAccountInfo2.assetAmount.toNumber(), 1); + assert.deepEqual( + sellStateAccountInfo2.cosignerAnnotation, + new Array(32).fill(0), + ); + }); + + it('failed to verify depositing with wrong allowlist aux', async () => { + const { mint, recipientTokenAccount, poolData, poolAta, sellState } = + await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'none', + { + owner: wallet.publicKey, + cosigner, + }, + ); + + try { + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: 'wrong-aux', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + } catch (err) { + assertProgramError(err, 'Unexpected metadata uri'); + } + }); + + it('failed to verify depositing nfts with empty metadata list', async () => { + const { mint, recipientTokenAccount, poolData, poolAta, sellState } = + await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'none', + { + owner: wallet.publicKey, + cosigner, + allowlists: undefined, + }, + ); + + try { + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: '', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + } catch (err) { + assertProgramError(err, 'invalid allowlists'); + } + }); + + it('failed to verify depositing nfts with external group member pointer', async () => { + const { groupAddress } = await createTestGroupMintExt( + connection, + wallet.payer, + ); + const { groupMemberKeyPair } = await createTestGroupMemberMint( + connection, + wallet.payer, + groupAddress, + ); + const { mint, recipientTokenAccount } = + await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + undefined, + groupAddress, + groupMemberKeyPair.publicKey, // external group member pointer + ); + + const poolData = await createPool(program, { + owner: wallet.publicKey, + cosigner, + allowlists: [ + { + kind: AllowlistKind.metadata, + value: mint, + }, + { + kind: AllowlistKind.group, + value: groupAddress, + }, + ...getEmptyAllowLists(4), + ], + }); + + const poolAta = await getAssociatedTokenAddress( + mint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: sellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + mint, + ); + + assert.equal(await connection.getBalance(poolAta), 0); + assert.equal(await connection.getBalance(sellState), 0); + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + + try { + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: '', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + } catch (err) { + assertProgramError(err, 'Invalid token member extensions'); + } + }); + + it('failed to verify depositing nfts with disallowed group', async () => { + const { groupAddress } = await createTestGroupMintExt( + connection, + wallet.payer, + ); + const { mint, recipientTokenAccount } = + await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + undefined, + groupAddress, + ); + + const poolData = await createPool(program, { + owner: wallet.publicKey, + cosigner, + allowlists: [ + { + kind: AllowlistKind.metadata, + value: mint, + }, + { + kind: AllowlistKind.group, + value: mint, // unexpected group address + }, + ...getEmptyAllowLists(4), + ], + }); + + const poolAta = await getAssociatedTokenAddress( + mint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: sellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + mint, + ); + + assert.equal(await connection.getBalance(poolAta), 0); + assert.equal(await connection.getBalance(sellState), 0); + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + + try { + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: '', + }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + } catch (err) { + assertProgramError(err, 'invalid allowlists'); + } + }); + }); +}); + +function assertProgramError(_err: unknown, message: string) { + expect(_err).to.be.instanceOf(anchor.ProgramError); + const err = _err as anchor.ProgramError; + + assert.strictEqual(err.msg, message); +} diff --git a/tests/mmm-ext-fulfill.spec.ts b/tests/mmm-ext-fulfill.spec.ts new file mode 100644 index 0000000..b3fedb0 --- /dev/null +++ b/tests/mmm-ext-fulfill.spec.ts @@ -0,0 +1,928 @@ +import * as anchor from '@project-serum/anchor'; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + getAssociatedTokenAddress, +} from '@solana/spl-token'; +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SYSVAR_RENT_PUBKEY, + SystemProgram, +} from '@solana/web3.js'; +import { assert } from 'chai'; +import { + Mmm, + getMMMSellStatePDA, + IDL, + MMMProgramID, + CurveKind, + getSolFulfillBuyPrices, + getM2BuyerSharedEscrow, + M2_PROGRAM, +} from '../sdk/src'; +import { + IMMUTABLE_OWNER_EXTENSION_LAMPORTS, + LAMPORT_ERROR_RANGE, + PRICE_ERROR_RANGE, + SIGNATURE_FEE_LAMPORTS, + airdrop, + assertIsBetween, + createPoolWithExampleT22ExtDeposits, + createTestMintAndTokenT22VanillaExt, + getSellStatePDARent, + getTokenAccount2022, + getTokenAccountRent, + sendAndAssertTx, +} from './utils'; + +describe('mmm-ext-fulfill', () => { + const { connection } = anchor.AnchorProvider.env(); + const wallet = new anchor.Wallet(Keypair.generate()); + const provider = new anchor.AnchorProvider(connection, wallet, { + commitment: 'processed', + }); + const program = new anchor.Program( + IDL, + MMMProgramID, + provider, + ) as anchor.Program; + const cosigner = Keypair.generate(); + const buyer = Keypair.generate(); + + async function executeFulfillSell( + maxPaymentAmount: number, + referral: Keypair, + poolKey: PublicKey, + mint: PublicKey, + sellState: PublicKey, + solEscrowKey: PublicKey, + poolAta: PublicKey, + buyerNftAtaAddress: PublicKey, + takerFeeBp: number, + makerFeeBp: number, + ) { + const tx = await program.methods + .solExtFulfillSell({ + assetAmount: new anchor.BN(1), + maxPaymentAmount: new anchor.BN(maxPaymentAmount), + buysideCreatorRoyaltyBp: 0, + allowlistAux: '', + takerFeeBp, + makerFeeBp, + }) + .accountsStrict({ + payer: buyer.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: referral.publicKey, + pool: poolKey, + buysideSolEscrowAccount: solEscrowKey, + assetMint: mint, + sellsideEscrowTokenAccount: poolAta, + payerAssetAccount: buyerNftAtaAddress, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .transaction(); + + const blockhashData = await connection.getLatestBlockhash(); + tx.feePayer = buyer.publicKey; + tx.recentBlockhash = blockhashData.blockhash; + tx.partialSign(cosigner, buyer); + + await sendAndAssertTx(connection, tx, blockhashData, false); + } + + beforeEach(async () => { + await airdrop(connection, wallet.publicKey, 50); + await airdrop(connection, buyer.publicKey, 50); + }); + + describe('ext_fulfill_sell', () => { + it('Sellside only', async () => { + const { mint, poolData, poolAta, sellState, solEscrowKey } = + await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'sell', // side + { + owner: wallet.publicKey, + cosigner: cosigner, + curveType: CurveKind.exp, + curveDelta: new anchor.BN(200), // 200 basis points + expiry: new anchor.BN(new Date().getTime() / 1000 + 1000), + reinvestFulfillBuy: true, + reinvestFulfillSell: true, + }, + ); + + const buyerNftAtaAddress = await getAssociatedTokenAddress( + mint, + buyer.publicKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + let initWalletBalance = await connection.getBalance(wallet.publicKey); + let initReferralBalance = await connection.getBalance( + poolData.referral.publicKey, + ); + let initBuyerBalance = await connection.getBalance(buyer.publicKey); + + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal(poolAccountInfo.lpFeeEarned.toNumber(), 0); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 1); + + let expectedTakerFees = 1.02 * LAMPORTS_PER_SOL * 0.01; + await executeFulfillSell( + 1.02 * LAMPORTS_PER_SOL + expectedTakerFees, + poolData.referral, + poolData.poolKey, + mint, + sellState, + solEscrowKey, + poolAta, + buyerNftAtaAddress, + 100, // takerFeeBp + 0, // makerFeeBp + ); + + let tokenAccountRent = + (await getTokenAccountRent(connection)) + + IMMUTABLE_OWNER_EXTENSION_LAMPORTS; + const sellStatePDARent = await getSellStatePDARent(connection); + + const expectedTxFees = + SIGNATURE_FEE_LAMPORTS * 2 + // cosigner + payer + tokenAccountRent; // token account + + { + const expectedTakerFees = 1.02 * LAMPORTS_PER_SOL * 0.01; + const expectedReferralFees = expectedTakerFees; + const [ + buyerBalance, + referralBalance, + poolAtaBalance, + poolEscrowBalance, + afterWalletBalance, + sellStateBalance, + ] = await Promise.all([ + connection.getBalance(buyer.publicKey), + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(poolAta), + connection.getBalance(solEscrowKey), + connection.getBalance(wallet.publicKey), + connection.getBalance(sellState), + ]); + assert.equal( + buyerBalance, + initBuyerBalance - + 1.02 * LAMPORTS_PER_SOL - + expectedTxFees - + expectedTakerFees, + ); + assert.equal( + referralBalance, + initReferralBalance + expectedReferralFees, + ); + assert.equal(poolAtaBalance, 0); + assert.equal(poolEscrowBalance, 1.02 * LAMPORTS_PER_SOL); + assertIsBetween( + afterWalletBalance, + initWalletBalance + tokenAccountRent + sellStatePDARent, + LAMPORT_ERROR_RANGE, + ); + assert.equal(sellStateBalance, 0); + + initReferralBalance = referralBalance; + initBuyerBalance = buyerBalance; + } + + poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assert.equal( + poolAccountInfo.spotPrice.toNumber(), + 1.02 * LAMPORTS_PER_SOL, + ); + assert.equal(poolAccountInfo.lpFeeEarned.toNumber(), 0); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + assert.equal( + poolAccountInfo.buysidePaymentAmount.toNumber(), + 1.02 * LAMPORTS_PER_SOL, + ); + }); + }); + + describe('ext_fulfill_buy', () => { + it('Buyside only', async () => { + const seller = Keypair.generate(); + await airdrop(connection, seller.publicKey, 50); + const { poolData, solEscrowKey, groupAddress } = + await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'buy', // side + { + owner: wallet.publicKey, + cosigner, + curveType: CurveKind.exp, + curveDelta: new anchor.BN(125), // 125 bp + expiry: new anchor.BN(0), + spotPrice: new anchor.BN(0.5 * LAMPORTS_PER_SOL), + referralBp: 200, + reinvestFulfillBuy: false, + reinvestFulfillSell: false, + }, + ); + const { + mint: extraMint, + recipientTokenAccount: extraRecipientTokenAccount, + } = await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + seller.publicKey, + groupAddress, + ); + + const ownerExtraNftAtaAddress = await getAssociatedTokenAddress( + extraMint, + wallet.publicKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: extraNftSellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + extraMint, + ); + const extraPoolAta = await getAssociatedTokenAddress( + extraMint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + let [ + initReferralBalance, + initSellerBalance, + initPaymentEscrowBalance, + initWalletBalance, + ] = await Promise.all([ + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(solEscrowKey), + connection.getBalance(wallet.publicKey), + ]); + + const expectedTotalPrice = 0.5; + const tx = await program.methods + .solExtFulfillBuy({ + assetAmount: new anchor.BN(1), + minPaymentAmount: new anchor.BN( + expectedTotalPrice * LAMPORTS_PER_SOL, + ), + allowlistAux: '', + takerFeeBp: 0, + makerFeeBp: 150, + }) + .accountsStrict({ + payer: seller.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount: solEscrowKey, + assetMint: extraMint, + payerAssetAccount: extraRecipientTokenAccount, + sellsideEscrowTokenAccount: extraPoolAta, + ownerTokenAccount: ownerExtraNftAtaAddress, + allowlistAuxAccount: SystemProgram.programId, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + sellState: extraNftSellState, + }) + .transaction(); + + const blockhashData = await connection.getLatestBlockhash(); + tx.feePayer = seller.publicKey; + tx.recentBlockhash = blockhashData.blockhash; + tx.partialSign(cosigner, seller); + + await sendAndAssertTx(connection, tx, blockhashData, false); + + const expectedMakerFees = expectedTotalPrice * LAMPORTS_PER_SOL * 0.015; + const expectedReferralFees = expectedMakerFees; + const [ + sellerBalance, + referralBalance, + afterWalletBalance, + poolEscrowBalance, + ownerNftTokenAccount, + ] = await Promise.all([ + connection.getBalance(seller.publicKey), + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(wallet.publicKey), + connection.getBalance(solEscrowKey), + getTokenAccount2022( + connection, + ownerExtraNftAtaAddress, + TOKEN_2022_PROGRAM_ID, + ), + ]); + + assertIsBetween( + sellerBalance, + initSellerBalance + + expectedTotalPrice * LAMPORTS_PER_SOL - + SIGNATURE_FEE_LAMPORTS * 2, + PRICE_ERROR_RANGE, + ); + assert.equal(referralBalance, initReferralBalance + expectedReferralFees); + assert.equal( + poolEscrowBalance, + initPaymentEscrowBalance - + expectedTotalPrice * LAMPORTS_PER_SOL - + expectedMakerFees, + ); + assert.equal(afterWalletBalance, initWalletBalance); + + const poolAccountInfo = await program.account.pool.fetch( + poolData.poolKey, + ); + assertIsBetween( + poolAccountInfo.spotPrice.toNumber(), + (expectedTotalPrice * LAMPORTS_PER_SOL) / 1.0125, + PRICE_ERROR_RANGE, + ); + // do not reinvest so sell side asset amount should be 0 + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + assert.equal(Number(ownerNftTokenAccount.amount), 1); + }); + + it('Buyside only with shared escrow but pool open', async () => { + const seller = Keypair.generate(); + const buyerSharedEscrow = getM2BuyerSharedEscrow(wallet.publicKey).key; + await Promise.all([ + airdrop(connection, seller.publicKey, 50), + airdrop(connection, buyerSharedEscrow, 50), + ]); + const { poolData, solEscrowKey, groupAddress } = + await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'buy', // side + { + owner: wallet.publicKey, + cosigner, + curveType: CurveKind.exp, + curveDelta: new anchor.BN(125), // 125 bp + expiry: new anchor.BN(0), + spotPrice: new anchor.BN(0.5 * LAMPORTS_PER_SOL), + referralBp: 200, + reinvestFulfillBuy: false, + reinvestFulfillSell: false, + }, + true, // sharedEscrow + 2, // sharedEscrowCount, one more so pool will not be closed + ); + const { + mint: extraMint, + recipientTokenAccount: extraRecipientTokenAccount, + } = await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + seller.publicKey, + groupAddress, + ); + + const ownerExtraNftAtaAddress = await getAssociatedTokenAddress( + extraMint, + wallet.publicKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: extraNftSellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + extraMint, + ); + const extraPoolAta = await getAssociatedTokenAddress( + extraMint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + let [ + initReferralBalance, + initSellerBalance, + initWalletBalance, + initBuyerSharedEscrowBalance, + ] = await Promise.all([ + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(wallet.publicKey), + connection.getBalance(buyerSharedEscrow), + ]); + + const expectedTotalPrice = 0.5; + const tx = await program.methods + .solExtFulfillBuy({ + assetAmount: new anchor.BN(1), + minPaymentAmount: new anchor.BN( + expectedTotalPrice * LAMPORTS_PER_SOL, + ), + allowlistAux: '', + takerFeeBp: 0, + makerFeeBp: 150, + }) + .accountsStrict({ + payer: seller.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount: solEscrowKey, + assetMint: extraMint, + payerAssetAccount: extraRecipientTokenAccount, + sellsideEscrowTokenAccount: extraPoolAta, + ownerTokenAccount: ownerExtraNftAtaAddress, + allowlistAuxAccount: SystemProgram.programId, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + sellState: extraNftSellState, + }) + .remainingAccounts([ + { + pubkey: M2_PROGRAM, + isWritable: false, + isSigner: false, + }, + { + pubkey: buyerSharedEscrow, + isWritable: true, + isSigner: false, + }, + ]) + .transaction(); + + const blockhashData = await connection.getLatestBlockhash(); + tx.feePayer = seller.publicKey; + tx.recentBlockhash = blockhashData.blockhash; + tx.partialSign(cosigner, seller); + + await sendAndAssertTx(connection, tx, blockhashData, false); + + const expectedMakerFees = expectedTotalPrice * LAMPORTS_PER_SOL * 0.015; + const expectedReferralFees = expectedMakerFees; + const [ + sellerBalance, + referralBalance, + poolAtaBalance, + afterWalletBalance, + poolEscrowBalance, + afterSharedEscrowBalance, + ] = await Promise.all([ + connection.getBalance(seller.publicKey), + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(extraPoolAta), + connection.getBalance(wallet.publicKey), + connection.getBalance(solEscrowKey), + connection.getBalance(buyerSharedEscrow), + ]); + + assert.equal( + sellerBalance, + initSellerBalance + + expectedTotalPrice * LAMPORTS_PER_SOL - + SIGNATURE_FEE_LAMPORTS * 2, + ); + assert.equal(referralBalance, initReferralBalance + expectedReferralFees); + assert.equal(poolAtaBalance, 0); + assert.equal(poolEscrowBalance, 0); // because it's shared escrow, so the pool escrow is empty + assert.equal(afterWalletBalance, initWalletBalance); + assert.notEqual(afterSharedEscrowBalance, 0); + assert.equal( + initBuyerSharedEscrowBalance - afterSharedEscrowBalance, + expectedTotalPrice * LAMPORTS_PER_SOL + expectedMakerFees, + ); + + const poolAccountInfo = await program.account.pool.fetch( + poolData.poolKey, + ); + assertIsBetween( + poolAccountInfo.spotPrice.toNumber(), + (expectedTotalPrice * LAMPORTS_PER_SOL) / 1.0125, + PRICE_ERROR_RANGE, + ); + // do not reinvest so sell side asset amount should be 0 + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 0); + assert.equal(poolAccountInfo.lpFeeEarned.toNumber(), 0); + }); + + it('Buyside only with shared escrow and close the pool', async () => { + const seller = Keypair.generate(); + const buyerSharedEscrow = getM2BuyerSharedEscrow(wallet.publicKey).key; + await Promise.all([ + airdrop(connection, seller.publicKey, 50), + airdrop(connection, buyerSharedEscrow, 50), + ]); + const { poolData, solEscrowKey, groupAddress } = + await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'buy', // side + { + owner: wallet.publicKey, + cosigner, + curveType: CurveKind.exp, + curveDelta: new anchor.BN(125), // 125 bp + expiry: new anchor.BN(0), + spotPrice: new anchor.BN(0.5 * LAMPORTS_PER_SOL), + referralBp: 200, + reinvestFulfillBuy: false, + reinvestFulfillSell: false, + }, + true, // sharedEscrow + 1, // sharedEscrowCount, only one so pool will be closed + ); + const { + mint: extraMint, + recipientTokenAccount: extraRecipientTokenAccount, + } = await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + seller.publicKey, + groupAddress, + ); + + const ownerExtraNftAtaAddress = await getAssociatedTokenAddress( + extraMint, + wallet.publicKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: extraNftSellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + extraMint, + ); + const extraPoolAta = await getAssociatedTokenAddress( + extraMint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + let [ + initReferralBalance, + initSellerBalance, + initBuyerSharedEscrowBalance, + ] = await Promise.all([ + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(buyerSharedEscrow), + ]); + + const expectedTotalPrice = 0.5; + const tx = await program.methods + .solExtFulfillBuy({ + assetAmount: new anchor.BN(1), + minPaymentAmount: new anchor.BN( + expectedTotalPrice * LAMPORTS_PER_SOL, + ), + allowlistAux: '', + takerFeeBp: 0, + makerFeeBp: 150, + }) + .accountsStrict({ + payer: seller.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount: solEscrowKey, + assetMint: extraMint, + payerAssetAccount: extraRecipientTokenAccount, + sellsideEscrowTokenAccount: extraPoolAta, + ownerTokenAccount: ownerExtraNftAtaAddress, + allowlistAuxAccount: SystemProgram.programId, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + sellState: extraNftSellState, + }) + .remainingAccounts([ + { + pubkey: M2_PROGRAM, + isWritable: false, + isSigner: false, + }, + { + pubkey: buyerSharedEscrow, + isWritable: true, + isSigner: false, + }, + ]) + .transaction(); + + const blockhashData = await connection.getLatestBlockhash(); + tx.feePayer = seller.publicKey; + tx.recentBlockhash = blockhashData.blockhash; + tx.partialSign(cosigner, seller); + + await sendAndAssertTx(connection, tx, blockhashData, false); + + const expectedMakerFees = expectedTotalPrice * LAMPORTS_PER_SOL * 0.015; + const expectedReferralFees = expectedMakerFees; + const [ + sellerBalance, + referralBalance, + poolAtaBalance, + poolEscrowBalance, + afterSharedEscrowBalance, + ] = await Promise.all([ + connection.getBalance(seller.publicKey), + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(extraPoolAta), + connection.getBalance(solEscrowKey), + connection.getBalance(buyerSharedEscrow), + ]); + + assert.equal( + sellerBalance, + initSellerBalance + + expectedTotalPrice * LAMPORTS_PER_SOL - + SIGNATURE_FEE_LAMPORTS * 2, + ); + assert.equal(referralBalance, initReferralBalance + expectedReferralFees); + assert.equal(poolAtaBalance, 0); + assert.equal(poolEscrowBalance, 0); // because it's shared escrow, so the pool escrow is empty + assert.notEqual(afterSharedEscrowBalance, 0); + assert.equal( + initBuyerSharedEscrowBalance - afterSharedEscrowBalance, + expectedTotalPrice * LAMPORTS_PER_SOL + expectedMakerFees, + ); + + const poolAccountInfo = await program.account.pool.fetchNullable( + poolData.poolKey, + ); + assert.isNull(poolAccountInfo); + }); + }); + + describe('ext_fulfillment', () => { + it('Two sides', async () => { + const seller = Keypair.generate(); + await airdrop(connection, seller.publicKey, 50); + const { mint, poolData, poolAta, sellState, solEscrowKey, groupAddress } = + await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'both', // side + { + owner: wallet.publicKey, + cosigner: cosigner, + curveType: CurveKind.exp, + curveDelta: new anchor.BN(300), // 300 basis points + expiry: new anchor.BN(new Date().getTime() / 1000 + 1000), + lpFeeBp: 200, + }, + ); + + const { + mint: extraMint, + recipientTokenAccount: extraRecipientTokenAccount, + } = await createTestMintAndTokenT22VanillaExt( + connection, + wallet.payer, + seller.publicKey, + groupAddress, + ); + + const { key: extraNftSellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + extraMint, + ); + const extraPoolAta = await getAssociatedTokenAddress( + extraMint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + let [initReferralBalance, initSellerBalance, initBuyerBalance] = + await Promise.all([ + connection.getBalance(poolData.referral.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(buyer.publicKey), + ]); + + const expectedLpFees = LAMPORTS_PER_SOL * 0.02; + const expectedTakerFees = LAMPORTS_PER_SOL * 0.01; + const expectedBuyPrices = getSolFulfillBuyPrices({ + totalPriceLamports: LAMPORTS_PER_SOL, + lpFeeBp: 200, + takerFeeBp: 100, + metadataRoyaltyBp: 0, + buysideCreatorRoyaltyBp: 0, + makerFeeBp: 0, + }); + + const tx = await program.methods + .solExtFulfillBuy({ + assetAmount: new anchor.BN(1), + minPaymentAmount: expectedBuyPrices.sellerReceives, + allowlistAux: null, + takerFeeBp: 100, + makerFeeBp: 0, + }) + .accountsStrict({ + payer: seller.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount: solEscrowKey, + assetMint: extraMint, + payerAssetAccount: extraRecipientTokenAccount, + sellsideEscrowTokenAccount: extraPoolAta, + ownerTokenAccount: extraRecipientTokenAccount, + allowlistAuxAccount: SystemProgram.programId, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + sellState: extraNftSellState, + }) + .transaction(); + + const blockhashData = await connection.getLatestBlockhash(); + tx.feePayer = seller.publicKey; + tx.recentBlockhash = blockhashData.blockhash; + tx.partialSign(cosigner, seller); + + await sendAndAssertTx(connection, tx, blockhashData, false); + + let tokenAccountRent = + (await getTokenAccountRent(connection)) + + IMMUTABLE_OWNER_EXTENSION_LAMPORTS; + + const expectedTxFees = SIGNATURE_FEE_LAMPORTS * 2; // cosigner + payer + { + const [ + sellerBalance, + referralBalance, + sellStatePDARent, + poolTokenAccount, + ] = await Promise.all([ + connection.getBalance(seller.publicKey), + connection.getBalance(poolData.referral.publicKey), + getSellStatePDARent(connection), + getTokenAccount2022(connection, extraPoolAta, TOKEN_2022_PROGRAM_ID), + ]); + + assert.equal( + sellerBalance, + initSellerBalance + + expectedBuyPrices.sellerReceives.toNumber() - + expectedTxFees - + sellStatePDARent, // no token account rent bc seller ata was closed and pool ata opened + ); + assert.equal( + referralBalance, + initReferralBalance + expectedBuyPrices.takerFeePaid.toNumber(), + ); + assert.equal(Number(poolTokenAccount.amount), 1); + initReferralBalance = referralBalance; + } + + let poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assertIsBetween( + poolAccountInfo.spotPrice.toNumber(), + LAMPORTS_PER_SOL / 1.03, + PRICE_ERROR_RANGE, + ); + assert.equal( + poolAccountInfo.lpFeeEarned.toNumber(), + expectedBuyPrices.lpFeePaid.toNumber(), + ); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 2); + + const buyerNftAtaAddress = await getAssociatedTokenAddress( + mint, + buyer.publicKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + { + const tx = await program.methods + .solExtFulfillSell({ + assetAmount: new anchor.BN(1), + maxPaymentAmount: new anchor.BN( + LAMPORTS_PER_SOL + expectedTakerFees + expectedLpFees, + ), + buysideCreatorRoyaltyBp: 0, + allowlistAux: '', + takerFeeBp: 100, + makerFeeBp: 100, + }) + .accountsStrict({ + payer: buyer.publicKey, + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount: solEscrowKey, + assetMint: mint, + sellsideEscrowTokenAccount: poolAta, + payerAssetAccount: buyerNftAtaAddress, + sellState: sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .transaction(); + + const blockhashData = await connection.getLatestBlockhash(); + tx.feePayer = buyer.publicKey; + tx.recentBlockhash = blockhashData.blockhash; + tx.partialSign(cosigner, buyer); + + await sendAndAssertTx(connection, tx, blockhashData, false); + } + + { + const expectedMakerFees = LAMPORTS_PER_SOL * 0.01; + const expectedReferralFees = expectedTakerFees + expectedMakerFees; + const [buyerBalance, referralBalance, buyerAta] = await Promise.all([ + connection.getBalance(buyer.publicKey), + connection.getBalance(poolData.referral.publicKey), + getTokenAccount2022( + connection, + buyerNftAtaAddress, + TOKEN_2022_PROGRAM_ID, + ), + ]); + + assertIsBetween( + buyerBalance, + initBuyerBalance - + LAMPORTS_PER_SOL - + expectedLpFees - + expectedTakerFees - + expectedTxFees - + tokenAccountRent, + PRICE_ERROR_RANGE, + ); + assertIsBetween( + referralBalance, + initReferralBalance + expectedReferralFees, + PRICE_ERROR_RANGE, + ); + assert.equal(Number(buyerAta.amount), 1); + } + + poolAccountInfo = await program.account.pool.fetch(poolData.poolKey); + assertIsBetween( + poolAccountInfo.spotPrice.toNumber(), + 1 * LAMPORTS_PER_SOL, + PRICE_ERROR_RANGE, + ); + assertIsBetween( + poolAccountInfo.lpFeeEarned.toNumber(), + expectedBuyPrices.lpFeePaid.toNumber() + 0.02 * LAMPORTS_PER_SOL, + PRICE_ERROR_RANGE, + ); + assert.equal(poolAccountInfo.sellsideAssetAmount.toNumber(), 1); + + const [finalSellerBalance, finalBuyerBalance] = await Promise.all([ + connection.getBalance(seller.publicKey), + connection.getBalance(buyer.publicKey), + ]); + + assert.isAtMost( + finalBuyerBalance + finalSellerBalance, + initBuyerBalance + initSellerBalance, + ); + }); + }); +}); diff --git a/tests/mmm-ext-withdraw.spec.ts b/tests/mmm-ext-withdraw.spec.ts new file mode 100644 index 0000000..3cd6b1b --- /dev/null +++ b/tests/mmm-ext-withdraw.spec.ts @@ -0,0 +1,79 @@ +import * as anchor from '@project-serum/anchor'; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, +} from '@solana/spl-token'; +import { Keypair, SystemProgram } from '@solana/web3.js'; +import { assert } from 'chai'; +import { Mmm, IDL, MMMProgramID } from '../sdk/src'; +import { + airdrop, + createPoolWithExampleT22ExtDeposits, + getTokenAccount2022, +} from './utils'; + +describe('mmm-ext-withdraw', () => { + const { connection } = anchor.AnchorProvider.env(); + const wallet = new anchor.Wallet(Keypair.generate()); + const provider = new anchor.AnchorProvider(connection, wallet, { + commitment: 'processed', + }); + const program = new anchor.Program( + IDL, + MMMProgramID, + provider, + ) as anchor.Program; + const cosigner = Keypair.generate(); + + beforeEach(async () => { + await airdrop(connection, wallet.publicKey, 50); + }); + + it('Withdraw assets', async () => { + const { + mint, + recipientTokenAccount, + poolData, + poolAta, + sellState, + solEscrowKey, + groupAddress, + } = await createPoolWithExampleT22ExtDeposits( + program, + connection, + wallet.payer, + 'sell', + { + owner: wallet.publicKey, + cosigner, + }, + ); + + await program.methods + .extWithdrawSell({ assetAmount: new anchor.BN(1), allowlistAux: null }) + .accountsStrict({ + owner: wallet.publicKey, + cosigner: cosigner.publicKey, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + buysideSolEscrowAccount: solEscrowKey, + allowlistAuxAccount: SystemProgram.programId, + sellState: sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([cosigner]) + .rpc({ skipPreflight: true }); + + const ownerNftAta = await getTokenAccount2022( + connection, + recipientTokenAccount, + TOKEN_2022_PROGRAM_ID, + ); + assert.equal(Number(ownerNftAta.amount), 1); + assert.equal(ownerNftAta.owner.toBase58(), wallet.publicKey.toBase58()); + }); +}); diff --git a/tests/mmm-fulfill-exp.spec.ts b/tests/mmm-fulfill-exp.spec.ts index 028a63d..dcead34 100644 --- a/tests/mmm-fulfill-exp.spec.ts +++ b/tests/mmm-fulfill-exp.spec.ts @@ -1186,7 +1186,7 @@ describe('mmm-fulfill-exp', () => { expectedLpFees - expectedTakerFees - expectedTxFees - - tokenAccountRent, // no token account rent bc seller ata was closed and pool ata opened + tokenAccountRent, PRICE_ERROR_RANGE, ); assertIsBetween( diff --git a/tests/utils/mmm.ts b/tests/utils/mmm.ts index 6803d64..2180d99 100644 --- a/tests/utils/mmm.ts +++ b/tests/utils/mmm.ts @@ -3,11 +3,8 @@ import { PROGRAM_ID as OCP_PROGRAM_ID, } from '@magiceden-oss/open_creator_protocol'; import { - assertAccountExists, - createAmount, generateSigner, OptionOrNullable, - percentAmount, publicKey, some, PublicKey as UmiPublicKey, @@ -16,14 +13,7 @@ import { createSignerFromKeypair, } from '@metaplex-foundation/umi'; import { createUmi } from '@metaplex-foundation/umi-bundle-tests'; -import { - createNft, - createV1, - TokenStandard, - mplTokenMetadata, - fetchDigitalAsset, - verifyCollection, -} from '@metaplex-foundation/mpl-token-metadata'; +import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; import * as anchor from '@project-serum/anchor'; import { Program } from '@project-serum/anchor'; import { @@ -40,6 +30,7 @@ import { getAssociatedTokenAddress, ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, } from '@solana/spl-token'; import { fromWeb3JsPublicKey, @@ -59,15 +50,20 @@ import { Mmm, } from '../../sdk/src'; import { - airdrop, fillAllowlists, getEmptyAllowLists, getKeypair, MIP1_COMPUTE_UNITS, OCP_COMPUTE_UNITS, } from './generic'; -import { createProgrammableNftMip1, createProgrammableNftUmi } from './mip1'; -import { getMetaplexInstance, mintCollection, mintNfts } from './nfts'; +import { createProgrammableNftUmi } from './mip1'; +import { + createTestGroupMintExt, + createTestMintAndTokenT22VanillaExt, + getMetaplexInstance, + mintCollection, + mintNfts, +} from './nfts'; import { umiMintNfts, Nft, umiMintCollection } from './umiNfts'; import { createTestMintAndTokenOCP } from './ocp'; @@ -158,6 +154,121 @@ export const createPool = async ( return { referral, uuid, poolKey }; }; +// create pool for T22 extension +export const createPoolWithExampleT22ExtDeposits = async ( + program: Program, + connection: Connection, + payer: Keypair, + side: 'buy' | 'sell' | 'both' | 'none', + poolArgs: Parameters[1], + sharedEscrow?: boolean, + sharedEscrowCount?: number, +) => { + const { groupAddress } = await createTestGroupMintExt(connection, payer); + const { mint, recipientTokenAccount } = + await createTestMintAndTokenT22VanillaExt( + connection, + payer, + poolArgs.owner, + groupAddress, + ); + + const poolData = await createPool(program, { + allowlists: [ + { + kind: AllowlistKind.metadata, + value: mint, + }, + { + kind: AllowlistKind.group, + value: groupAddress, + }, + ...getEmptyAllowLists(4), + ], + ...poolArgs, + }); + + const poolAta = await getAssociatedTokenAddress( + mint, + poolData.poolKey, + true, + TOKEN_2022_PROGRAM_ID, + ); + + const { key: sellState } = getMMMSellStatePDA( + program.programId, + poolData.poolKey, + mint, + ); + + if (!sharedEscrow && (side === 'both' || side === 'sell')) { + await program.methods + .extDepositSell({ + assetAmount: new anchor.BN(1), + allowlistAux: 'example.com', + }) + .accountsStrict({ + owner: poolArgs.owner, + cosigner: poolArgs.cosigner?.publicKey!, + pool: poolData.poolKey, + assetMint: mint, + assetTokenAccount: recipientTokenAccount, + sellsideEscrowTokenAccount: poolAta, + sellState, + systemProgram: SystemProgram.programId, + tokenProgram: TOKEN_2022_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .signers([poolArgs.cosigner!]) + .rpc({ skipPreflight: true }); + } + + const { key: solEscrowKey } = getMMMBuysideSolEscrowPDA( + program.programId, + poolData.poolKey, + ); + + if (!sharedEscrow && (side === 'both' || side === 'buy')) { + await program.methods + .solDepositBuy({ paymentAmount: new anchor.BN(10 * LAMPORTS_PER_SOL) }) + .accountsStrict({ + owner: poolArgs.owner, + cosigner: poolArgs.cosigner?.publicKey ?? poolArgs.owner, + pool: poolData.poolKey, + buysideSolEscrowAccount: solEscrowKey, + systemProgram: SystemProgram.programId, + }) + .signers([...(poolArgs.cosigner ? [poolArgs.cosigner] : [])]) + .rpc({ skipPreflight: true }); + } + + if (sharedEscrow) { + const sharedEscrowAccount = getM2BuyerSharedEscrow(poolArgs.owner).key; + await program.methods + .setSharedEscrow({ + sharedEscrowCount: new anchor.BN(sharedEscrowCount || 2), + }) + .accountsStrict({ + owner: poolArgs.owner, + cosigner: poolArgs.cosigner?.publicKey ?? poolArgs.owner, + pool: poolData.poolKey, + sharedEscrowAccount, + }) + .signers([...(poolArgs.cosigner ? [poolArgs.cosigner] : [])]) + .rpc(); + } + + return { + mint, + recipientTokenAccount, + poolData, + poolAta, + sellState, + solEscrowKey, + groupAddress, + }; +}; + export const createPoolWithExampleDeposits = async ( program: Program, connection: Connection, diff --git a/tests/utils/nfts.ts b/tests/utils/nfts.ts index 9a12902..dc2c668 100644 --- a/tests/utils/nfts.ts +++ b/tests/utils/nfts.ts @@ -5,8 +5,30 @@ import { PublicKey, token as getSplTokenAmount, } from '@metaplex-foundation/js'; -import { Connection } from '@solana/web3.js'; -import { getKeypair } from './generic'; +import { + Connection, + Keypair, + SystemProgram, + Transaction, +} from '@solana/web3.js'; +import { getKeypair, sendAndAssertTx } from './generic'; +import { + ExtensionType, + TOKEN_2022_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + createInitializeInstruction, + createInitializeGroupMemberPointerInstruction, + createInitializeMetadataPointerInstruction, + createInitializeMint2Instruction, + createMintToInstruction, + getAssociatedTokenAddressSync, + getMintLen, + createInitializeGroupPointerInstruction, +} from '@solana/spl-token'; +import { + createInitializeGroupInstruction, + createInitializeMemberInstruction, +} from '@solana/spl-token-group'; export const getMetaplexInstance = (conn: Connection) => { return Metaplex.make(conn).use(keypairIdentity(getKeypair())); @@ -109,3 +131,238 @@ export const mintCollection = async ( return { collection: collectionNft, members: collectionMembers }; }; + +export async function createTestMintAndTokenT22VanillaExt( + connection: Connection, + payer: Keypair, + recipient?: PublicKey, + groupAddress?: PublicKey, + groupMemberAddress?: PublicKey, +) { + const mintKeypair = Keypair.generate(); + const effectiveGroupAddress = groupAddress ?? Keypair.generate().publicKey; + const tokenProgramId = TOKEN_2022_PROGRAM_ID; + const effectiveRecipient = recipient ?? payer.publicKey; + const targetTokenAccount = getAssociatedTokenAddressSync( + mintKeypair.publicKey, + effectiveRecipient, + true, + tokenProgramId, + ); + + const mintSpace = getMintLen([ + ExtensionType.MetadataPointer, + ExtensionType.GroupMemberPointer, + ]); + const mintLamports = await connection.getMinimumBalanceForRentExemption( + mintSpace * 2, + ); + + const createMintAccountIx = SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: mintKeypair.publicKey, + space: mintSpace, + lamports: mintLamports, + programId: tokenProgramId, + }); + const createMetadataPointerIx = createInitializeMetadataPointerInstruction( + mintKeypair.publicKey, + payer.publicKey, + mintKeypair.publicKey, + tokenProgramId, + ); + + const memberAddress = groupMemberAddress ?? mintKeypair.publicKey; + const createGroupMemberPointerIx = + createInitializeGroupMemberPointerInstruction( + mintKeypair.publicKey, + payer.publicKey, + memberAddress, + tokenProgramId, + ); + const createInitMintIx = createInitializeMint2Instruction( + mintKeypair.publicKey, + 0, + payer.publicKey, + payer.publicKey, + tokenProgramId, + ); + + const createMetadataIx = createInitializeInstruction({ + metadata: mintKeypair.publicKey, + updateAuthority: payer.publicKey, + mint: mintKeypair.publicKey, + mintAuthority: payer.publicKey, + name: 'xyzname', + symbol: 'xyz', + uri: 'example.com', + programId: tokenProgramId, + }); + + const createGroupMemberIx = createInitializeMemberInstruction({ + programId: tokenProgramId, + member: mintKeypair.publicKey, + memberMint: mintKeypair.publicKey, + memberMintAuthority: payer.publicKey, + group: effectiveGroupAddress, + groupUpdateAuthority: payer.publicKey, + }); + + const createAtaIx = createAssociatedTokenAccountInstruction( + payer.publicKey, + targetTokenAccount, + effectiveRecipient, + mintKeypair.publicKey, + tokenProgramId, + ); + + const mintToIx = createMintToInstruction( + mintKeypair.publicKey, + targetTokenAccount, + payer.publicKey, + 1, // amount + [], + tokenProgramId, + ); + + const blockhashData = await connection.getLatestBlockhash(); + const tx = new Transaction().add( + createMintAccountIx, + createGroupMemberPointerIx, + createMetadataPointerIx, + createInitMintIx, + createMetadataIx, + createGroupMemberIx, + createAtaIx, + mintToIx, + ); + tx.recentBlockhash = blockhashData.blockhash; + tx.feePayer = payer.publicKey; + tx.partialSign(payer, mintKeypair); + await sendAndAssertTx(connection, tx, blockhashData, false); + + return { + mint: mintKeypair.publicKey, + recipientTokenAccount: targetTokenAccount, + }; +} + +export async function createTestGroupMintExt( + connection: Connection, + payer: Keypair, +) { + const tokenProgramId = TOKEN_2022_PROGRAM_ID; + const groupKeyPair = Keypair.generate(); + const mintSpace = getMintLen([ExtensionType.GroupPointer]); + const mintLamports = await connection.getMinimumBalanceForRentExemption( + mintSpace * 2, + ); + + const createMintAccountIx = SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: groupKeyPair.publicKey, + space: mintSpace, + lamports: mintLamports, + programId: tokenProgramId, + }); + const createGroupPointerIx = createInitializeGroupPointerInstruction( + groupKeyPair.publicKey, + payer.publicKey, + groupKeyPair.publicKey, + tokenProgramId, + ); + + const createInitMintIx = createInitializeMint2Instruction( + groupKeyPair.publicKey, + 0, + payer.publicKey, + payer.publicKey, + tokenProgramId, + ); + + const createGroupIx = createInitializeGroupInstruction({ + programId: tokenProgramId, + group: groupKeyPair.publicKey, + mint: groupKeyPair.publicKey, + mintAuthority: payer.publicKey, + updateAuthority: payer.publicKey, + maxSize: 10, + }); + + const blockhashData = await connection.getLatestBlockhash(); + const tx = new Transaction().add( + createMintAccountIx, + createGroupPointerIx, + createInitMintIx, + createGroupIx, + ); + tx.recentBlockhash = blockhashData.blockhash; + tx.feePayer = payer.publicKey; + tx.partialSign(payer, groupKeyPair); + await sendAndAssertTx(connection, tx, blockhashData, false); + + return { + groupAddress: groupKeyPair.publicKey, + }; +} + +export async function createTestGroupMemberMint( + connection: Connection, + payer: Keypair, + groupAddress: PublicKey, +) { + const tokenProgramId = TOKEN_2022_PROGRAM_ID; + const groupMemberKeyPair = Keypair.generate(); + const mintSpace = getMintLen([ExtensionType.GroupPointer]); + const mintLamports = await connection.getMinimumBalanceForRentExemption( + mintSpace * 2, + ); + + const createMintAccountIx = SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: groupMemberKeyPair.publicKey, + space: mintSpace, + lamports: mintLamports, + programId: tokenProgramId, + }); + const createGroupMemberPointerIx = + createInitializeGroupMemberPointerInstruction( + groupMemberKeyPair.publicKey, + payer.publicKey, + groupMemberKeyPair.publicKey, + tokenProgramId, + ); + + const createInitMintIx = createInitializeMint2Instruction( + groupMemberKeyPair.publicKey, + 0, + payer.publicKey, + payer.publicKey, + tokenProgramId, + ); + + const createGroupMemberIx = createInitializeMemberInstruction({ + programId: tokenProgramId, + member: groupMemberKeyPair.publicKey, + memberMint: groupMemberKeyPair.publicKey, + memberMintAuthority: payer.publicKey, + group: groupAddress, + groupUpdateAuthority: payer.publicKey, + }); + + const blockhashData = await connection.getLatestBlockhash(); + const tx = new Transaction().add( + createMintAccountIx, + createGroupMemberPointerIx, + createInitMintIx, + createGroupMemberIx, + ); + tx.recentBlockhash = blockhashData.blockhash; + tx.feePayer = payer.publicKey; + tx.partialSign(payer, groupMemberKeyPair); + await sendAndAssertTx(connection, tx, blockhashData, false); + + return { + groupMemberKeyPair, + }; +}