From eb000db66feb258df1206ba257c30a9c54c8fcfc Mon Sep 17 00:00:00 2001 From: Samuel Vanderwaal Date: Wed, 12 Jul 2023 13:21:34 -0800 Subject: [PATCH] fix next edition calculation for burned items (#1126) * calculate next edition from edition marker * change error message to be more descriptive * regen api; fix JS tests * refactor logic for multiple PDAs * fix logic * clean up; add multiple marker pda test * ignore really long test for CI * new extra and check * rejen JS api * add buy_v2 * reduce buy_v2 test edition number for ci * regenerate JS api * refactor to remove redundant code * fix buy_v2 test * clean up tests * fix bug in loop * add tests for minting from multiple markers --- fixed-price-sale/js/idl/fixed_price_sale.json | 418 ++++++++- fixed-price-sale/js/run-test.sh | 40 + .../js/src/generated/errors/index.ts | 43 + .../js/src/generated/instructions/buyV2.ts | 224 +++++ .../js/src/generated/instructions/index.ts | 1 + fixed-price-sale/js/test/actions/mintNft.ts | 6 +- fixed-price-sale/program/src/error.rs | 6 + fixed-price-sale/program/src/lib.rs | 69 +- fixed-price-sale/program/src/processor/buy.rs | 104 ++- fixed-price-sale/program/src/utils.rs | 36 + fixed-price-sale/program/tests/buy.rs | 825 ++++++++++++++++- fixed-price-sale/program/tests/buy_v2.rs | 838 ++++++++++++++++++ .../program/tests/change_market.rs | 3 + .../program/tests/claim_resource.rs | 3 + .../program/tests/close_market.rs | 2 + .../program/tests/create_market.rs | 10 + .../program/tests/resume_market.rs | 4 + .../program/tests/suspend_market.rs | 5 + .../program/tests/utils/helpers.rs | 398 ++++++++- .../program/tests/utils/setup_functions.rs | 14 +- fixed-price-sale/program/tests/withdraw.rs | 5 + 21 files changed, 3005 insertions(+), 49 deletions(-) create mode 100755 fixed-price-sale/js/run-test.sh create mode 100644 fixed-price-sale/js/src/generated/instructions/buyV2.ts create mode 100644 fixed-price-sale/program/tests/buy_v2.rs diff --git a/fixed-price-sale/js/idl/fixed_price_sale.json b/fixed-price-sale/js/idl/fixed_price_sale.json index 9fef50e12e..6e6f859d4c 100644 --- a/fixed-price-sale/js/idl/fixed_price_sale.json +++ b/fixed-price-sale/js/idl/fixed_price_sale.json @@ -8,7 +8,10 @@ { "name": "store", "isMut": false, - "isSigner": false + "isSigner": false, + "relations": [ + "admin" + ] }, { "name": "admin", @@ -43,12 +46,36 @@ { "name": "vault", "isMut": true, - "isSigner": false + "isSigner": false, + "relations": [ + "owner" + ] }, { "name": "owner", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "mt_vault" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Mint", + "path": "resource_mint" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Store", + "path": "store" + } + ] + } }, { "name": "resourceToken", @@ -124,7 +151,11 @@ { "name": "market", "isMut": true, - "isSigner": false + "isSigner": false, + "relations": [ + "treasury_holder", + "selling_resource" + ] }, { "name": "sellingResource", @@ -144,7 +175,27 @@ { "name": "tradeHistory", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "history" + }, + { + "kind": "account", + "type": "publicKey", + "path": "user_wallet" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Market", + "path": "market" + } + ] + } }, { "name": "treasuryHolder", @@ -179,13 +230,200 @@ { "name": "vault", "isMut": true, - "isSigner": false + "isSigner": false, + "relations": [ + "owner" + ] }, { "name": "owner", "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "mt_vault" + }, + { + "kind": "account", + "type": "publicKey", + "account": "SellingResource", + "path": "selling_resource.resource" + }, + { + "kind": "account", + "type": "publicKey", + "account": "SellingResource", + "path": "selling_resource.store" + } + ] + } + }, + { + "name": "newTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "masterEditionMetadata", + "isMut": true, + "isSigner": false + }, + { + "name": "clock", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenMetadataProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "tradeHistoryBump", + "type": "u8" + }, + { + "name": "vaultOwnerBump", + "type": "u8" + } + ] + }, + { + "name": "buyV2", + "accounts": [ + { + "name": "market", + "isMut": true, + "isSigner": false, + "relations": [ + "treasury_holder", + "selling_resource" + ] + }, + { + "name": "sellingResource", + "isMut": true, + "isSigner": false + }, + { + "name": "userTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userWallet", + "isMut": true, + "isSigner": true + }, + { + "name": "tradeHistory", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "history" + }, + { + "kind": "account", + "type": "publicKey", + "path": "user_wallet" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Market", + "path": "market" + } + ] + } + }, + { + "name": "treasuryHolder", + "isMut": true, + "isSigner": false + }, + { + "name": "newMetadata", + "isMut": true, "isSigner": false }, + { + "name": "newEdition", + "isMut": true, + "isSigner": false + }, + { + "name": "masterEdition", + "isMut": true, + "isSigner": false + }, + { + "name": "newMint", + "isMut": true, + "isSigner": false + }, + { + "name": "editionMarker", + "isMut": true, + "isSigner": false + }, + { + "name": "vault", + "isMut": true, + "isSigner": false, + "relations": [ + "owner" + ] + }, + { + "name": "owner", + "isMut": false, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "mt_vault" + }, + { + "kind": "account", + "type": "publicKey", + "account": "SellingResource", + "path": "selling_resource.resource" + }, + { + "kind": "account", + "type": "publicKey", + "account": "SellingResource", + "path": "selling_resource.store" + } + ] + } + }, { "name": "newTokenAccount", "isMut": true, @@ -230,6 +468,10 @@ { "name": "vaultOwnerBump", "type": "u8" + }, + { + "name": "editionMarkerNumber", + "type": "u64" } ] }, @@ -239,7 +481,10 @@ { "name": "market", "isMut": true, - "isSigner": false + "isSigner": false, + "relations": [ + "owner" + ] }, { "name": "owner", @@ -260,7 +505,10 @@ { "name": "market", "isMut": true, - "isSigner": false + "isSigner": false, + "relations": [ + "owner" + ] }, { "name": "owner", @@ -281,7 +529,10 @@ { "name": "market", "isMut": true, - "isSigner": false + "isSigner": false, + "relations": [ + "owner" + ] }, { "name": "owner", @@ -333,7 +584,10 @@ { "name": "market", "isMut": true, - "isSigner": false + "isSigner": false, + "relations": [ + "owner" + ] }, { "name": "owner", @@ -354,7 +608,12 @@ { "name": "market", "isMut": false, - "isSigner": false + "isSigner": false, + "relations": [ + "treasury_holder", + "selling_resource", + "treasury_mint" + ] }, { "name": "sellingResource", @@ -379,7 +638,28 @@ { "name": "owner", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "holder" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Market", + "path": "market.treasury_mint" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Market", + "path": "market.selling_resource" + } + ] + } }, { "name": "destination", @@ -399,7 +679,27 @@ { "name": "payoutTicket", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "payout_ticket" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Market", + "path": "market" + }, + { + "kind": "account", + "type": "publicKey", + "path": "funder" + } + ] + } }, { "name": "rent", @@ -459,7 +759,10 @@ { "name": "sellingResource", "isMut": true, - "isSigner": false + "isSigner": false, + "relations": [ + "store" + ] }, { "name": "mint", @@ -474,7 +777,27 @@ { "name": "owner", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "holder" + }, + { + "kind": "account", + "type": "publicKey", + "path": "mint" + }, + { + "kind": "account", + "type": "publicKey", + "account": "SellingResource", + "path": "selling_resource" + } + ] + } }, { "name": "systemProgram", @@ -535,7 +858,11 @@ { "name": "market", "isMut": false, - "isSigner": false + "isSigner": false, + "relations": [ + "selling_resource", + "treasury_holder" + ] }, { "name": "treasuryHolder", @@ -545,7 +872,10 @@ { "name": "sellingResource", "isMut": false, - "isSigner": false + "isSigner": false, + "relations": [ + "vault" + ] }, { "name": "sellingResourceOwner", @@ -555,7 +885,10 @@ { "name": "vault", "isMut": true, - "isSigner": false + "isSigner": false, + "relations": [ + "owner" + ] }, { "name": "metadata", @@ -565,7 +898,28 @@ { "name": "owner", "isMut": false, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "mt_vault" + }, + { + "kind": "account", + "type": "publicKey", + "account": "SellingResource", + "path": "selling_resource.resource" + }, + { + "kind": "account", + "type": "publicKey", + "account": "SellingResource", + "path": "selling_resource.store" + } + ] + } }, { "name": "destination", @@ -616,7 +970,21 @@ { "name": "primaryMetadataCreators", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "primary_creators" + }, + { + "kind": "account", + "type": "publicKey", + "path": "metadata" + } + ] + } }, { "name": "systemProgram", @@ -1149,6 +1517,16 @@ "code": 6043, "name": "WrongGatingToken", "msg": "Wrong gating token" + }, + { + "code": 6044, + "name": "EditionMarkerFull", + "msg": "No available editions in edition marker" + }, + { + "code": 6045, + "name": "InvalidEditionMarkerAccount", + "msg": "Invalid edition marker" } ], "metadata": { diff --git a/fixed-price-sale/js/run-test.sh b/fixed-price-sale/js/run-test.sh new file mode 100755 index 0000000000..4d16e9a71e --- /dev/null +++ b/fixed-price-sale/js/run-test.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# +# To run this script, you need: +# - npm install -g esbuild-runner +# - npm install -g tap-spec + +# error output colour +RED() { echo $'\e[1;31m'$1$'\e[0m'; } +RUN_ALL=0 + +# check whether we are running all test files or not + +while getopts a-: optchar; do + case "${optchar}" in + a) + RUN_ALL=1 ;; + -) + case "${OPTARG}" in + all) RUN_ALL=1 ;; + *) ;; + esac ;; + *) ;; + esac +done + +# runs single or multiple tests + +if [ $RUN_ALL -eq 1 ]; then + for file in `ls test/*.test.ts` + do + esr $file | tap-spec + done +else + if [ ! -z "$1" ] && [[ -f "$1" ]]; then + esr $1 | tap-spec + else + echo "$(RED "Error: ")Please specify a test file or [-a | --all] to run all tests" + exit 1 + fi +fi diff --git a/fixed-price-sale/js/src/generated/errors/index.ts b/fixed-price-sale/js/src/generated/errors/index.ts index 912a6292ce..5cda03a893 100644 --- a/fixed-price-sale/js/src/generated/errors/index.ts +++ b/fixed-price-sale/js/src/generated/errors/index.ts @@ -930,6 +930,49 @@ export class WrongGatingTokenError extends Error { createErrorFromCodeLookup.set(0x179b, () => new WrongGatingTokenError()); createErrorFromNameLookup.set('WrongGatingToken', () => new WrongGatingTokenError()); +/** + * EditionMarkerFull: 'No available editions in edition marker' + * + * @category Errors + * @category generated + */ +export class EditionMarkerFullError extends Error { + readonly code: number = 0x179c; + readonly name: string = 'EditionMarkerFull'; + constructor() { + super('No available editions in edition marker'); + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, EditionMarkerFullError); + } + } +} + +createErrorFromCodeLookup.set(0x179c, () => new EditionMarkerFullError()); +createErrorFromNameLookup.set('EditionMarkerFull', () => new EditionMarkerFullError()); + +/** + * InvalidEditionMarkerAccount: 'Invalid edition marker' + * + * @category Errors + * @category generated + */ +export class InvalidEditionMarkerAccountError extends Error { + readonly code: number = 0x179d; + readonly name: string = 'InvalidEditionMarkerAccount'; + constructor() { + super('Invalid edition marker'); + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, InvalidEditionMarkerAccountError); + } + } +} + +createErrorFromCodeLookup.set(0x179d, () => new InvalidEditionMarkerAccountError()); +createErrorFromNameLookup.set( + 'InvalidEditionMarkerAccount', + () => new InvalidEditionMarkerAccountError(), +); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/fixed-price-sale/js/src/generated/instructions/buyV2.ts b/fixed-price-sale/js/src/generated/instructions/buyV2.ts new file mode 100644 index 0000000000..9840e31f96 --- /dev/null +++ b/fixed-price-sale/js/src/generated/instructions/buyV2.ts @@ -0,0 +1,224 @@ +/** + * This code was GENERATED using the solita package. + * Please DO NOT EDIT THIS FILE, instead rerun solita to update it or write a wrapper to add functionality. + * + * See: https://github.com/metaplex-foundation/solita + */ + +import * as splToken from '@solana/spl-token'; +import * as beet from '@metaplex-foundation/beet'; +import * as web3 from '@solana/web3.js'; + +/** + * @category Instructions + * @category BuyV2 + * @category generated + */ +export type BuyV2InstructionArgs = { + tradeHistoryBump: number; + vaultOwnerBump: number; + editionMarkerNumber: beet.bignum; +}; +/** + * @category Instructions + * @category BuyV2 + * @category generated + */ +export const buyV2Struct = new beet.BeetArgsStruct< + BuyV2InstructionArgs & { + instructionDiscriminator: number[] /* size: 8 */; + } +>( + [ + ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], + ['tradeHistoryBump', beet.u8], + ['vaultOwnerBump', beet.u8], + ['editionMarkerNumber', beet.u64], + ], + 'BuyV2InstructionArgs', +); +/** + * Accounts required by the _buyV2_ instruction + * + * @property [_writable_] market + * @property [_writable_] sellingResource + * @property [_writable_] userTokenAccount + * @property [_writable_, **signer**] userWallet + * @property [_writable_] tradeHistory + * @property [_writable_] treasuryHolder + * @property [_writable_] newMetadata + * @property [_writable_] newEdition + * @property [_writable_] masterEdition + * @property [_writable_] newMint + * @property [_writable_] editionMarker + * @property [_writable_] vault + * @property [] owner + * @property [_writable_] newTokenAccount + * @property [_writable_] masterEditionMetadata + * @property [] clock + * @property [] tokenMetadataProgram + * @category Instructions + * @category BuyV2 + * @category generated + */ +export type BuyV2InstructionAccounts = { + market: web3.PublicKey; + sellingResource: web3.PublicKey; + userTokenAccount: web3.PublicKey; + userWallet: web3.PublicKey; + tradeHistory: web3.PublicKey; + treasuryHolder: web3.PublicKey; + newMetadata: web3.PublicKey; + newEdition: web3.PublicKey; + masterEdition: web3.PublicKey; + newMint: web3.PublicKey; + editionMarker: web3.PublicKey; + vault: web3.PublicKey; + owner: web3.PublicKey; + newTokenAccount: web3.PublicKey; + masterEditionMetadata: web3.PublicKey; + clock: web3.PublicKey; + rent?: web3.PublicKey; + tokenMetadataProgram: web3.PublicKey; + tokenProgram?: web3.PublicKey; + systemProgram?: web3.PublicKey; + anchorRemainingAccounts?: web3.AccountMeta[]; +}; + +export const buyV2InstructionDiscriminator = [184, 23, 238, 97, 103, 197, 211, 61]; + +/** + * Creates a _BuyV2_ instruction. + * + * @param accounts that will be accessed while the instruction is processed + * @param args to provide as instruction data to the program + * + * @category Instructions + * @category BuyV2 + * @category generated + */ +export function createBuyV2Instruction( + accounts: BuyV2InstructionAccounts, + args: BuyV2InstructionArgs, + programId = new web3.PublicKey('SaLeTjyUa5wXHnGuewUSyJ5JWZaHwz3TxqUntCE9czo'), +) { + const [data] = buyV2Struct.serialize({ + instructionDiscriminator: buyV2InstructionDiscriminator, + ...args, + }); + const keys: web3.AccountMeta[] = [ + { + pubkey: accounts.market, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.sellingResource, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.userTokenAccount, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.userWallet, + isWritable: true, + isSigner: true, + }, + { + pubkey: accounts.tradeHistory, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.treasuryHolder, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.newMetadata, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.newEdition, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.masterEdition, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.newMint, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.editionMarker, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.vault, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.owner, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.newTokenAccount, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.masterEditionMetadata, + isWritable: true, + isSigner: false, + }, + { + pubkey: accounts.clock, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.rent ?? web3.SYSVAR_RENT_PUBKEY, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.tokenMetadataProgram, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.tokenProgram ?? splToken.TOKEN_PROGRAM_ID, + isWritable: false, + isSigner: false, + }, + { + pubkey: accounts.systemProgram ?? web3.SystemProgram.programId, + isWritable: false, + isSigner: false, + }, + ]; + + if (accounts.anchorRemainingAccounts != null) { + for (const acc of accounts.anchorRemainingAccounts) { + keys.push(acc); + } + } + + const ix = new web3.TransactionInstruction({ + programId, + keys, + data, + }); + return ix; +} diff --git a/fixed-price-sale/js/src/generated/instructions/index.ts b/fixed-price-sale/js/src/generated/instructions/index.ts index 10bbd61ad3..ae6346fa18 100644 --- a/fixed-price-sale/js/src/generated/instructions/index.ts +++ b/fixed-price-sale/js/src/generated/instructions/index.ts @@ -1,4 +1,5 @@ export * from './buy'; +export * from './buyV2'; export * from './changeMarket'; export * from './claimResource'; export * from './closeMarket'; diff --git a/fixed-price-sale/js/test/actions/mintNft.ts b/fixed-price-sale/js/test/actions/mintNft.ts index d4d5e78862..c244a4f7ac 100644 --- a/fixed-price-sale/js/test/actions/mintNft.ts +++ b/fixed-price-sale/js/test/actions/mintNft.ts @@ -4,7 +4,7 @@ import { createCreateMasterEditionV3Instruction, Creator, DataV2, - createCreateMetadataAccountV2Instruction, + createCreateMetadataAccountV3Instruction, } from '@metaplex-foundation/mpl-token-metadata'; import { createMintToInstruction } from '@solana/spl-token'; import { strict as assert } from 'assert'; @@ -66,7 +66,7 @@ export async function mintNFT({ const pdas = metaplex.nfts().pdas(); const metadata = pdas.metadata({ mint: mint.publicKey }); - const createMetadataInstruction = createCreateMetadataAccountV2Instruction( + const createMetadataInstruction = createCreateMetadataAccountV3Instruction( { metadata, mint: mint.publicKey, @@ -74,7 +74,7 @@ export async function mintNFT({ mintAuthority: payer.publicKey, payer: payer.publicKey, }, - { createMetadataAccountArgsV2: { isMutable: true, data } }, + { createMetadataAccountArgsV3: { isMutable: true, data, collectionDetails: null } }, ); createTokenTx.add(createMetadataInstruction); diff --git a/fixed-price-sale/program/src/error.rs b/fixed-price-sale/program/src/error.rs index 0b2f871e5a..768f141707 100644 --- a/fixed-price-sale/program/src/error.rs +++ b/fixed-price-sale/program/src/error.rs @@ -136,4 +136,10 @@ pub enum ErrorCode { // 6043 #[msg("Wrong gating token")] WrongGatingToken, + // 6044 + #[msg("No available editions in edition marker")] + EditionMarkerFull, + // 6045 + #[msg("Invalid edition marker")] + InvalidEditionMarkerAccount, } diff --git a/fixed-price-sale/program/src/lib.rs b/fixed-price-sale/program/src/lib.rs index e725eec348..9278c308c4 100644 --- a/fixed-price-sale/program/src/lib.rs +++ b/fixed-price-sale/program/src/lib.rs @@ -47,10 +47,20 @@ pub mod fixed_price_sale { ctx: Context<'_, '_, '_, 'info, Buy<'info>>, _trade_history_bump: u8, vault_owner_bump: u8, + ) -> Result<()> { + ctx.accounts + .process(vault_owner_bump, None, ctx.remaining_accounts) + } + + pub fn buy_v2<'info>( + ctx: Context<'_, '_, '_, 'info, Buy<'info>>, + _trade_history_bump: u8, + vault_owner_bump: u8, + edition_marker_number: u64, ) -> Result<()> { ctx.accounts.process( - _trade_history_bump, vault_owner_bump, + Some(edition_marker_number), ctx.remaining_accounts, ) } @@ -264,6 +274,63 @@ pub struct Buy<'info> { // metadata_account: UncheckedAccount<'info> } +#[derive(Accounts)] +#[instruction(trade_history:u8, vault_owner_bump: u8)] +pub struct BuyV2<'info> { + #[account(mut, has_one=treasury_holder, has_one=selling_resource)] + market: Box>, + #[account(mut)] + selling_resource: Box>, + #[account(mut)] + /// CHECK: checked in program + user_token_account: UncheckedAccount<'info>, + #[account(mut)] + user_wallet: Signer<'info>, + #[account(init_if_needed, seeds=[HISTORY_PREFIX.as_bytes(), user_wallet.key().as_ref(), market.key().as_ref()], bump, payer=user_wallet, space=TradeHistory::LEN)] + trade_history: Box>, + #[account(mut)] + /// CHECK: checked in program + treasury_holder: UncheckedAccount<'info>, + // Will be created by `mpl_token_metadata` + #[account(mut)] + /// CHECK: checked in program + new_metadata: UncheckedAccount<'info>, + // Will be created by `mpl_token_metadata` + #[account(mut)] + /// CHECK: checked in program + new_edition: UncheckedAccount<'info>, + #[account(mut, owner=mpl_token_metadata::id())] + /// CHECK: checked in program + master_edition: UncheckedAccount<'info>, + #[account(mut)] + new_mint: Box>, + // Will be created by `mpl_token_metadata` + #[account(mut)] + /// CHECK: checked in program + edition_marker: UncheckedAccount<'info>, + #[account(mut, has_one=owner)] + vault: Box>, + #[account(seeds=[VAULT_OWNER_PREFIX.as_bytes(), selling_resource.resource.as_ref(), selling_resource.store.as_ref()], bump=vault_owner_bump)] + /// CHECK: checked in program + owner: UncheckedAccount<'info>, + #[account(mut, constraint = new_token_account.owner == user_wallet.key())] + new_token_account: Box>, + #[account(mut, owner=mpl_token_metadata::id())] + /// CHECK: checked in program + master_edition_metadata: UncheckedAccount<'info>, + clock: Sysvar<'info, Clock>, + rent: Sysvar<'info, Rent>, + /// CHECK: checked in program + token_metadata_program: UncheckedAccount<'info>, + token_program: Program<'info, Token>, + system_program: Program<'info, System>, + // if gatekeeper set for the collection these accounts also should be passed + // IMPORTANT: accounts should be passed strictly in this order + // user_collection_token_account: Account<'info, TokenAccount> + // token_account_mint: Account<'info, Mint> + // metadata_account: UncheckedAccount<'info> +} + #[derive(Accounts)] #[instruction(treasury_owner_bump: u8, payout_ticket_bump: u8)] pub struct Withdraw<'info> { diff --git a/fixed-price-sale/program/src/processor/buy.rs b/fixed-price-sale/program/src/processor/buy.rs index 5c4e22c559..3213c0f08e 100644 --- a/fixed-price-sale/program/src/processor/buy.rs +++ b/fixed-price-sale/program/src/processor/buy.rs @@ -12,16 +12,13 @@ use anchor_lang::{ system_program::System, }; use anchor_spl::token; -use mpl_token_metadata::{ - state::{Metadata, TokenMetadataAccount}, - utils::get_supply_off_master_edition, -}; +use mpl_token_metadata::state::{EditionMarker, Metadata, TokenMetadataAccount}; impl<'info> Buy<'info> { pub fn process( &mut self, - _trade_history_bump: u8, vault_owner_bump: u8, + edition_marker_number: Option, remaining_accounts: &[AccountInfo<'info>], ) -> Result<()> { let market = &mut self.market; @@ -45,10 +42,97 @@ impl<'info> Buy<'info> { let system_program = &self.system_program; let metadata_mint = selling_resource.resource; - // do supply +1 to increase master edition supply - let edition = get_supply_off_master_edition(&master_edition.to_account_info())? - .checked_add(1) - .ok_or(ErrorCode::MathOverflow)?; + + // First we find the edition marker number. If it's passed in by the user, we use that and check that + // it matches the edition marker PDA passed in. + + // Otherwise we start at 0 and loop until we find the right edition marker. + + let edition_marker_number = if let Some(edition_marker_number) = edition_marker_number { + let edition_marker_number_str = edition_marker_number.to_string(); + + let pda = Pubkey::find_program_address( + &[ + "metadata".as_bytes(), + mpl_token_metadata::ID.as_ref(), + metadata_mint.as_ref(), + "edition".as_bytes(), + edition_marker_number_str.as_bytes(), + ], + &mpl_token_metadata::ID, + ) + .0; + + if pda != *edition_marker_info.key { + return Err(ErrorCode::InvalidEditionMarkerAccount.into()); + } + + edition_marker_number + } else { + let mut edition_marker_number = 0u64; + + loop { + let edition_marker_number_str = edition_marker_number.to_string(); + let pda = Pubkey::find_program_address( + &[ + "metadata".as_bytes(), + mpl_token_metadata::ID.as_ref(), + metadata_mint.as_ref(), + "edition".as_bytes(), + edition_marker_number_str.as_bytes(), + ], + &mpl_token_metadata::ID, + ) + .0; + + if pda == *edition_marker_info.key { + break; + } + + edition_marker_number = edition_marker_number + .checked_add(1) + .ok_or(ErrorCode::MathOverflow)?; + } + + edition_marker_number + }; + + // Now we calculate the edition number to be minted by finding the first available edition + // in order. + + let is_first_marker = edition_marker_number == 0; + + // Find the first available edition number in this edition marker. + let edition = if edition_marker_info.data_is_empty() { + // First Edition marker skips the first bit because editions start at 1. + if is_first_marker { + 1 + } else { + 248u64 + .checked_mul(edition_marker_number) + .ok_or(ErrorCode::MathOverflow)? + } + } else { + let marker = EditionMarker::from_account_info(edition_marker_info)?; + + if let Some((index, bit)) = find_first_zero_bit(marker.ledger, is_first_marker) { + // 248 * edition_marker_number + (index * 8 + bit as usize) as u64 + + let relative_index = index + .checked_mul(8) + .ok_or(ErrorCode::MathOverflow)? + .checked_add(bit as usize) + .ok_or(ErrorCode::MathOverflow)? as u64; + + 248u64 + .checked_mul(edition_marker_number) + .ok_or(ErrorCode::MathOverflow)? + .checked_add(relative_index) + .ok_or(ErrorCode::MathOverflow)? + } else { + return Err(ErrorCode::EditionMarkerFull.into()); + } + }; // Check, that `Market` is not in `Suspended` state if market.state == MarketState::Suspended { @@ -141,7 +225,7 @@ impl<'info> Buy<'info> { &vault.to_account_info(), &master_edition_metadata.to_account_info(), &master_edition.to_account_info(), - &metadata_mint, + &selling_resource.resource, edition_marker_info, &token_program.to_account_info(), &system_program.to_account_info(), diff --git a/fixed-price-sale/program/src/utils.rs b/fixed-price-sale/program/src/utils.rs index c0ec13c794..c82d3dbd50 100644 --- a/fixed-price-sale/program/src/utils.rs +++ b/fixed-price-sale/program/src/utils.rs @@ -5,6 +5,7 @@ use anchor_lang::{ prelude::*, solana_program::{program::invoke_signed, system_instruction}, }; +use mpl_token_metadata::state::EDITION_MARKER_BIT_SIZE; pub const NAME_MAX_LEN: usize = 40; // max len of a string buffer in bytes pub const NAME_DEFAULT_SIZE: usize = 4 + NAME_MAX_LEN; // max lenght of serialized string (str_len + ) @@ -288,3 +289,38 @@ pub fn calculate_secondary_shares_for_market_owner( ) .ok_or(ErrorCode::MathOverflow)?) } + +pub(crate) fn find_first_zero_bit(arr: [u8; 31], first_marker: bool) -> Option<(usize, u8)> { + // First edition marker starts at 1 so first bit is zero and needs to be skipped. + + for (i, &byte) in arr.iter().enumerate() { + if byte != 0xff { + // There's at least one zero bit in this byte + for bit in (0..8).rev() { + if (byte & (1 << bit)) == 0 { + if first_marker && i == 0 && bit == 7 { + continue; + } + return Some((i, 7 - bit)); + } + } + } + } + None +} + +pub fn find_edition_marker_pda(mint: &Pubkey, edition_num: u64) -> (Pubkey, u8) { + let edition_marker_number = edition_num.checked_div(EDITION_MARKER_BIT_SIZE).unwrap(); + let edition_marker_number_str = edition_marker_number.to_string(); + + Pubkey::find_program_address( + &[ + "metadata".as_bytes(), + mpl_token_metadata::ID.as_ref(), + mint.as_ref(), + "edition".as_bytes(), + edition_marker_number_str.as_bytes(), + ], + &mpl_token_metadata::ID, + ) +} diff --git a/fixed-price-sale/program/tests/buy.rs b/fixed-price-sale/program/tests/buy.rs index 0df108abba..7588584582 100644 --- a/fixed-price-sale/program/tests/buy.rs +++ b/fixed-price-sale/program/tests/buy.rs @@ -6,8 +6,9 @@ mod buy { setup_context, utils::{ helpers::{ - airdrop, create_collection, create_master_nft, create_mint, create_token_account, - mint_to, unwrap_ignoring_io_error_in_ci, + airdrop, buy_one, buy_setup, create_collection, create_master_nft, create_mint, + create_token_account, fill_edition_marker, mint_to, unwrap_ignoring_io_error_in_ci, + BuyManager, }, setup_functions::{setup_selling_resource, setup_store}, }, @@ -21,9 +22,14 @@ mod buy { instruction as mpl_fixed_price_sale_instruction, state::{GatingConfig, SellingResource, TradeHistory}, utils::{ - find_trade_history_address, find_treasury_owner_address, find_vault_owner_address, + find_edition_marker_pda, find_trade_history_address, find_treasury_owner_address, + find_vault_owner_address, }, }; + use mpl_token_metadata::{ + instruction::burn_edition_nft, + state::{MasterEditionV2, TokenMetadataAccount}, + }; use solana_program::{clock::Clock, instruction::AccountMeta}; use solana_program_test::*; use solana_sdk::{ @@ -51,6 +57,7 @@ mod buy { None, true, false, + 1, ) .await; @@ -349,6 +356,7 @@ mod buy { None, true, false, + 1, ) .await; @@ -616,6 +624,7 @@ mod buy { None, true, false, + 1, ) .await; @@ -893,6 +902,7 @@ mod buy { None, true, false, + 1, ) .await; @@ -1175,6 +1185,7 @@ mod buy { None, true, false, + 1, ) .await; @@ -1484,6 +1495,7 @@ mod buy { None, true, false, + 1, ) .await; @@ -1795,6 +1807,7 @@ mod buy { None, true, false, + 1, ) .await; @@ -2091,6 +2104,7 @@ mod buy { None, true, false, + 1, ) .await; @@ -2489,6 +2503,7 @@ mod buy { None, true, false, + 1, ) .await; @@ -2809,6 +2824,7 @@ mod buy { None, true, false, + 1, ) .await; @@ -3129,6 +3145,7 @@ mod buy { None, true, false, + 1, ) .await; @@ -3421,6 +3438,7 @@ mod buy { None, true, false, + 1, ) .await; @@ -3745,6 +3763,7 @@ mod buy { None, true, false, + 1, ) .await; @@ -4027,7 +4046,805 @@ mod buy { ERROR_CODE_OFFSET + ErrorCode::WrongGatingToken as u32 ); } - _ => assert!(false), + _ => panic!("Wrong error code"), + } + } + + #[tokio::test] + async fn mint_after_edition_burn() { + setup_context!(context, mpl_fixed_price_sale, mpl_token_metadata); + let (admin_wallet, store_keypair) = setup_store(&mut context).await; + + let (selling_resource_keypair, selling_resource_owner_keypair, vault) = + setup_selling_resource( + &mut context, + &admin_wallet, + &store_keypair, + 100, + None, + true, + false, + 10, + ) + .await; + + airdrop( + &mut context, + &selling_resource_owner_keypair.pubkey(), + 10_000_000_000, + ) + .await; + + let market_keypair = Keypair::new(); + + let treasury_mint_keypair = Keypair::new(); + create_mint( + &mut context, + &treasury_mint_keypair, + &admin_wallet.pubkey(), + 0, + ) + .await; + + let (treasury_owner, treasury_owner_bump) = find_treasury_owner_address( + &treasury_mint_keypair.pubkey(), + &selling_resource_keypair.pubkey(), + ); + + let treasury_holder_keypair = Keypair::new(); + create_token_account( + &mut context, + &treasury_holder_keypair, + &treasury_mint_keypair.pubkey(), + &treasury_owner, + ) + .await; + + let start_date = context + .banks_client + .get_sysvar::() + .await + .unwrap() + .unix_timestamp + + 1; + + let name = "Marktname".to_string(); + let description = "Marktbeschreibung".to_string(); + let mutable = true; + let price = 1_000_000; + let pieces_in_one_wallet = Some(10); + + // CreateMarket + let accounts = mpl_fixed_price_sale_accounts::CreateMarket { + market: market_keypair.pubkey(), + store: store_keypair.pubkey(), + selling_resource_owner: selling_resource_owner_keypair.pubkey(), + selling_resource: selling_resource_keypair.pubkey(), + mint: treasury_mint_keypair.pubkey(), + treasury_holder: treasury_holder_keypair.pubkey(), + owner: treasury_owner, + system_program: system_program::id(), + } + .to_account_metas(None); + + let data = mpl_fixed_price_sale_instruction::CreateMarket { + _treasury_owner_bump: treasury_owner_bump, + name: name.to_owned(), + description: description.to_owned(), + mutable, + price, + pieces_in_one_wallet, + start_date: start_date as u64, + end_date: None, + gating_config: None, + } + .data(); + + let instruction = Instruction { + program_id: mpl_fixed_price_sale::id(), + data, + accounts, + }; + + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&context.payer.pubkey()), + &[ + &context.payer, + &market_keypair, + &selling_resource_owner_keypair, + ], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction_with_commitment(tx, CommitmentLevel::Confirmed) + .await + .unwrap(); + + let clock = context.banks_client.get_sysvar::().await.unwrap(); + context.warp_to_slot(clock.slot + 1500).unwrap(); + + // Buy setup + let selling_resource_data = context + .banks_client + .get_account(selling_resource_keypair.pubkey()) + .await + .unwrap() + .unwrap() + .data; + let selling_resource = + SellingResource::try_deserialize(&mut selling_resource_data.as_ref()).unwrap(); + + let (trade_history, trade_history_bump) = + find_trade_history_address(&context.payer.pubkey(), &market_keypair.pubkey()); + let (owner, vault_owner_bump) = + find_vault_owner_address(&selling_resource.resource, &selling_resource.store); + + let payer_pubkey = context.payer.pubkey(); + + let user_token_account = Keypair::new(); + create_token_account( + &mut context, + &user_token_account, + &treasury_mint_keypair.pubkey(), + &payer_pubkey, + ) + .await; + + mint_to( + &mut context, + &treasury_mint_keypair.pubkey(), + &user_token_account.pubkey(), + &admin_wallet, + 10_000_000, + ) + .await; + + let new_mint_keypair = Keypair::new(); + create_mint(&mut context, &new_mint_keypair, &payer_pubkey, 0).await; + + let new_mint_token_account = Keypair::new(); + create_token_account( + &mut context, + &new_mint_token_account, + &new_mint_keypair.pubkey(), + &payer_pubkey, + ) + .await; + + let payer_keypair = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + mint_to( + &mut context, + &new_mint_keypair.pubkey(), + &new_mint_token_account.pubkey(), + &payer_keypair, + 1, + ) + .await; + + let (master_edition_metadata, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + selling_resource.resource.as_ref(), + ], + &mpl_token_metadata::id(), + ); + + let (master_edition, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + selling_resource.resource.as_ref(), + mpl_token_metadata::state::EDITION.as_bytes(), + ], + &mpl_token_metadata::id(), + ); + + let (edition_marker, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + selling_resource.resource.as_ref(), + mpl_token_metadata::state::EDITION.as_bytes(), + selling_resource.supply.to_string().as_bytes(), + ], + &mpl_token_metadata::id(), + ); + + let (new_metadata, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + new_mint_keypair.pubkey().as_ref(), + ], + &mpl_token_metadata::id(), + ); + + let (new_edition, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + new_mint_keypair.pubkey().as_ref(), + mpl_token_metadata::state::EDITION.as_bytes(), + ], + &mpl_token_metadata::id(), + ); + + // Buy + let accounts = mpl_fixed_price_sale_accounts::Buy { + market: market_keypair.pubkey(), + selling_resource: selling_resource_keypair.pubkey(), + user_token_account: user_token_account.pubkey(), + user_wallet: context.payer.pubkey(), + trade_history, + treasury_holder: treasury_holder_keypair.pubkey(), + new_metadata, + new_edition, + master_edition, + new_mint: new_mint_keypair.pubkey(), + edition_marker, + vault: selling_resource.vault, + owner, + new_token_account: new_mint_token_account.pubkey(), + master_edition_metadata, + clock: sysvar::clock::id(), + rent: sysvar::rent::id(), + token_metadata_program: mpl_token_metadata::id(), + token_program: spl_token::id(), + system_program: system_program::id(), + } + .to_account_metas(None); + + let data = mpl_fixed_price_sale_instruction::Buy { + _trade_history_bump: trade_history_bump, + vault_owner_bump, } + .data(); + + let instruction = Instruction { + program_id: mpl_fixed_price_sale::id(), + data, + accounts, + }; + + let tx = Transaction::new_signed_with_payer( + &[instruction.clone()], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction_with_commitment(tx, CommitmentLevel::Confirmed) + .await + .unwrap(); + + let master_edition_account = context + .banks_client + .get_account(master_edition) + .await + .unwrap() + .unwrap(); + let master_edition_struct = + MasterEditionV2::safe_deserialize(&master_edition_account.data).unwrap(); + + assert_eq!(master_edition_struct.supply, 1); + + /* Burn the edition */ + let ix = burn_edition_nft( + mpl_token_metadata::ID, + new_metadata, + payer_pubkey, + new_mint_keypair.pubkey(), + selling_resource.resource, + new_mint_token_account.pubkey(), + vault.pubkey(), + master_edition, + new_edition, + edition_marker, + spl_token::ID, + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &[&context.payer, &payer_keypair], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + let master_edition_account = context + .banks_client + .get_account(master_edition) + .await + .unwrap() + .unwrap(); + let master_edition_struct = + MasterEditionV2::safe_deserialize(&master_edition_account.data).unwrap(); + + assert_eq!(master_edition_struct.supply, 0); + /* BURN ENDED */ + + /* Buy Another */ + + let new_mint_keypair = Keypair::new(); + create_mint(&mut context, &new_mint_keypair, &payer_pubkey, 0).await; + + let new_mint_token_account = Keypair::new(); + create_token_account( + &mut context, + &new_mint_token_account, + &new_mint_keypair.pubkey(), + &payer_pubkey, + ) + .await; + + let payer_keypair = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + mint_to( + &mut context, + &new_mint_keypair.pubkey(), + &new_mint_token_account.pubkey(), + &payer_keypair, + 1, + ) + .await; + + let (new_metadata, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + new_mint_keypair.pubkey().as_ref(), + ], + &mpl_token_metadata::id(), + ); + + let (new_edition, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + new_mint_keypair.pubkey().as_ref(), + mpl_token_metadata::state::EDITION.as_bytes(), + ], + &mpl_token_metadata::id(), + ); + + // Buy + let accounts = mpl_fixed_price_sale_accounts::Buy { + market: market_keypair.pubkey(), + selling_resource: selling_resource_keypair.pubkey(), + user_token_account: user_token_account.pubkey(), + user_wallet: context.payer.pubkey(), + trade_history, + treasury_holder: treasury_holder_keypair.pubkey(), + new_metadata, + new_edition, + master_edition, + new_mint: new_mint_keypair.pubkey(), + edition_marker, + vault: selling_resource.vault, + owner, + new_token_account: new_mint_token_account.pubkey(), + master_edition_metadata, + clock: sysvar::clock::id(), + rent: sysvar::rent::id(), + token_metadata_program: mpl_token_metadata::id(), + token_program: spl_token::id(), + system_program: system_program::id(), + } + .to_account_metas(None); + + let data = mpl_fixed_price_sale_instruction::Buy { + _trade_history_bump: trade_history_bump, + vault_owner_bump, + } + .data(); + + let instruction = Instruction { + program_id: mpl_fixed_price_sale::id(), + data, + accounts, + }; + + let tx = Transaction::new_signed_with_payer( + &[instruction.clone()], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction_with_commitment(tx, CommitmentLevel::Confirmed) + .await + .unwrap(); + } + + #[tokio::test] + async fn mint_from_multiple_pdas() { + setup_context!(context, mpl_fixed_price_sale, mpl_token_metadata); + let (admin_wallet, store_keypair) = setup_store(&mut context).await; + + let edition_mint_amount = 1000; + let max_supply = 2 * edition_mint_amount; + + let (selling_resource_keypair, selling_resource_owner_keypair, _vault) = + setup_selling_resource( + &mut context, + &admin_wallet, + &store_keypair, + 100, + None, + true, + false, + max_supply, + ) + .await; + + airdrop( + &mut context, + &selling_resource_owner_keypair.pubkey(), + 10_000_000_000_000, + ) + .await; + + let market_keypair = Keypair::new(); + + let treasury_mint_keypair = Keypair::new(); + create_mint( + &mut context, + &treasury_mint_keypair, + &admin_wallet.pubkey(), + 0, + ) + .await; + + let (treasury_owner, treasyry_owner_bump) = find_treasury_owner_address( + &treasury_mint_keypair.pubkey(), + &selling_resource_keypair.pubkey(), + ); + + let treasury_holder_keypair = Keypair::new(); + create_token_account( + &mut context, + &treasury_holder_keypair, + &treasury_mint_keypair.pubkey(), + &treasury_owner, + ) + .await; + + let start_date = context + .banks_client + .get_sysvar::() + .await + .unwrap() + .unix_timestamp + + 1; + + let name = "Marktname".to_string(); + let description = "Marktbeschreibung".to_string(); + let mutable = true; + let price = 1_000; + let pieces_in_one_wallet = Some(edition_mint_amount); + + // CreateMarket + let accounts = mpl_fixed_price_sale_accounts::CreateMarket { + market: market_keypair.pubkey(), + store: store_keypair.pubkey(), + selling_resource_owner: selling_resource_owner_keypair.pubkey(), + selling_resource: selling_resource_keypair.pubkey(), + mint: treasury_mint_keypair.pubkey(), + treasury_holder: treasury_holder_keypair.pubkey(), + owner: treasury_owner, + system_program: system_program::id(), + } + .to_account_metas(None); + + let data = mpl_fixed_price_sale_instruction::CreateMarket { + _treasury_owner_bump: treasyry_owner_bump, + name: name.to_owned(), + description: description.to_owned(), + mutable, + price, + pieces_in_one_wallet, + start_date: start_date as u64, + end_date: None, + gating_config: None, + } + .data(); + + let instruction = Instruction { + program_id: mpl_fixed_price_sale::id(), + data, + accounts, + }; + + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&context.payer.pubkey()), + &[ + &context.payer, + &market_keypair, + &selling_resource_owner_keypair, + ], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction_with_commitment(tx, CommitmentLevel::Confirmed) + .await + .unwrap(); + + let clock = context.banks_client.get_sysvar::().await.unwrap(); + context.warp_to_slot(clock.slot + 1500).unwrap(); + + // Buy setup + let mut buy_manager = BuyManager { + context: &mut context, + selling_resource_keypair, + selling_resource: None, + market_keypair, + treasury_mint_keypair, + treasury_holder_keypair, + admin_wallet, + user_token_account: None, + trade_history: None, + trade_history_bump: None, + vault_owner_bump: None, + vault_owner: None, + }; + + buy_setup(&mut buy_manager).await.unwrap(); + + // Buy one from the first edition marker to initialize the account. + buy_one(&mut buy_manager, Some(0)).await.unwrap(); + + // Fill up the first edition marker so we can mint from the second. + // The supply will be incorrect but that doesn't matter for this test. + fill_edition_marker(&mut buy_manager, 0).await; + + // We should be able to buy editions from the second edition marker + buy_one(&mut buy_manager, Some(1)).await.unwrap(); + buy_one(&mut buy_manager, Some(1)).await.unwrap(); + + // Did it work? + let (edition_marker, _) = find_edition_marker_pda( + &buy_manager.selling_resource.as_ref().unwrap().resource, + 248, + ); + + let edition_marker_account = buy_manager + .context + .banks_client + .get_account(edition_marker) + .await + .unwrap() + .unwrap(); + + // Key is correct. + assert_eq!(edition_marker_account.data[0], 7); + // First two editions are minted: 1100 0000 == 192 + assert_eq!(edition_marker_account.data[1], 192); + + // Fill the second edition marker + fill_edition_marker(&mut buy_manager, 1).await; + + // Mint a couple from the third edition marker + buy_one(&mut buy_manager, Some(2)).await.unwrap(); + buy_one(&mut buy_manager, Some(2)).await.unwrap(); + + // Did it work? + let (edition_marker, _) = find_edition_marker_pda( + &buy_manager.selling_resource.as_ref().unwrap().resource, + 496, + ); + + let edition_marker_account = buy_manager + .context + .banks_client + .get_account(edition_marker) + .await + .unwrap() + .unwrap(); + + // Key is correct. + assert_eq!(edition_marker_account.data[0], 7); + // First two editions are minted: 1100 0000 == 192 + assert_eq!(edition_marker_account.data[1], 192); + } + + #[ignore] + #[tokio::test] + // Boutique test for running locally + async fn mint_many() { + setup_context!(context, mpl_fixed_price_sale, mpl_token_metadata); + let (admin_wallet, store_keypair) = setup_store(&mut context).await; + + let edition_mint_amount = 1000; + let max_supply = 2 * edition_mint_amount; + + let (selling_resource_keypair, selling_resource_owner_keypair, _vault) = + setup_selling_resource( + &mut context, + &admin_wallet, + &store_keypair, + 100, + None, + true, + false, + max_supply, + ) + .await; + + airdrop( + &mut context, + &selling_resource_owner_keypair.pubkey(), + 10_000_000_000_000, + ) + .await; + + let market_keypair = Keypair::new(); + + let treasury_mint_keypair = Keypair::new(); + create_mint( + &mut context, + &treasury_mint_keypair, + &admin_wallet.pubkey(), + 0, + ) + .await; + + let (treasury_owner, treasyry_owner_bump) = find_treasury_owner_address( + &treasury_mint_keypair.pubkey(), + &selling_resource_keypair.pubkey(), + ); + + let treasury_holder_keypair = Keypair::new(); + create_token_account( + &mut context, + &treasury_holder_keypair, + &treasury_mint_keypair.pubkey(), + &treasury_owner, + ) + .await; + + let start_date = context + .banks_client + .get_sysvar::() + .await + .unwrap() + .unix_timestamp + + 1; + + let name = "Marktname".to_string(); + let description = "Marktbeschreibung".to_string(); + let mutable = true; + let price = 1_000; + let pieces_in_one_wallet = Some(edition_mint_amount); + + // CreateMarket + let accounts = mpl_fixed_price_sale_accounts::CreateMarket { + market: market_keypair.pubkey(), + store: store_keypair.pubkey(), + selling_resource_owner: selling_resource_owner_keypair.pubkey(), + selling_resource: selling_resource_keypair.pubkey(), + mint: treasury_mint_keypair.pubkey(), + treasury_holder: treasury_holder_keypair.pubkey(), + owner: treasury_owner, + system_program: system_program::id(), + } + .to_account_metas(None); + + let data = mpl_fixed_price_sale_instruction::CreateMarket { + _treasury_owner_bump: treasyry_owner_bump, + name: name.to_owned(), + description: description.to_owned(), + mutable, + price, + pieces_in_one_wallet, + start_date: start_date as u64, + end_date: None, + gating_config: None, + } + .data(); + + let instruction = Instruction { + program_id: mpl_fixed_price_sale::id(), + data, + accounts, + }; + + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&context.payer.pubkey()), + &[ + &context.payer, + &market_keypair, + &selling_resource_owner_keypair, + ], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction_with_commitment(tx, CommitmentLevel::Confirmed) + .await + .unwrap(); + + let clock = context.banks_client.get_sysvar::().await.unwrap(); + context.warp_to_slot(clock.slot + 1500).unwrap(); + + // Buy setup + let mut buy_manager = BuyManager { + context: &mut context, + selling_resource_keypair, + selling_resource: None, + market_keypair, + treasury_mint_keypair, + treasury_holder_keypair, + admin_wallet, + user_token_account: None, + trade_history: None, + trade_history_bump: None, + vault_owner_bump: None, + vault_owner: None, + }; + + buy_setup(&mut buy_manager).await.unwrap(); + + for i in 1..=edition_mint_amount { + buy_one(&mut buy_manager, None).await.unwrap(); + if i % 5 == 0 { + let slot = buy_manager + .context + .banks_client + .get_root_slot() + .await + .unwrap(); + buy_manager.context.warp_to_slot(slot + 100).unwrap(); + } + } + + let clock = buy_manager + .context + .banks_client + .get_sysvar::() + .await + .unwrap(); + buy_manager.context.warp_to_slot(clock.slot + 3).unwrap(); + + // Checks + let selling_resource_acc = buy_manager + .context + .banks_client + .get_account(buy_manager.selling_resource_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let selling_resource_data = + SellingResource::try_deserialize(&mut selling_resource_acc.data.as_ref()).unwrap(); + + let (trade_history, _) = find_trade_history_address( + &buy_manager.context.payer.pubkey(), + &buy_manager.market_keypair.pubkey(), + ); + + let trade_history_acc = buy_manager + .context + .banks_client + .get_account(trade_history) + .await + .unwrap() + .unwrap(); + let trade_history_data = + TradeHistory::try_deserialize(&mut trade_history_acc.data.as_ref()).unwrap(); + + assert_eq!(selling_resource_data.supply, edition_mint_amount); + assert_eq!(trade_history_data.already_bought, edition_mint_amount); } } diff --git a/fixed-price-sale/program/tests/buy_v2.rs b/fixed-price-sale/program/tests/buy_v2.rs new file mode 100644 index 0000000000..c5852188b7 --- /dev/null +++ b/fixed-price-sale/program/tests/buy_v2.rs @@ -0,0 +1,838 @@ +mod utils; + +#[cfg(feature = "test-bpf")] +mod buy_v2 { + use crate::{ + setup_context, + utils::{ + helpers::{ + airdrop, buy_one_v2, buy_setup, create_mint, create_token_account, + fill_edition_marker, mint_to, BuyManager, + }, + setup_functions::{setup_selling_resource, setup_store}, + }, + }; + use anchor_lang::{AccountDeserialize, InstructionData, ToAccountMetas}; + use mpl_fixed_price_sale::{ + accounts as mpl_fixed_price_sale_accounts, instruction as mpl_fixed_price_sale_instruction, + state::{SellingResource, TradeHistory}, + utils::{ + find_edition_marker_pda, find_trade_history_address, find_treasury_owner_address, + find_vault_owner_address, + }, + }; + use mpl_token_metadata::{ + instruction::burn_edition_nft, + state::{MasterEditionV2, TokenMetadataAccount}, + }; + use solana_program::clock::Clock; + use solana_program_test::*; + use solana_sdk::{ + commitment_config::CommitmentLevel, instruction::Instruction, pubkey::Pubkey, + signature::Keypair, signer::Signer, system_program, sysvar, transaction::Transaction, + }; + + #[tokio::test] + async fn mint_after_edition_burn() { + setup_context!(context, mpl_fixed_price_sale, mpl_token_metadata); + let (admin_wallet, store_keypair) = setup_store(&mut context).await; + + let (selling_resource_keypair, selling_resource_owner_keypair, vault) = + setup_selling_resource( + &mut context, + &admin_wallet, + &store_keypair, + 100, + None, + true, + false, + 10, + ) + .await; + + airdrop( + &mut context, + &selling_resource_owner_keypair.pubkey(), + 10_000_000_000, + ) + .await; + + let market_keypair = Keypair::new(); + + let treasury_mint_keypair = Keypair::new(); + create_mint( + &mut context, + &treasury_mint_keypair, + &admin_wallet.pubkey(), + 0, + ) + .await; + + let (treasury_owner, treasury_owner_bump) = find_treasury_owner_address( + &treasury_mint_keypair.pubkey(), + &selling_resource_keypair.pubkey(), + ); + + let treasury_holder_keypair = Keypair::new(); + create_token_account( + &mut context, + &treasury_holder_keypair, + &treasury_mint_keypair.pubkey(), + &treasury_owner, + ) + .await; + + let start_date = context + .banks_client + .get_sysvar::() + .await + .unwrap() + .unix_timestamp + + 1; + + let name = "Marktname".to_string(); + let description = "Marktbeschreibung".to_string(); + let mutable = true; + let price = 1_000_000; + let pieces_in_one_wallet = Some(10); + + // CreateMarket + let accounts = mpl_fixed_price_sale_accounts::CreateMarket { + market: market_keypair.pubkey(), + store: store_keypair.pubkey(), + selling_resource_owner: selling_resource_owner_keypair.pubkey(), + selling_resource: selling_resource_keypair.pubkey(), + mint: treasury_mint_keypair.pubkey(), + treasury_holder: treasury_holder_keypair.pubkey(), + owner: treasury_owner, + system_program: system_program::id(), + } + .to_account_metas(None); + + let data = mpl_fixed_price_sale_instruction::CreateMarket { + _treasury_owner_bump: treasury_owner_bump, + name: name.to_owned(), + description: description.to_owned(), + mutable, + price, + pieces_in_one_wallet, + start_date: start_date as u64, + end_date: None, + gating_config: None, + } + .data(); + + let instruction = Instruction { + program_id: mpl_fixed_price_sale::id(), + data, + accounts, + }; + + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&context.payer.pubkey()), + &[ + &context.payer, + &market_keypair, + &selling_resource_owner_keypair, + ], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction_with_commitment(tx, CommitmentLevel::Confirmed) + .await + .unwrap(); + + let clock = context.banks_client.get_sysvar::().await.unwrap(); + context.warp_to_slot(clock.slot + 1500).unwrap(); + + // Buy setup + let selling_resource_data = context + .banks_client + .get_account(selling_resource_keypair.pubkey()) + .await + .unwrap() + .unwrap() + .data; + let selling_resource = + SellingResource::try_deserialize(&mut selling_resource_data.as_ref()).unwrap(); + + let (trade_history, trade_history_bump) = + find_trade_history_address(&context.payer.pubkey(), &market_keypair.pubkey()); + let (owner, vault_owner_bump) = + find_vault_owner_address(&selling_resource.resource, &selling_resource.store); + + let payer_pubkey = context.payer.pubkey(); + + let user_token_account = Keypair::new(); + create_token_account( + &mut context, + &user_token_account, + &treasury_mint_keypair.pubkey(), + &payer_pubkey, + ) + .await; + + mint_to( + &mut context, + &treasury_mint_keypair.pubkey(), + &user_token_account.pubkey(), + &admin_wallet, + 10_000_000, + ) + .await; + + let new_mint_keypair = Keypair::new(); + create_mint(&mut context, &new_mint_keypair, &payer_pubkey, 0).await; + + let new_mint_token_account = Keypair::new(); + create_token_account( + &mut context, + &new_mint_token_account, + &new_mint_keypair.pubkey(), + &payer_pubkey, + ) + .await; + + let payer_keypair = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + mint_to( + &mut context, + &new_mint_keypair.pubkey(), + &new_mint_token_account.pubkey(), + &payer_keypair, + 1, + ) + .await; + + let (master_edition_metadata, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + selling_resource.resource.as_ref(), + ], + &mpl_token_metadata::id(), + ); + + let (master_edition, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + selling_resource.resource.as_ref(), + mpl_token_metadata::state::EDITION.as_bytes(), + ], + &mpl_token_metadata::id(), + ); + + let (edition_marker, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + selling_resource.resource.as_ref(), + mpl_token_metadata::state::EDITION.as_bytes(), + selling_resource.supply.to_string().as_bytes(), + ], + &mpl_token_metadata::id(), + ); + + let (new_metadata, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + new_mint_keypair.pubkey().as_ref(), + ], + &mpl_token_metadata::id(), + ); + + let (new_edition, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + new_mint_keypair.pubkey().as_ref(), + mpl_token_metadata::state::EDITION.as_bytes(), + ], + &mpl_token_metadata::id(), + ); + + let edition_marker_number = 0; + + // Buy + let accounts = mpl_fixed_price_sale_accounts::Buy { + market: market_keypair.pubkey(), + selling_resource: selling_resource_keypair.pubkey(), + user_token_account: user_token_account.pubkey(), + user_wallet: context.payer.pubkey(), + trade_history, + treasury_holder: treasury_holder_keypair.pubkey(), + new_metadata, + new_edition, + master_edition, + new_mint: new_mint_keypair.pubkey(), + edition_marker, + vault: selling_resource.vault, + owner, + new_token_account: new_mint_token_account.pubkey(), + master_edition_metadata, + clock: sysvar::clock::id(), + rent: sysvar::rent::id(), + token_metadata_program: mpl_token_metadata::id(), + token_program: spl_token::id(), + system_program: system_program::id(), + } + .to_account_metas(None); + + let data = mpl_fixed_price_sale_instruction::BuyV2 { + _trade_history_bump: trade_history_bump, + vault_owner_bump, + edition_marker_number, + } + .data(); + + let instruction = Instruction { + program_id: mpl_fixed_price_sale::id(), + data, + accounts, + }; + + let tx = Transaction::new_signed_with_payer( + &[instruction.clone()], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction_with_commitment(tx, CommitmentLevel::Confirmed) + .await + .unwrap(); + + let master_edition_account = context + .banks_client + .get_account(master_edition) + .await + .unwrap() + .unwrap(); + let master_edition_struct = + MasterEditionV2::safe_deserialize(&master_edition_account.data).unwrap(); + + assert_eq!(master_edition_struct.supply, 1); + + /* Burn the edition */ + let ix = burn_edition_nft( + mpl_token_metadata::ID, + new_metadata, + payer_pubkey, + new_mint_keypair.pubkey(), + selling_resource.resource, + new_mint_token_account.pubkey(), + vault.pubkey(), + master_edition, + new_edition, + edition_marker, + spl_token::ID, + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &[&context.payer, &payer_keypair], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + let master_edition_account = context + .banks_client + .get_account(master_edition) + .await + .unwrap() + .unwrap(); + let master_edition_struct = + MasterEditionV2::safe_deserialize(&master_edition_account.data).unwrap(); + + assert_eq!(master_edition_struct.supply, 0); /* BURN ENDED */ + + /* Buy Another */ + + let new_mint_keypair = Keypair::new(); + create_mint(&mut context, &new_mint_keypair, &payer_pubkey, 0).await; + + let new_mint_token_account = Keypair::new(); + create_token_account( + &mut context, + &new_mint_token_account, + &new_mint_keypair.pubkey(), + &payer_pubkey, + ) + .await; + + let payer_keypair = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + mint_to( + &mut context, + &new_mint_keypair.pubkey(), + &new_mint_token_account.pubkey(), + &payer_keypair, + 1, + ) + .await; + + let (new_metadata, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + new_mint_keypair.pubkey().as_ref(), + ], + &mpl_token_metadata::id(), + ); + + let (new_edition, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + new_mint_keypair.pubkey().as_ref(), + mpl_token_metadata::state::EDITION.as_bytes(), + ], + &mpl_token_metadata::id(), + ); + + // Buy + let accounts = mpl_fixed_price_sale_accounts::Buy { + market: market_keypair.pubkey(), + selling_resource: selling_resource_keypair.pubkey(), + user_token_account: user_token_account.pubkey(), + user_wallet: context.payer.pubkey(), + trade_history, + treasury_holder: treasury_holder_keypair.pubkey(), + new_metadata, + new_edition, + master_edition, + new_mint: new_mint_keypair.pubkey(), + edition_marker, + vault: selling_resource.vault, + owner, + new_token_account: new_mint_token_account.pubkey(), + master_edition_metadata, + clock: sysvar::clock::id(), + rent: sysvar::rent::id(), + token_metadata_program: mpl_token_metadata::id(), + token_program: spl_token::id(), + system_program: system_program::id(), + } + .to_account_metas(None); + + let data = mpl_fixed_price_sale_instruction::BuyV2 { + _trade_history_bump: trade_history_bump, + vault_owner_bump, + edition_marker_number, + } + .data(); + + let instruction = Instruction { + program_id: mpl_fixed_price_sale::id(), + data, + accounts, + }; + + let tx = Transaction::new_signed_with_payer( + &[instruction.clone()], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction_with_commitment(tx, CommitmentLevel::Confirmed) + .await + .unwrap(); + } + + #[tokio::test] + async fn mint_from_multiple_pdas() { + setup_context!(context, mpl_fixed_price_sale, mpl_token_metadata); + let (admin_wallet, store_keypair) = setup_store(&mut context).await; + + let edition_mint_amount = 1000; + let max_supply = 2 * edition_mint_amount; + + let (selling_resource_keypair, selling_resource_owner_keypair, _vault) = + setup_selling_resource( + &mut context, + &admin_wallet, + &store_keypair, + 100, + None, + true, + false, + max_supply, + ) + .await; + + airdrop( + &mut context, + &selling_resource_owner_keypair.pubkey(), + 10_000_000_000_000, + ) + .await; + + let market_keypair = Keypair::new(); + + let treasury_mint_keypair = Keypair::new(); + create_mint( + &mut context, + &treasury_mint_keypair, + &admin_wallet.pubkey(), + 0, + ) + .await; + + let (treasury_owner, treasyry_owner_bump) = find_treasury_owner_address( + &treasury_mint_keypair.pubkey(), + &selling_resource_keypair.pubkey(), + ); + + let treasury_holder_keypair = Keypair::new(); + create_token_account( + &mut context, + &treasury_holder_keypair, + &treasury_mint_keypair.pubkey(), + &treasury_owner, + ) + .await; + + let start_date = context + .banks_client + .get_sysvar::() + .await + .unwrap() + .unix_timestamp + + 1; + + let name = "Marktname".to_string(); + let description = "Marktbeschreibung".to_string(); + let mutable = true; + let price = 1_000; + let pieces_in_one_wallet = Some(edition_mint_amount); + + // CreateMarket + let accounts = mpl_fixed_price_sale_accounts::CreateMarket { + market: market_keypair.pubkey(), + store: store_keypair.pubkey(), + selling_resource_owner: selling_resource_owner_keypair.pubkey(), + selling_resource: selling_resource_keypair.pubkey(), + mint: treasury_mint_keypair.pubkey(), + treasury_holder: treasury_holder_keypair.pubkey(), + owner: treasury_owner, + system_program: system_program::id(), + } + .to_account_metas(None); + + let data = mpl_fixed_price_sale_instruction::CreateMarket { + _treasury_owner_bump: treasyry_owner_bump, + name: name.to_owned(), + description: description.to_owned(), + mutable, + price, + pieces_in_one_wallet, + start_date: start_date as u64, + end_date: None, + gating_config: None, + } + .data(); + + let instruction = Instruction { + program_id: mpl_fixed_price_sale::id(), + data, + accounts, + }; + + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&context.payer.pubkey()), + &[ + &context.payer, + &market_keypair, + &selling_resource_owner_keypair, + ], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction_with_commitment(tx, CommitmentLevel::Confirmed) + .await + .unwrap(); + + let clock = context.banks_client.get_sysvar::().await.unwrap(); + context.warp_to_slot(clock.slot + 1500).unwrap(); + + // Buy setup + let mut buy_manager = BuyManager { + context: &mut context, + selling_resource_keypair, + selling_resource: None, + market_keypair, + treasury_mint_keypair, + treasury_holder_keypair, + admin_wallet, + user_token_account: None, + trade_history: None, + trade_history_bump: None, + vault_owner_bump: None, + vault_owner: None, + }; + + buy_setup(&mut buy_manager).await.unwrap(); + + // Buy one from the first edition marker to initialize the account. + buy_one_v2(&mut buy_manager, 0).await.unwrap(); + + // Fill up the first edition marker so we can mint from the second. + // The supply will be incorrect but that doesn't matter for this test. + fill_edition_marker(&mut buy_manager, 0).await; + + // We should be able to buy editions from the second edition marker + buy_one_v2(&mut buy_manager, 1).await.unwrap(); + buy_one_v2(&mut buy_manager, 1).await.unwrap(); + + // Did it work? + let (edition_marker, _) = find_edition_marker_pda( + &buy_manager.selling_resource.as_ref().unwrap().resource, + 248, + ); + + let edition_marker_account = buy_manager + .context + .banks_client + .get_account(edition_marker) + .await + .unwrap() + .unwrap(); + + // Key is correct. + assert_eq!(edition_marker_account.data[0], 7); + // First two editions are minted: 1100 0000 == 192 + assert_eq!(edition_marker_account.data[1], 192); + + // Fill the second edition marker + fill_edition_marker(&mut buy_manager, 1).await; + + // Mint a couple from the third edition marker + buy_one_v2(&mut buy_manager, 2).await.unwrap(); + buy_one_v2(&mut buy_manager, 2).await.unwrap(); + + // Did it work? + let (edition_marker, _) = find_edition_marker_pda( + &buy_manager.selling_resource.as_ref().unwrap().resource, + 496, + ); + + let edition_marker_account = buy_manager + .context + .banks_client + .get_account(edition_marker) + .await + .unwrap() + .unwrap(); + + // Key is correct. + assert_eq!(edition_marker_account.data[0], 7); + // First two editions are minted: 1100 0000 == 192 + assert_eq!(edition_marker_account.data[1], 192); + } + + #[ignore] + #[tokio::test] + // Boutique test for running locally + async fn mint_many() { + setup_context!(context, mpl_fixed_price_sale, mpl_token_metadata); + let (admin_wallet, store_keypair) = setup_store(&mut context).await; + + let edition_mint_amount = 1000; + let max_supply = 2 * edition_mint_amount; + + let (selling_resource_keypair, selling_resource_owner_keypair, _vault) = + setup_selling_resource( + &mut context, + &admin_wallet, + &store_keypair, + 100, + None, + true, + false, + max_supply, + ) + .await; + + airdrop( + &mut context, + &selling_resource_owner_keypair.pubkey(), + 10_000_000_000_000, + ) + .await; + + let market_keypair = Keypair::new(); + + let treasury_mint_keypair = Keypair::new(); + create_mint( + &mut context, + &treasury_mint_keypair, + &admin_wallet.pubkey(), + 0, + ) + .await; + + let (treasury_owner, treasyry_owner_bump) = find_treasury_owner_address( + &treasury_mint_keypair.pubkey(), + &selling_resource_keypair.pubkey(), + ); + + let treasury_holder_keypair = Keypair::new(); + create_token_account( + &mut context, + &treasury_holder_keypair, + &treasury_mint_keypair.pubkey(), + &treasury_owner, + ) + .await; + + let start_date = context + .banks_client + .get_sysvar::() + .await + .unwrap() + .unix_timestamp + + 1; + + let name = "Marktname".to_string(); + let description = "Marktbeschreibung".to_string(); + let mutable = true; + let price = 1_000; + let pieces_in_one_wallet = Some(edition_mint_amount); + + // CreateMarket + let accounts = mpl_fixed_price_sale_accounts::CreateMarket { + market: market_keypair.pubkey(), + store: store_keypair.pubkey(), + selling_resource_owner: selling_resource_owner_keypair.pubkey(), + selling_resource: selling_resource_keypair.pubkey(), + mint: treasury_mint_keypair.pubkey(), + treasury_holder: treasury_holder_keypair.pubkey(), + owner: treasury_owner, + system_program: system_program::id(), + } + .to_account_metas(None); + + let data = mpl_fixed_price_sale_instruction::CreateMarket { + _treasury_owner_bump: treasyry_owner_bump, + name: name.to_owned(), + description: description.to_owned(), + mutable, + price, + pieces_in_one_wallet, + start_date: start_date as u64, + end_date: None, + gating_config: None, + } + .data(); + + let instruction = Instruction { + program_id: mpl_fixed_price_sale::id(), + data, + accounts, + }; + + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&context.payer.pubkey()), + &[ + &context.payer, + &market_keypair, + &selling_resource_owner_keypair, + ], + context.last_blockhash, + ); + + context + .banks_client + .process_transaction_with_commitment(tx, CommitmentLevel::Confirmed) + .await + .unwrap(); + + let clock = context.banks_client.get_sysvar::().await.unwrap(); + context.warp_to_slot(clock.slot + 1500).unwrap(); + + // Buy setup + let mut buy_manager = BuyManager { + context: &mut context, + selling_resource_keypair, + selling_resource: None, + market_keypair, + treasury_mint_keypair, + treasury_holder_keypair, + admin_wallet, + user_token_account: None, + trade_history: None, + trade_history_bump: None, + vault_owner_bump: None, + vault_owner: None, + }; + + buy_setup(&mut buy_manager).await.unwrap(); + + for i in 1..=edition_mint_amount { + let edition_marker_number = i / 248; + buy_one_v2(&mut buy_manager, edition_marker_number) + .await + .unwrap(); + if i % 5 == 0 { + let slot = buy_manager + .context + .banks_client + .get_root_slot() + .await + .unwrap(); + buy_manager.context.warp_to_slot(slot + 100).unwrap(); + } + } + + let clock = buy_manager + .context + .banks_client + .get_sysvar::() + .await + .unwrap(); + buy_manager.context.warp_to_slot(clock.slot + 3).unwrap(); + + // Checks + let selling_resource_acc = buy_manager + .context + .banks_client + .get_account(buy_manager.selling_resource_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let selling_resource_data = + SellingResource::try_deserialize(&mut selling_resource_acc.data.as_ref()).unwrap(); + + let (trade_history, _) = find_trade_history_address( + &buy_manager.context.payer.pubkey(), + &buy_manager.market_keypair.pubkey(), + ); + + let trade_history_acc = buy_manager + .context + .banks_client + .get_account(trade_history) + .await + .unwrap() + .unwrap(); + let trade_history_data = + TradeHistory::try_deserialize(&mut trade_history_acc.data.as_ref()).unwrap(); + + assert_eq!(selling_resource_data.supply, edition_mint_amount); + assert_eq!(trade_history_data.already_bought, edition_mint_amount); + } +} diff --git a/fixed-price-sale/program/tests/change_market.rs b/fixed-price-sale/program/tests/change_market.rs index 7b095f83db..f7344ef278 100644 --- a/fixed-price-sale/program/tests/change_market.rs +++ b/fixed-price-sale/program/tests/change_market.rs @@ -41,6 +41,7 @@ mod change_market { None, true, false, + 1, ) .await; @@ -234,6 +235,7 @@ mod change_market { None, true, false, + 1, ) .await; @@ -449,6 +451,7 @@ mod change_market { None, true, false, + 1, ) .await; diff --git a/fixed-price-sale/program/tests/claim_resource.rs b/fixed-price-sale/program/tests/claim_resource.rs index af403aa800..9a11421416 100644 --- a/fixed-price-sale/program/tests/claim_resource.rs +++ b/fixed-price-sale/program/tests/claim_resource.rs @@ -46,6 +46,7 @@ mod claim_resource { None, true, true, + 1, ) .await; @@ -506,6 +507,7 @@ mod claim_resource { None, true, true, + 1, ) .await; @@ -929,6 +931,7 @@ mod claim_resource { None, true, true, + 1, ) .await; diff --git a/fixed-price-sale/program/tests/close_market.rs b/fixed-price-sale/program/tests/close_market.rs index e1db446b0b..dc4d8b385c 100644 --- a/fixed-price-sale/program/tests/close_market.rs +++ b/fixed-price-sale/program/tests/close_market.rs @@ -39,6 +39,7 @@ mod close_market { None, true, false, + 1, ) .await; @@ -186,6 +187,7 @@ mod close_market { None, true, false, + 1, ) .await; diff --git a/fixed-price-sale/program/tests/create_market.rs b/fixed-price-sale/program/tests/create_market.rs index 115b5f0cc8..57dc6b68d1 100644 --- a/fixed-price-sale/program/tests/create_market.rs +++ b/fixed-price-sale/program/tests/create_market.rs @@ -35,6 +35,7 @@ mod create_market { None, true, false, + 1, ) .await; @@ -170,6 +171,7 @@ mod create_market { None, true, false, + 1, ) .await; @@ -179,6 +181,7 @@ mod create_market { &store_keypair, &selling_resource_keypair, &selling_resource_owner_keypair, + Some(1), ) .await; @@ -213,6 +216,7 @@ mod create_market { None, true, false, + 1, ) .await; @@ -324,6 +328,7 @@ mod create_market { None, true, false, + 1, ) .await; @@ -438,6 +443,7 @@ mod create_market { None, true, false, + 1, ) .await; @@ -539,6 +545,7 @@ mod create_market { None, true, false, + 1, ) .await; @@ -651,6 +658,7 @@ mod create_market { None, true, false, + 1, ) .await; @@ -749,6 +757,7 @@ mod create_market { None, true, false, + 1, ) .await; @@ -854,6 +863,7 @@ mod create_market { None, true, false, + 1, ) .await; diff --git a/fixed-price-sale/program/tests/resume_market.rs b/fixed-price-sale/program/tests/resume_market.rs index 259964b7e4..66c5c12605 100644 --- a/fixed-price-sale/program/tests/resume_market.rs +++ b/fixed-price-sale/program/tests/resume_market.rs @@ -39,6 +39,7 @@ mod resume_market { None, true, false, + 1, ) .await; @@ -215,6 +216,7 @@ mod resume_market { None, true, false, + 1, ) .await; @@ -387,6 +389,7 @@ mod resume_market { None, true, false, + 1, ) .await; @@ -565,6 +568,7 @@ mod resume_market { None, true, false, + 1, ) .await; diff --git a/fixed-price-sale/program/tests/suspend_market.rs b/fixed-price-sale/program/tests/suspend_market.rs index dc68f86ef6..73a2f7890a 100644 --- a/fixed-price-sale/program/tests/suspend_market.rs +++ b/fixed-price-sale/program/tests/suspend_market.rs @@ -39,6 +39,7 @@ mod suspend_market { None, true, false, + 1, ) .await; @@ -186,6 +187,7 @@ mod suspend_market { None, true, false, + 1, ) .await; @@ -330,6 +332,7 @@ mod suspend_market { None, true, false, + 1, ) .await; @@ -506,6 +509,7 @@ mod suspend_market { None, true, false, + 1, ) .await; @@ -679,6 +683,7 @@ mod suspend_market { None, true, false, + 1, ) .await; diff --git a/fixed-price-sale/program/tests/utils/helpers.rs b/fixed-price-sale/program/tests/utils/helpers.rs index d04d7fe2e8..5e58f88c28 100644 --- a/fixed-price-sale/program/tests/utils/helpers.rs +++ b/fixed-price-sale/program/tests/utils/helpers.rs @@ -4,13 +4,20 @@ use anchor_client::solana_sdk::{ pubkey::Pubkey, signer::{keypair::Keypair, Signer}, }; +use anchor_lang::{system_program, AccountDeserialize, InstructionData, ToAccountMetas}; +use mpl_fixed_price_sale::{ + accounts as mpl_fixed_price_sale_accounts, instruction as mpl_fixed_price_sale_instruction, + state::SellingResource, + utils::{find_edition_marker_pda, find_trade_history_address, find_vault_owner_address}, +}; use mpl_testing_utils::assert_error; -use mpl_token_metadata::state::Collection; -use solana_program::instruction::InstructionError; -use solana_program::{clock::Clock, system_instruction}; +use mpl_token_metadata::state::{Collection, Key}; +use solana_program::{clock::Clock, instruction::Instruction, system_instruction}; +use solana_program::{instruction::InstructionError, sysvar}; use solana_program_test::*; use solana_sdk::{ - commitment_config::CommitmentLevel, program_pack::Pack, transaction::Transaction, + account::AccountSharedData, commitment_config::CommitmentLevel, program_pack::Pack, + transaction::Transaction, }; use solana_sdk::{transaction::TransactionError, transport::TransportError}; use std::convert::TryFrom; @@ -109,7 +116,7 @@ pub async fn create_mint( &spl_token::id(), &mint.pubkey(), authority, - Some(&authority), + Some(authority), decimals, ) .unwrap(), @@ -466,3 +473,384 @@ pub fn unwrap_ignoring_io_error_in_ci(result: Result<(), BanksClientError>) { }, } } + +pub struct BuyManager<'a> { + pub context: &'a mut ProgramTestContext, + pub selling_resource_keypair: Keypair, + pub selling_resource: Option, + pub market_keypair: Keypair, + pub treasury_mint_keypair: Keypair, + pub treasury_holder_keypair: Keypair, + pub admin_wallet: Keypair, + pub user_token_account: Option, + pub trade_history: Option, + pub trade_history_bump: Option, + pub vault_owner_bump: Option, + pub vault_owner: Option, +} + +pub async fn buy_setup<'a>(manager: &mut BuyManager<'a>) -> Result<(), BanksClientError> { + let selling_resource_data = manager + .context + .banks_client + .get_account(manager.selling_resource_keypair.pubkey()) + .await + .unwrap() + .unwrap() + .data; + let selling_resource = + SellingResource::try_deserialize(&mut selling_resource_data.as_ref()).unwrap(); + + let (trade_history, trade_history_bump) = find_trade_history_address( + &manager.context.payer.pubkey(), + &manager.market_keypair.pubkey(), + ); + let (owner, vault_owner_bump) = + find_vault_owner_address(&selling_resource.resource, &selling_resource.store); + + let payer_pubkey = manager.context.payer.pubkey(); + + let user_token_account = Keypair::new(); + create_token_account( + manager.context, + &user_token_account, + &manager.treasury_mint_keypair.pubkey(), + &payer_pubkey, + ) + .await; + + manager.selling_resource = Some(selling_resource); + manager.user_token_account = Some(user_token_account); + manager.trade_history = Some(trade_history); + manager.trade_history_bump = Some(trade_history_bump); + manager.vault_owner_bump = Some(vault_owner_bump); + manager.vault_owner = Some(owner); + + println!("vault owner bump: {}", vault_owner_bump); + println!("vault owner: {}", owner); + // println!("manager owner: {}", manager.owner); + println!("trade history bump: {}", trade_history_bump); + println!("trade history: {}", trade_history); + + Ok(()) +} + +pub async fn buy_one<'a>( + manager: &mut BuyManager<'_>, + edition_marker: Option, +) -> Result<(), BanksClientError> { + let payer_pubkey = manager.context.payer.pubkey(); + + mint_to( + manager.context, + &manager.treasury_mint_keypair.pubkey(), + &manager.user_token_account.as_ref().unwrap().pubkey(), + &manager.admin_wallet, + 1_000_000, + ) + .await; + + let new_mint_keypair = Keypair::new(); + create_mint(manager.context, &new_mint_keypair, &payer_pubkey, 0).await; + + let new_mint_token_account = Keypair::new(); + create_token_account( + manager.context, + &new_mint_token_account, + &new_mint_keypair.pubkey(), + &payer_pubkey, + ) + .await; + + let payer_keypair = Keypair::from_bytes(&manager.context.payer.to_bytes()).unwrap(); + mint_to( + manager.context, + &new_mint_keypair.pubkey(), + &new_mint_token_account.pubkey(), + &payer_keypair, + 1, + ) + .await; + + let (master_edition_metadata, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + manager.selling_resource.as_ref().unwrap().resource.as_ref(), + ], + &mpl_token_metadata::id(), + ); + + let (master_edition, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + manager.selling_resource.as_ref().unwrap().resource.as_ref(), + mpl_token_metadata::state::EDITION.as_bytes(), + ], + &mpl_token_metadata::id(), + ); + + let selling_resource_data = manager + .context + .banks_client + .get_account(manager.selling_resource_keypair.pubkey()) + .await + .unwrap() + .unwrap() + .data; + let selling_resource = + SellingResource::try_deserialize(&mut selling_resource_data.as_ref()).unwrap(); + + let edition_num = if let Some(marker_num) = edition_marker { + marker_num * 248 + } else { + selling_resource.supply + 1 + }; + + let (edition_marker, _) = find_edition_marker_pda( + &manager.selling_resource.as_ref().unwrap().resource, + edition_num, + ); + + let (new_metadata, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + new_mint_keypair.pubkey().as_ref(), + ], + &mpl_token_metadata::id(), + ); + + let (new_edition, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + new_mint_keypair.pubkey().as_ref(), + mpl_token_metadata::state::EDITION.as_bytes(), + ], + &mpl_token_metadata::id(), + ); + + // Buy + let accounts = mpl_fixed_price_sale_accounts::Buy { + market: manager.market_keypair.pubkey(), + selling_resource: manager.selling_resource_keypair.pubkey(), + user_token_account: manager.user_token_account.as_ref().unwrap().pubkey(), + user_wallet: manager.context.payer.pubkey(), + trade_history: manager.trade_history.unwrap(), + treasury_holder: manager.treasury_holder_keypair.pubkey(), + new_metadata, + new_edition, + master_edition, + new_mint: new_mint_keypair.pubkey(), + edition_marker, + vault: manager.selling_resource.as_ref().unwrap().vault, + owner: manager.vault_owner.unwrap(), + new_token_account: new_mint_token_account.pubkey(), + master_edition_metadata, + clock: sysvar::clock::ID, + rent: sysvar::rent::ID, + token_metadata_program: mpl_token_metadata::ID, + token_program: spl_token::ID, + system_program: system_program::ID, + } + .to_account_metas(None); + + let data = mpl_fixed_price_sale_instruction::Buy { + _trade_history_bump: manager.trade_history_bump.unwrap(), + vault_owner_bump: manager.vault_owner_bump.unwrap(), + } + .data(); + + let instruction = Instruction { + program_id: mpl_fixed_price_sale::id(), + data, + accounts, + }; + + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&manager.context.payer.pubkey()), + &[&manager.context.payer], + manager.context.last_blockhash, + ); + + manager + .context + .banks_client + .process_transaction_with_commitment(tx, CommitmentLevel::Confirmed) + .await + .unwrap(); + + Ok(()) +} + +pub async fn buy_one_v2<'a>( + manager: &mut BuyManager<'_>, + edition_marker_number: u64, +) -> Result<(), BanksClientError> { + let payer_pubkey = manager.context.payer.pubkey(); + + mint_to( + manager.context, + &manager.treasury_mint_keypair.pubkey(), + &manager.user_token_account.as_ref().unwrap().pubkey(), + &manager.admin_wallet, + 1_000_000, + ) + .await; + + let new_mint_keypair = Keypair::new(); + create_mint(manager.context, &new_mint_keypair, &payer_pubkey, 0).await; + + let new_mint_token_account = Keypair::new(); + create_token_account( + manager.context, + &new_mint_token_account, + &new_mint_keypair.pubkey(), + &payer_pubkey, + ) + .await; + + let payer_keypair = Keypair::from_bytes(&manager.context.payer.to_bytes()).unwrap(); + mint_to( + manager.context, + &new_mint_keypair.pubkey(), + &new_mint_token_account.pubkey(), + &payer_keypair, + 1, + ) + .await; + + let (master_edition_metadata, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + manager.selling_resource.as_ref().unwrap().resource.as_ref(), + ], + &mpl_token_metadata::id(), + ); + + let (master_edition, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + manager.selling_resource.as_ref().unwrap().resource.as_ref(), + mpl_token_metadata::state::EDITION.as_bytes(), + ], + &mpl_token_metadata::id(), + ); + + let selling_resource_data = manager + .context + .banks_client + .get_account(manager.selling_resource_keypair.pubkey()) + .await + .unwrap() + .unwrap() + .data; + let selling_resource = + SellingResource::try_deserialize(&mut selling_resource_data.as_ref()).unwrap(); + + let edition_num = edition_marker_number * 248; + + let (edition_marker, _) = find_edition_marker_pda( + &manager.selling_resource.as_ref().unwrap().resource, + edition_num, + ); + + let (new_metadata, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + new_mint_keypair.pubkey().as_ref(), + ], + &mpl_token_metadata::id(), + ); + + let (new_edition, _) = Pubkey::find_program_address( + &[ + mpl_token_metadata::state::PREFIX.as_bytes(), + mpl_token_metadata::id().as_ref(), + new_mint_keypair.pubkey().as_ref(), + mpl_token_metadata::state::EDITION.as_bytes(), + ], + &mpl_token_metadata::id(), + ); + + // Buy + let accounts = mpl_fixed_price_sale_accounts::Buy { + market: manager.market_keypair.pubkey(), + selling_resource: manager.selling_resource_keypair.pubkey(), + user_token_account: manager.user_token_account.as_ref().unwrap().pubkey(), + user_wallet: manager.context.payer.pubkey(), + trade_history: manager.trade_history.unwrap(), + treasury_holder: manager.treasury_holder_keypair.pubkey(), + new_metadata, + new_edition, + master_edition, + new_mint: new_mint_keypair.pubkey(), + edition_marker, + vault: manager.selling_resource.as_ref().unwrap().vault, + owner: manager.vault_owner.unwrap(), + new_token_account: new_mint_token_account.pubkey(), + master_edition_metadata, + clock: sysvar::clock::ID, + rent: sysvar::rent::ID, + token_metadata_program: mpl_token_metadata::ID, + token_program: spl_token::ID, + system_program: system_program::ID, + } + .to_account_metas(None); + + let data = mpl_fixed_price_sale_instruction::BuyV2 { + _trade_history_bump: manager.trade_history_bump.unwrap(), + vault_owner_bump: manager.vault_owner_bump.unwrap(), + edition_marker_number, + } + .data(); + + let instruction = Instruction { + program_id: mpl_fixed_price_sale::id(), + data, + accounts, + }; + + let tx = Transaction::new_signed_with_payer( + &[instruction], + Some(&manager.context.payer.pubkey()), + &[&manager.context.payer], + manager.context.last_blockhash, + ); + + manager + .context + .banks_client + .process_transaction_with_commitment(tx, CommitmentLevel::Confirmed) + .await + .unwrap(); + + Ok(()) +} + +pub async fn fill_edition_marker(manager: &mut BuyManager<'_>, number: u64) { + let (edition_marker, _) = find_edition_marker_pda( + &manager.selling_resource.as_ref().unwrap().resource, + 248 * number, + ); + + let mut edition_marker_account = manager + .context + .banks_client + .get_account(edition_marker) + .await + .unwrap() + .unwrap(); + edition_marker_account.data[1..].fill(255); + + let shared_account: AccountSharedData = edition_marker_account.into(); + manager + .context + .set_account(&edition_marker, &shared_account); +} diff --git a/fixed-price-sale/program/tests/utils/setup_functions.rs b/fixed-price-sale/program/tests/utils/setup_functions.rs index b4f89ba4c6..41af29b79e 100644 --- a/fixed-price-sale/program/tests/utils/setup_functions.rs +++ b/fixed-price-sale/program/tests/utils/setup_functions.rs @@ -9,6 +9,7 @@ use mpl_fixed_price_sale::{ accounts as mpl_fixed_price_sale_accounts, instruction as mpl_fixed_price_sale_instruction, utils::{find_treasury_owner_address, find_vault_owner_address}, }; +use solana_program::pubkey::Pubkey; use solana_program_test::ProgramTestContext; use solana_sdk::{ commitment_config::CommitmentLevel, @@ -38,7 +39,7 @@ pub async fn setup_store(context: &mut ProgramTestContext) -> (Keypair, Keypair) let admin_wallet = Keypair::new(); let store_keypair = Keypair::new(); - airdrop(context, &admin_wallet.pubkey(), 10_000_000_000).await; + airdrop(context, &admin_wallet.pubkey(), 1_000_000_000_000).await; let name = "Test store".to_string(); let description = "Just a test store".to_string(); @@ -90,6 +91,7 @@ pub async fn setup_selling_resource( creators: Option>, selling_resource_owner_creator: bool, is_mutable: bool, + max_supply: u64, ) -> (Keypair, Keypair, Keypair) { let selling_resource_keypair = Keypair::new(); let selling_resource_owner_keypair = Keypair::new(); @@ -169,14 +171,14 @@ pub async fn setup_selling_resource( &actual_update_authority, admin_wallet, &metadata, - Some(1), + Some(max_supply), ) .await; airdrop( context, &selling_resource_owner_keypair.pubkey(), - 10_000_000_000, + 1_000_000_000_000, ) .await; @@ -200,7 +202,7 @@ pub async fn setup_selling_resource( let data = mpl_fixed_price_sale_instruction::InitSellingResource { master_edition_bump, vault_owner_bump, - max_supply: Some(1), + max_supply: Some(max_supply), } .data(); @@ -239,6 +241,7 @@ pub async fn setup_market( store_keypair: &Keypair, selling_resource_keypair: &Keypair, selling_resource_owner_keypair: &Keypair, + user_limit: Option, ) -> Keypair { let market_keypair = Keypair::new(); @@ -271,7 +274,6 @@ pub async fn setup_market( let description = "Marktbeschreibung".to_string(); let mutable = true; let price = 1_000_000; - let pieces_in_one_wallet = Some(1); let accounts = mpl_fixed_price_sale_accounts::CreateMarket { market: market_keypair.pubkey(), @@ -291,7 +293,7 @@ pub async fn setup_market( description: description.to_owned(), mutable, price, - pieces_in_one_wallet, + pieces_in_one_wallet: user_limit, start_date: start_date as u64, end_date: None, gating_config: None, diff --git a/fixed-price-sale/program/tests/withdraw.rs b/fixed-price-sale/program/tests/withdraw.rs index 05e4f02474..a3b9a2fc9e 100644 --- a/fixed-price-sale/program/tests/withdraw.rs +++ b/fixed-price-sale/program/tests/withdraw.rs @@ -48,6 +48,7 @@ mod withdraw { None, true, true, + 1, ) .await; @@ -479,6 +480,7 @@ mod withdraw { None, true, true, + 1, ) .await; @@ -874,6 +876,7 @@ mod withdraw { None, true, false, + 1, ) .await; @@ -1246,6 +1249,7 @@ mod withdraw { None, true, false, + 1, ) .await; @@ -1617,6 +1621,7 @@ mod withdraw { None, true, true, + 1, ) .await;