Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cnft] Cnft offer shared escrow remaining account fix #108

Merged
merged 1 commit into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ pub fn handler<'info>(
.checked_sub(1)
.ok_or(MMMErrorCode::NumericOverflow)?;

remaining_accounts[2..].split_at(creator_length + 2)
remaining_accounts[2..].split_at(creator_length)
} else {
remaining_accounts.split_at(creator_length)
};
Expand Down
285 changes: 273 additions & 12 deletions tests/mmm-cnft.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
getProofPath,
getSolFulfillBuyPrices,
IDL,
M2_PROGRAM,
Mmm,
MMMProgramID,
} from '../sdk/src';
Expand Down Expand Up @@ -59,6 +60,7 @@ async function createCNftCollectionOffer(
const poolData = await createPool(program, {
...poolArgs,
reinvestFulfillBuy: false,
reinvestFulfillSell: false,
buysideCreatorRoyaltyBp: 10_000,
});

Expand All @@ -68,18 +70,6 @@ async function createCNftCollectionOffer(
poolData.poolKey,
);

await program.methods
.solDepositBuy({ paymentAmount: new anchor.BN(10 * LAMPORTS_PER_SOL) })
.accountsStrict({
owner: poolArgs.owner,
cosigner: poolArgs.cosigner?.publicKey ?? poolArgs.owner,
pool: poolKey,
buysideSolEscrowAccount,
systemProgram: SystemProgram.programId,
})
.signers([...(poolArgs.cosigner ? [poolArgs.cosigner] : [])])
.rpc({ skipPreflight: true });

if (sharedEscrow) {
const sharedEscrowAccount = getM2BuyerSharedEscrow(poolArgs.owner).key;
await program.methods
Expand All @@ -94,6 +84,18 @@ async function createCNftCollectionOffer(
})
.signers([...(poolArgs.cosigner ? [poolArgs.cosigner] : [])])
.rpc();
} else {
await program.methods
.solDepositBuy({ paymentAmount: new anchor.BN(10 * LAMPORTS_PER_SOL) })
.accountsStrict({
owner: poolArgs.owner,
cosigner: poolArgs.cosigner?.publicKey ?? poolArgs.owner,
pool: poolKey,
buysideSolEscrowAccount,
systemProgram: SystemProgram.programId,
})
.signers([...(poolArgs.cosigner ? [poolArgs.cosigner] : [])])
.rpc({ skipPreflight: true });
}

return {
Expand All @@ -110,6 +112,7 @@ describe('cnft tests', () => {
let provider = new anchor.AnchorProvider(connection, buyer, {
commitment: 'confirmed',
});
const sharedEscrowAccount = getM2BuyerSharedEscrow(buyer.publicKey).key;

let umi: Umi;
const program = new anchor.Program(
Expand All @@ -124,6 +127,7 @@ describe('cnft tests', () => {
airdrop(connection, buyer.publicKey, 100);
airdrop(connection, seller.publicKey, 100);
airdrop(connection, cosigner.publicKey, 100);
airdrop(connection, sharedEscrowAccount, 100);
});

it('cnft fulfill buy - happy path', async () => {
Expand Down Expand Up @@ -365,6 +369,263 @@ describe('cnft tests', () => {
);
});

it('cnft fulfill buy - happy path shared escrow', async () => {
// 1. Create a tree.
const {
merkleTree,
sellerProof, //already truncated
leafIndex,
metadata,
getBubblegumTreeRef,
getCnftRef,
nft,
creatorRoyalties,
collectionKey,
} = await setupTree(
umi,
publicKey(seller.publicKey),
DEFAULT_TEST_SETUP_TREE_PARAMS,
);

// 2. Create a shared escrow offer.
const { buysideSolEscrowAccount, poolData } =
await createCNftCollectionOffer(
program,
{
owner: new PublicKey(buyer.publicKey),
cosigner,
allowlists: [
{
kind: AllowlistKind.mcc,
value: collectionKey,
},
...getEmptyAllowLists(5),
],
},
true, // shared escrow
1, // shared escrow count
);

const [treeAuthority, _] = getBubblegumAuthorityPDA(
new PublicKey(nft.tree.merkleTree),
);

const [assetId, bump] = findLeafAssetIdPda(umi, {
merkleTree,
leafIndex,
});

const { key: sellState } = getMMMSellStatePDA(
program.programId,
poolData.poolKey,
new PublicKey(assetId),
);

const spotPrice = 1;
const expectedBuyPrices = getSolFulfillBuyPrices({
totalPriceLamports: spotPrice * LAMPORTS_PER_SOL,
lpFeeBp: 0,
takerFeeBp: 100,
metadataRoyaltyBp: 500,
buysideCreatorRoyaltyBp: 10_000,
makerFeeBp: 0,
});

const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress(
connection,
nft.tree.merkleTree,
);

const proofPath: AccountMeta[] = getProofPath(
nft.nft.fullProof,
treeAccount.getCanopyDepth(),
);

const {
accounts: creatorAccounts,
creatorShares,
creatorVerified,
sellerFeeBasisPoints,
} = getCreatorRoyaltiesArgs(creatorRoyalties);

// get balances before fulfill buy
const [
buyerBefore,
sellerBefore,
sharedEscrowAccountBalanceBefore,
creator1Before,
creator2Before,
] = await Promise.all([
connection.getBalance(buyer.publicKey),
connection.getBalance(seller.publicKey),
connection.getBalance(sharedEscrowAccount),
connection.getBalance(creatorAccounts[0].pubkey),
connection.getBalance(creatorAccounts[1].pubkey),
]);

try {
const metadataSerializer = getMetadataArgsSerializer();
const metadataArgs: MetadataArgs = metadataSerializer.deserialize(
metadataSerializer.serialize(metadata),
)[0];

const fulfillBuyTxnSig = await program.methods
.cnftFulfillBuy({
assetId: new PublicKey(assetId),
root: getByteArray(nft.tree.root),
nonce: new BN(nft.tree.nonce),
index: nft.nft.nftIndex,
minPaymentAmount: new BN(expectedBuyPrices.sellerReceives),
makerFeeBp: 0,
takerFeeBp: 100,
metadataArgs: {
name: metadataArgs.name,
symbol: metadataArgs.symbol,
uri: metadataArgs.uri,
sellerFeeBasisPoints: metadataArgs.sellerFeeBasisPoints,
primarySaleHappened: metadataArgs.primarySaleHappened,
isMutable: metadataArgs.isMutable,
editionNonce: isSome(metadataArgs.editionNonce)
? metadataArgs.editionNonce.value
: null,
tokenStandard: isSome(metadataArgs.tokenStandard)
? convertToDecodeTokenStandardEnum(
metadataArgs.tokenStandard.value,
)
: null,
collection: isSome(metadataArgs.collection)
? {
verified: metadataArgs.collection.value.verified,
key: new PublicKey(metadataArgs.collection.value.key),
}
: null, // Ensure it's a struct or null
uses: isSome(metadataArgs.uses)
? {
useMethod: convertToDecodeUseMethodEnum(
metadataArgs.uses.value.useMethod,
),
remaining: metadataArgs.uses.value.remaining,
total: metadataArgs.uses.value.total,
}
: null,
tokenProgramVersion: convertToDecodeTokenProgramVersion(
metadataArgs.tokenProgramVersion,
),
creators: metadataArgs.creators.map((c) => ({
address: new PublicKey(c.address),
verified: c.verified,
share: c.share,
})),
},
})
.accountsStrict({
payer: new PublicKey(seller.publicKey),
owner: buyer.publicKey,
cosigner: cosigner.publicKey,
referral: poolData.referral.publicKey,
pool: poolData.poolKey,
buysideSolEscrowAccount,
treeAuthority,
merkleTree: nft.tree.merkleTree,
logWrapper: SPL_NOOP_PROGRAM_ID,
bubblegumProgram: MPL_BUBBLEGUM_PROGRAM_ID,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
sellState,
systemProgram: SystemProgram.programId,
})
.remainingAccounts([
{
pubkey: M2_PROGRAM,
isSigner: false,
isWritable: false,
},
{
pubkey: sharedEscrowAccount,
isWritable: true,
isSigner: false,
},
...creatorAccounts,
...proofPath,
])
.signers([cosigner, seller.payer])
// note: skipPreflight causes some weird error.
// so just surround in this try-catch to get the logs
.rpc(/* { skipPreflight: true } */);
const tx = await connection.getParsedTransaction(fulfillBuyTxnSig, {
maxSupportedTransactionVersion: 0,
});
console.log(`${JSON.stringify(tx)}`);
} catch (e) {
if (e instanceof SendTransactionError) {
const err = e as SendTransactionError;
console.log(
`err.logs: ${JSON.stringify(
await err.getLogs(provider.connection),
null,
2,
)}`,
);
}
throw e;
}

// Verify that buyer now owns the cNFT.
await verifyOwnership(
umi,
merkleTree,
publicKey(buyer.publicKey),
leafIndex,
metadata,
[],
);

// Get balances after fulfill buy
const [
buyerAfter,
sellerAfter,
sharedEscrowAccountBalanceAfter,
creator1After,
creator2After,
] = await Promise.all([
connection.getBalance(buyer.publicKey),
connection.getBalance(seller.publicKey),
connection.getBalance(sharedEscrowAccount),
connection.getBalance(creatorAccounts[0].pubkey),
connection.getBalance(creatorAccounts[1].pubkey),
]);

assert.equal(
sharedEscrowAccountBalanceBefore,
sharedEscrowAccountBalanceAfter + spotPrice * LAMPORTS_PER_SOL,
);

assert.equal(
sellerAfter,
sellerBefore +
spotPrice * LAMPORTS_PER_SOL -
expectedBuyPrices.takerFeePaid.toNumber() -
expectedBuyPrices.royaltyPaid.toNumber(),
);

assertIsBetween(
creator1After,
creator1Before +
(expectedBuyPrices.royaltyPaid.toNumber() *
metadata.creators[0].share) /
100,
PRICE_ERROR_RANGE,
);

assertIsBetween(
creator2After,
creator2Before +
(expectedBuyPrices.royaltyPaid.toNumber() *
metadata.creators[1].share) /
100,
PRICE_ERROR_RANGE,
);
});

it('cnft fulfill buy - incorrect collection fail allowlist check', async () => {
// 1. Create a tree.
const {
Expand Down
Loading