diff --git a/clients/js/src/createTree.ts b/clients/js/src/createTree.ts index bc6eb227..3aa780c8 100644 --- a/clients/js/src/createTree.ts +++ b/clients/js/src/createTree.ts @@ -27,6 +27,18 @@ export const createTree = async ( getMerkleTreeSize(input.maxDepth, input.maxBufferSize, input.canopyDepth); const lamports = await context.rpc.getRent(space); + let programId; + if (input.compressionProgram) { + programId = Array.isArray(input.compressionProgram) + ? input.compressionProgram[0] + : input.compressionProgram; + } else { + programId = context.programs.getPublicKey( + 'splAccountCompression', + SPL_ACCOUNT_COMPRESSION_PROGRAM_ID + ); + } + return ( transactionBuilder() // Create the empty Merkle tree account. @@ -36,10 +48,7 @@ export const createTree = async ( newAccount: input.merkleTree, lamports, space, - programId: context.programs.getPublicKey( - 'splAccountCompression', - SPL_ACCOUNT_COMPRESSION_PROGRAM_ID - ), + programId, }) ) // Create the tree config. diff --git a/clients/js/src/generated/errors/mplBubblegum.ts b/clients/js/src/generated/errors/mplBubblegum.ts index 87e50c90..ac191f2d 100644 --- a/clients/js/src/generated/errors/mplBubblegum.ts +++ b/clients/js/src/generated/errors/mplBubblegum.ts @@ -604,6 +604,32 @@ export class InvalidCanopySizeError extends ProgramError { codeToErrorMap.set(0x1799, InvalidCanopySizeError); nameToErrorMap.set('InvalidCanopySize', InvalidCanopySizeError); +/** InvalidLogWrapper: Invalid log wrapper program */ +export class InvalidLogWrapperError extends ProgramError { + readonly name: string = 'InvalidLogWrapper'; + + readonly code: number = 0x179a; // 6042 + + constructor(program: Program, cause?: Error) { + super('Invalid log wrapper program', program, cause); + } +} +codeToErrorMap.set(0x179a, InvalidLogWrapperError); +nameToErrorMap.set('InvalidLogWrapper', InvalidLogWrapperError); + +/** InvalidCompressionProgram: Invalid compression program */ +export class InvalidCompressionProgramError extends ProgramError { + readonly name: string = 'InvalidCompressionProgram'; + + readonly code: number = 0x179b; // 6043 + + constructor(program: Program, cause?: Error) { + super('Invalid compression program', program, cause); + } +} +codeToErrorMap.set(0x179b, InvalidCompressionProgramError); +nameToErrorMap.set('InvalidCompressionProgram', InvalidCompressionProgramError); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/clients/js/test/createTree.test.ts b/clients/js/test/createTree.test.ts index e40a6fb7..7eaccc56 100644 --- a/clients/js/test/createTree.test.ts +++ b/clients/js/test/createTree.test.ts @@ -1,8 +1,75 @@ -import { generateSigner, publicKey } from '@metaplex-foundation/umi'; +import { createAccount } from '@metaplex-foundation/mpl-toolbox'; +import { + generateSigner, + publicKey, + Context, + Signer, + TransactionBuilder, + transactionBuilder, + PublicKey, +} from '@metaplex-foundation/umi'; import test from 'ava'; -import { TreeConfig, createTree, fetchTreeConfigFromSeeds } from '../src'; +import { + TreeConfig, + createTree, + createTreeConfig, + fetchTreeConfigFromSeeds, + safeFetchTreeConfigFromSeeds, + getMerkleTreeSize, + SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, +} from '../src'; import { createUmi } from './_setup'; +const createTreeWithSpecificMerkleOwner = async ( + context: Parameters[0] & + Parameters[0] & + Pick, + input: Omit[1], 'merkleTree'> & { + merkleTree: Signer; + merkleTreeSize?: number; + canopyDepth?: number; + merkleTreeOwner?: PublicKey; + } +): Promise => { + const space = + input.merkleTreeSize ?? + getMerkleTreeSize(input.maxDepth, input.maxBufferSize, input.canopyDepth); + const lamports = await context.rpc.getRent(space); + + let programId; + if (input.compressionProgram) { + programId = Array.isArray(input.compressionProgram) + ? input.compressionProgram[0] + : input.compressionProgram; + } else { + programId = context.programs.getPublicKey( + 'splAccountCompression', + SPL_ACCOUNT_COMPRESSION_PROGRAM_ID + ); + } + + return ( + transactionBuilder() + // Create the empty Merkle tree account. + .add( + createAccount(context, { + payer: input.payer ?? context.payer, + newAccount: input.merkleTree, + lamports, + space, + programId: input.merkleTreeOwner ? input.merkleTreeOwner : programId, + }) + ) + // Create the tree config. + .add( + createTreeConfig(context, { + ...input, + merkleTree: input.merkleTree.publicKey, + }) + ) + ); +}; + test('it can create a Bubblegum tree', async (t) => { // Given a brand new merkle tree signer. const umi = await createUmi(); @@ -60,3 +127,185 @@ test('it can create a Bubblegum tree using a newer size', async (t) => { isPublic: false, }); }); + +test('it can create a Bubblegum tree using mpl-account-compression and mpl-noop', async (t) => { + // Given a brand new merkle tree signer. + const umi = await createUmi(); + const merkleTree = generateSigner(umi); + + // When we create a tree at this address. + const builder = await createTree(umi, { + merkleTree, + maxDepth: 14, + maxBufferSize: 64, + logWrapper: publicKey('mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3'), + compressionProgram: publicKey( + 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW' + ), + }); + await builder.sendAndConfirm(umi); + + // Then an account exists at the merkle tree address. + t.true(await umi.rpc.accountExists(merkleTree.publicKey)); + + // And a tree config was created with the correct data. + const treeConfig = await fetchTreeConfigFromSeeds(umi, { + merkleTree: merkleTree.publicKey, + }); + t.like(treeConfig, { + treeCreator: publicKey(umi.identity), + treeDelegate: publicKey(umi.identity), + totalMintCapacity: 2n ** 14n, + numMinted: 0n, + isPublic: false, + }); +}); + +test('it cannot create a Bubblegum tree using invalid logWrapper with spl-account-compression', async (t) => { + // Given a brand new merkle tree signer. + const umi = await createUmi(); + const merkleTree = generateSigner(umi); + + // When we create a tree at this address. + const builder = await createTree(umi, { + merkleTree, + maxDepth: 14, + maxBufferSize: 64, + logWrapper: generateSigner(umi).publicKey, + }); + + const promise = builder.sendAndConfirm(umi); + + // Then we expect a program error. + await t.throwsAsync(promise, { name: 'InvalidLogWrapper' }); + + // And an account does not exist at the merkle tree address. + t.false(await umi.rpc.accountExists(merkleTree.publicKey)); + + // And a tree config was not created with the correct data. + const treeConfig = await safeFetchTreeConfigFromSeeds(umi, { + merkleTree: merkleTree.publicKey, + }); + t.is(treeConfig, null); +}); + +test('it cannot create a Bubblegum tree using invalid logWrapper with mpl-account-compression', async (t) => { + // Given a brand new merkle tree signer. + const umi = await createUmi(); + const merkleTree = generateSigner(umi); + + // When we create a tree at this address. + const builder = await createTree(umi, { + merkleTree, + maxDepth: 14, + maxBufferSize: 64, + logWrapper: generateSigner(umi).publicKey, + compressionProgram: publicKey( + 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW' + ), + }); + + const promise = builder.sendAndConfirm(umi); + + // Then we expect a program error. + await t.throwsAsync(promise, { name: 'InvalidLogWrapper' }); + + // And an account does not exist at the merkle tree address. + t.false(await umi.rpc.accountExists(merkleTree.publicKey)); + + // And a tree config was not created with the correct data. + const treeConfig = await safeFetchTreeConfigFromSeeds(umi, { + merkleTree: merkleTree.publicKey, + }); + t.is(treeConfig, null); +}); + +test('it cannot create a Bubblegum tree when compression program does not match tree owned by spl-account-compression', async (t) => { + // Given a brand new merkle tree signer. + const umi = await createUmi(); + const merkleTree = generateSigner(umi); + + // When we create a tree at this address. + const builder = await createTreeWithSpecificMerkleOwner(umi, { + merkleTree, + maxDepth: 14, + maxBufferSize: 64, + compressionProgram: generateSigner(umi).publicKey, + merkleTreeOwner: umi.programs.getPublicKey( + 'splAccountCompression', + SPL_ACCOUNT_COMPRESSION_PROGRAM_ID + ), + }); + + const promise = builder.sendAndConfirm(umi); + + // Then we expect a program error. + await t.throwsAsync(promise, { name: 'InvalidCompressionProgram' }); + + // And an account does not exist at the merkle tree address. + t.false(await umi.rpc.accountExists(merkleTree.publicKey)); + + // And a tree config was not created with the correct data. + const treeConfig = await safeFetchTreeConfigFromSeeds(umi, { + merkleTree: merkleTree.publicKey, + }); + t.is(treeConfig, null); +}); + +test('it cannot create a Bubblegum tree when compression program does not match tree owned by mpl-account-compression', async (t) => { + // Given a brand new merkle tree signer. + const umi = await createUmi(); + const merkleTree = generateSigner(umi); + + // When we create a tree at this address. + const builder = await createTreeWithSpecificMerkleOwner(umi, { + merkleTree, + maxDepth: 14, + maxBufferSize: 64, + logWrapper: publicKey('mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3'), + compressionProgram: generateSigner(umi).publicKey, + merkleTreeOwner: publicKey('mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW'), + }); + + const promise = builder.sendAndConfirm(umi); + + // Then we expect a program error. + await t.throwsAsync(promise, { name: 'InvalidCompressionProgram' }); + + // And an account does not exist at the merkle tree address. + t.false(await umi.rpc.accountExists(merkleTree.publicKey)); + + // And a tree config was not created with the correct data. + const treeConfig = await safeFetchTreeConfigFromSeeds(umi, { + merkleTree: merkleTree.publicKey, + }); + t.is(treeConfig, null); +}); + +test('it cannot create a Bubblegum tree with incorrect Merkle tree owner', async (t) => { + // Given a brand new merkle tree signer. + const umi = await createUmi(); + const merkleTree = generateSigner(umi); + + // When we create a tree at this address. + const builder = await createTreeWithSpecificMerkleOwner(umi, { + merkleTree, + maxDepth: 14, + maxBufferSize: 64, + merkleTreeOwner: generateSigner(umi).publicKey, + }); + + const promise = builder.sendAndConfirm(umi); + + // Then we expect a program error. + await t.throwsAsync(promise, { name: 'IncorrectOwner' }); + + // And an account does not exist at the merkle tree address. + t.false(await umi.rpc.accountExists(merkleTree.publicKey)); + + // And a tree config was not created with the correct data. + const treeConfig = await safeFetchTreeConfigFromSeeds(umi, { + merkleTree: merkleTree.publicKey, + }); + t.is(treeConfig, null); +}); diff --git a/clients/js/test/transfer.test.ts b/clients/js/test/transfer.test.ts index e2ad78f9..4f8366c2 100644 --- a/clients/js/test/transfer.test.ts +++ b/clients/js/test/transfer.test.ts @@ -55,6 +55,140 @@ test('it can transfer a compressed NFT', async (t) => { t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(updatedLeaf)); }); +test('it can transfer a compressed NFT using mpl-account-compression and mpl-noop', async (t) => { + // Given a tree with a minted NFT owned by leafOwnerA. + const umi = await createUmi(); + const merkleTree = await createTree(umi, { + logWrapper: publicKey('mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3'), + compressionProgram: publicKey( + 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW' + ), + }); + let merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + const leafOwnerA = generateSigner(umi); + const { metadata, leafIndex } = await mint(umi, { + merkleTree, + leafOwner: leafOwnerA.publicKey, + logWrapper: publicKey('mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3'), + compressionProgram: publicKey( + 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW' + ), + }); + + // When leafOwnerA transfers the NFT to leafOwnerB. + const leafOwnerB = generateSigner(umi); + await transfer(umi, { + leafOwner: leafOwnerA, + newLeafOwner: leafOwnerB.publicKey, + merkleTree, + root: getCurrentRoot(merkleTreeAccount.tree), + dataHash: hashMetadataData(metadata), + creatorHash: hashMetadataCreators(metadata.creators), + nonce: leafIndex, + index: leafIndex, + proof: [], + logWrapper: publicKey('mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3'), + compressionProgram: publicKey( + 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW' + ), + }).sendAndConfirm(umi); + + // Then the leaf was updated in the merkle tree. + const updatedLeaf = hashLeaf(umi, { + merkleTree, + owner: leafOwnerB.publicKey, + leafIndex, + metadata, + }); + merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(updatedLeaf)); +}); + +test('it cannot transfer a compressed NFT owned by spl-account-compression using mpl programs', async (t) => { + // Given a tree with a minted NFT owned by leafOwnerA. + const umi = await createUmi(); + const merkleTree = await createTree(umi); + let merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + const leafOwnerA = generateSigner(umi); + const { metadata, leafIndex } = await mint(umi, { + merkleTree, + leafOwner: leafOwnerA.publicKey, + }); + + // When leafOwnerA transfers the NFT to leafOwnerB. + const leafOwnerB = generateSigner(umi); + const promise = transfer(umi, { + leafOwner: leafOwnerA, + newLeafOwner: leafOwnerB.publicKey, + merkleTree, + root: getCurrentRoot(merkleTreeAccount.tree), + dataHash: hashMetadataData(metadata), + creatorHash: hashMetadataCreators(metadata.creators), + nonce: leafIndex, + index: leafIndex, + proof: [], + logWrapper: publicKey('mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3'), + compressionProgram: publicKey( + 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW' + ), + }).sendAndConfirm(umi); + + // Then we expect a program error. + await t.throwsAsync(promise, { name: 'InvalidLogWrapper' }); + + // Then the leaf was not updated in the merkle tree. + const originalLeaf = hashLeaf(umi, { + merkleTree, + owner: leafOwnerA.publicKey, + leafIndex, + metadata, + }); + merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(originalLeaf)); +}); + +test('it cannot transfer a compressed NFT owned by spl-account-compression using mpl-account-compression', async (t) => { + // Given a tree with a minted NFT owned by leafOwnerA. + const umi = await createUmi(); + const merkleTree = await createTree(umi); + let merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + const leafOwnerA = generateSigner(umi); + const { metadata, leafIndex } = await mint(umi, { + merkleTree, + leafOwner: leafOwnerA.publicKey, + }); + + // When leafOwnerA transfers the NFT to leafOwnerB. + const leafOwnerB = generateSigner(umi); + const promise = transfer(umi, { + leafOwner: leafOwnerA, + newLeafOwner: leafOwnerB.publicKey, + merkleTree, + root: getCurrentRoot(merkleTreeAccount.tree), + dataHash: hashMetadataData(metadata), + creatorHash: hashMetadataCreators(metadata.creators), + nonce: leafIndex, + index: leafIndex, + proof: [], + compressionProgram: publicKey( + 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW' + ), + }).sendAndConfirm(umi); + + // Then we expect a program error. + await t.throwsAsync(promise, { name: 'InvalidCompressionProgram' }); + + // Then the leaf was not updated in the merkle tree. + const originalLeaf = hashLeaf(umi, { + merkleTree, + owner: leafOwnerA.publicKey, + leafIndex, + metadata, + }); + merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(originalLeaf)); +}); + test('it can transfer a compressed NFT as a delegated authority', async (t) => { // Given a tree with a delegated compressed NFT owned by leafOwnerA. const umi = await createUmi(); diff --git a/clients/rust/src/generated/errors/mpl_bubblegum.rs b/clients/rust/src/generated/errors/mpl_bubblegum.rs index 87dc4019..9eba86ef 100644 --- a/clients/rust/src/generated/errors/mpl_bubblegum.rs +++ b/clients/rust/src/generated/errors/mpl_bubblegum.rs @@ -136,6 +136,12 @@ pub enum MplBubblegumError { /// 6041 (0x1799) - Canopy size should be set bigger for this tree #[error("Canopy size should be set bigger for this tree")] InvalidCanopySize, + /// 6042 (0x179A) - Invalid log wrapper program + #[error("Invalid log wrapper program")] + InvalidLogWrapper, + /// 6043 (0x179B) - Invalid compression program + #[error("Invalid compression program")] + InvalidCompressionProgram, } impl solana_program::program_error::PrintProgramError for MplBubblegumError { diff --git a/configs/scripts/program/dump.sh b/configs/scripts/program/dump.sh index 386df6c6..eabff05a 100755 --- a/configs/scripts/program/dump.sh +++ b/configs/scripts/program/dump.sh @@ -1,7 +1,9 @@ #!/bin/bash -EXTERNAL_ID=("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" "cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK" "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV") -EXTERNAL_SO=("mpl_token_metadata.so" "spl_account_compression.so" "spl_noop.so") +EXTERNAL_ID_MAINNET=("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" "cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK" "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV") +EXTERNAL_SO_MAINNET=("mpl_token_metadata.so" "spl_account_compression.so" "spl_noop.so") +EXTERNAL_ID_DEVNET=("mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW" "mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3") +EXTERNAL_SO_DEVNET=("mpl_account_compression.so" "mpl_noop.so") # output colours RED() { echo $'\e[1;31m'$1$'\e[0m'; } @@ -15,12 +17,12 @@ cd $(dirname $(dirname $(dirname $SCRIPT_DIR))) OUTPUT=$1 -if [ -z ${RPC+x} ]; then - RPC="https://api.mainnet-beta.solana.com" -fi +RPC_MAINNET="https://api.mainnet-beta.solana.com" +RPC_DEVNET="https://api.devnet.solana.com" if [ -z "$OUTPUT" ]; then echo "missing output directory" + cd ${CURRENT_DIR} exit 1 fi @@ -29,56 +31,93 @@ if [ ! -d ${OUTPUT} ]; then mkdir ${OUTPUT} fi -# only prints this if we have external programs -if [ ${#EXTERNAL_ID[@]} -gt 0 ]; then - echo "Dumping external accounts to '${OUTPUT}':" -fi - # copy external programs or accounts binaries from the chain copy_from_chain() { - ACCOUNT_TYPE=`echo $1 | cut -d. -f2` - PREFIX=$2 + RPC=$1 + ACCOUNT_ID=$2 + ACCOUNT_TYPE=`echo $3 | cut -d. -f2` + PREFIX=$4 case "$ACCOUNT_TYPE" in "bin") - solana account -u $RPC ${EXTERNAL_ID[$i]} -o ${OUTPUT}/$2$1 > /dev/null + solana account -u "$RPC" "$ACCOUNT_ID" -o ${OUTPUT}/$4$3 > /dev/null || { + echo $(RED "[ ERROR ] Failed to dump program '$ACCOUNT_ID'") + cd ${CURRENT_DIR} + exit 1 + } ;; "so") - solana program dump -u $RPC ${EXTERNAL_ID[$i]} ${OUTPUT}/$2$1 > /dev/null + solana program dump -u "$RPC" "$ACCOUNT_ID" ${OUTPUT}/$4$3 > /dev/null || { + echo $(RED "[ ERROR ] Failed to dump program '$ACCOUNT_ID'") + cd ${CURRENT_DIR} + exit 1 + } ;; *) - echo $(RED "[ ERROR ] unknown account type for '$1'") + echo $(RED "[ ERROR ] unknown account type for '$3'") + cd ${CURRENT_DIR} exit 1 ;; esac if [ -z "$PREFIX" ]; then - echo "Wrote account data to ${OUTPUT}/$2$1" + echo "Wrote account data to ${OUTPUT}/$4$3" fi } -# dump external programs binaries if needed -for i in ${!EXTERNAL_ID[@]}; do - if [ ! -f "${OUTPUT}/${EXTERNAL_SO[$i]}" ]; then - copy_from_chain "${EXTERNAL_SO[$i]}" +# only prints this if we have mainnet external programs +if [ ${#EXTERNAL_ID_MAINNET[@]} -gt 0 ]; then + echo "Dumping external accounts from mainnet to '${OUTPUT}':" +fi + +# dump mainnet external programs binaries if needed +for i in ${!EXTERNAL_ID_MAINNET[@]}; do + if [ ! -f "${OUTPUT}/${EXTERNAL_SO_MAINNET[$i]}" ]; then + copy_from_chain $RPC_MAINNET "${EXTERNAL_ID_MAINNET[$i]}" "${EXTERNAL_SO_MAINNET[$i]}" + else + copy_from_chain $RPC_MAINNET "${EXTERNAL_ID_MAINNET[$i]}" "${EXTERNAL_SO_MAINNET[$i]}" "onchain-" + + ON_CHAIN=`sha256sum -b ${OUTPUT}/onchain-${EXTERNAL_SO_MAINNET[$i]} | cut -d ' ' -f 1` + LOCAL=`sha256sum -b ${OUTPUT}/${EXTERNAL_SO_MAINNET[$i]} | cut -d ' ' -f 1` + + if [ "$ON_CHAIN" != "$LOCAL" ]; then + echo $(YLW "[ WARNING ] on-chain and local binaries are different for '${EXTERNAL_SO_MAINNET[$i]}'") + else + echo "$(GRN "[ SKIPPED ]") on-chain and local binaries are the same for '${EXTERNAL_SO_MAINNET[$i]}'" + fi + + rm ${OUTPUT}/onchain-${EXTERNAL_SO_MAINNET[$i]} + fi +done + +# only prints this if we have devnet external programs +if [ ${#EXTERNAL_ID_DEVNET[@]} -gt 0 ]; then + echo "" + echo "Dumping external accounts from devnet to '${OUTPUT}':" +fi + +# dump devnet external programs binaries if needed +for i in ${!EXTERNAL_ID_DEVNET[@]}; do + if [ ! -f "${OUTPUT}/${EXTERNAL_SO_DEVNET[$i]}" ]; then + copy_from_chain $RPC_DEVNET "${EXTERNAL_ID_DEVNET[$i]}" "${EXTERNAL_SO_DEVNET[$i]}" else - copy_from_chain "${EXTERNAL_SO[$i]}" "onchain-" + copy_from_chain $RPC_DEVNET "${EXTERNAL_ID_DEVNET[$i]}" "${EXTERNAL_SO_DEVNET[$i]}" "onchain-" - ON_CHAIN=`sha256sum -b ${OUTPUT}/onchain-${EXTERNAL_SO[$i]} | cut -d ' ' -f 1` - LOCAL=`sha256sum -b ${OUTPUT}/${EXTERNAL_SO[$i]} | cut -d ' ' -f 1` + ON_CHAIN=`sha256sum -b ${OUTPUT}/onchain-${EXTERNAL_SO_DEVNET[$i]} | cut -d ' ' -f 1` + LOCAL=`sha256sum -b ${OUTPUT}/${EXTERNAL_SO_DEVNET[$i]} | cut -d ' ' -f 1` if [ "$ON_CHAIN" != "$LOCAL" ]; then - echo $(YLW "[ WARNING ] on-chain and local binaries are different for '${EXTERNAL_SO[$i]}'") + echo $(YLW "[ WARNING ] on-chain and local binaries are different for '${EXTERNAL_SO_DEVNET[$i]}'") else - echo "$(GRN "[ SKIPPED ]") on-chain and local binaries are the same for '${EXTERNAL_SO[$i]}'" + echo "$(GRN "[ SKIPPED ]") on-chain and local binaries are the same for '${EXTERNAL_SO_DEVNET[$i]}'" fi - rm ${OUTPUT}/onchain-${EXTERNAL_SO[$i]} + rm ${OUTPUT}/onchain-${EXTERNAL_SO_DEVNET[$i]} fi done # only prints this if we have external programs -if [ ${#EXTERNAL_ID[@]} -gt 0 ]; then +if [ ${#EXTERNAL_ID_MAINNET[@]} -gt 0 ] || [ ${#EXTERNAL_ID_DEVNET[@]} -gt 0 ]; then echo "" fi diff --git a/configs/validator.cjs b/configs/validator.cjs index d4761550..f96bbfb1 100755 --- a/configs/validator.cjs +++ b/configs/validator.cjs @@ -10,16 +10,21 @@ module.exports = { validator: { commitment: "processed", programs: [ + { + label: "MPL Account Compression", + programId: "mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW", + deployPath: getProgram("mpl_account_compression.so"), + }, + { + label: "MPL Noop", + programId: "mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3", + deployPath: getProgram("mpl_noop.so"), + }, { label: "Mpl Bubblegum", programId: "BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY", deployPath: getProgram("bubblegum.so"), }, - { - label: "Token Metadata", - programId: "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", - deployPath: getProgram("mpl_token_metadata.so"), - }, { label: "SPL Account Compression", programId: "cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK", @@ -30,6 +35,11 @@ module.exports = { programId: "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV", deployPath: getProgram("spl_noop.so"), }, + { + label: "Token Metadata", + programId: "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", + deployPath: getProgram("mpl_token_metadata.so"), + }, ], }, }; diff --git a/idls/bubblegum.json b/idls/bubblegum.json index 070afde2..f670a6f0 100644 --- a/idls/bubblegum.json +++ b/idls/bubblegum.json @@ -2285,6 +2285,16 @@ "code": 6041, "name": "InvalidCanopySize", "msg": "Canopy size should be set bigger for this tree" + }, + { + "code": 6042, + "name": "InvalidLogWrapper", + "msg": "Invalid log wrapper program" + }, + { + "code": 6043, + "name": "InvalidCompressionProgram", + "msg": "Invalid compression program" } ], "metadata": { diff --git a/programs/bubblegum/Cargo.lock b/programs/bubblegum/Cargo.lock index 04241678..207be02f 100644 --- a/programs/bubblegum/Cargo.lock +++ b/programs/bubblegum/Cargo.lock @@ -261,8 +261,8 @@ checksum = "6c4fd6e43b2ca6220d2ef1641539e678bfc31b6cc393cf892b373b5997b6a39a" dependencies = [ "anchor-lang", "solana-program", - "spl-associated-token-account 2.3.0", - "spl-token 4.0.0", + "spl-associated-token-account", + "spl-token", "spl-token-2022 0.9.0", ] @@ -839,6 +839,8 @@ dependencies = [ "anchor-spl", "async-trait", "bytemuck", + "mpl-account-compression", + "mpl-noop", "mpl-token-auth-rules", "mpl-token-metadata", "num-traits", @@ -846,11 +848,11 @@ dependencies = [ "solana-program-test", "solana-sdk", "spl-account-compression", - "spl-associated-token-account 1.1.3", + "spl-associated-token-account", "spl-concurrent-merkle-tree", "spl-merkle-tree-reference", "spl-noop", - "spl-token 3.5.0", + "spl-token", ] [[package]] @@ -871,9 +873,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.17.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" dependencies = [ "bytemuck_derive", ] @@ -2411,6 +2413,28 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "mpl-account-compression" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ad6a6ba15b2e880d43b94f40510f1b64f2981a843cfc7b8877919467d2ab1a" +dependencies = [ + "anchor-lang", + "bytemuck", + "mpl-noop", + "solana-program", + "spl-concurrent-merkle-tree", +] + +[[package]] +name = "mpl-noop" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "179556a9254920ca8b150b18728d2f3106879f710c1ef5a049a8f0d5b03eede5" +dependencies = [ + "solana-program", +] + [[package]] name = "mpl-token-auth-rules" version = "1.5.1" @@ -2602,15 +2626,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_enum" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" -dependencies = [ - "num_enum_derive 0.5.11", -] - [[package]] name = "num_enum" version = "0.6.1" @@ -2629,18 +2644,6 @@ dependencies = [ "num_enum_derive 0.7.3", ] -[[package]] -name = "num_enum_derive" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "num_enum_derive" version = "0.6.1" @@ -2659,7 +2662,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", "syn 2.0.76", @@ -3820,7 +3823,7 @@ dependencies = [ "serde_json", "solana-config-program", "solana-sdk", - "spl-token 4.0.0", + "spl-token", "spl-token-2022 1.0.0", "spl-token-group-interface", "spl-token-metadata-interface", @@ -4799,9 +4802,9 @@ dependencies = [ "serde_json", "solana-account-decoder", "solana-sdk", - "spl-associated-token-account 2.3.0", - "spl-memo 4.0.0", - "spl-token 4.0.0", + "spl-associated-token-account", + "spl-memo", + "spl-token", "spl-token-2022 1.0.0", "thiserror", ] @@ -4964,9 +4967,9 @@ dependencies = [ [[package]] name = "spl-account-compression" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602499d5fe3b60280239c4656a361b283c8c5f73f769c6cf41d2e8a151ce72db" +checksum = "2785042005954aec5d5db7fcb99a78754b222be906a89d10a3d66ebdbc8e9548" dependencies = [ "anchor-lang", "bytemuck", @@ -4975,22 +4978,6 @@ dependencies = [ "spl-noop", ] -[[package]] -name = "spl-associated-token-account" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978dba3bcbe88d0c2c58366c254d9ea41c5f73357e72fc0bdee4d6b5fc99c8f4" -dependencies = [ - "assert_matches", - "borsh 0.9.3", - "num-derive 0.3.3", - "num-traits", - "solana-program", - "spl-token 3.5.0", - "spl-token-2022 0.6.1", - "thiserror", -] - [[package]] name = "spl-associated-token-account" version = "2.3.0" @@ -5002,16 +4989,16 @@ dependencies = [ "num-derive 0.4.2", "num-traits", "solana-program", - "spl-token 4.0.0", + "spl-token", "spl-token-2022 1.0.0", "thiserror", ] [[package]] name = "spl-concurrent-merkle-tree" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5f45b971d82cbb0416fdffad3c9098f259545d54072e83a0a482f60f8f689" +checksum = "a14033366e14117679851c7759c3d66c6430a495f0523bd88076d3a275828931" dependencies = [ "bytemuck", "solana-program", @@ -5053,15 +5040,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "spl-memo" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0dc6f70db6bacea7ff25870b016a65ba1d1b6013536f08e4fd79a8f9005325" -dependencies = [ - "solana-program", -] - [[package]] name = "spl-memo" version = "4.0.0" @@ -5073,9 +5051,9 @@ dependencies = [ [[package]] name = "spl-merkle-tree-reference" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28437c617c7f0db6b7229a489239f3ea6160499542d9367fbca2fc5ec7744abb" +checksum = "70d540c0983d5214dbba3cc7f708e2f5ac7d99294e1f633fd36178a22434285d" dependencies = [ "solana-program", "thiserror", @@ -5156,21 +5134,6 @@ dependencies = [ "spl-type-length-value", ] -[[package]] -name = "spl-token" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e85e168a785e82564160dcb87b2a8e04cee9bfd1f4d488c729d53d6a4bd300d" -dependencies = [ - "arrayref", - "bytemuck", - "num-derive 0.3.3", - "num-traits", - "num_enum 0.5.11", - "solana-program", - "thiserror", -] - [[package]] name = "spl-token" version = "4.0.0" @@ -5186,24 +5149,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "spl-token-2022" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0043b590232c400bad5ee9eb983ced003d15163c4c5d56b090ac6d9a57457b47" -dependencies = [ - "arrayref", - "bytemuck", - "num-derive 0.3.3", - "num-traits", - "num_enum 0.5.11", - "solana-program", - "solana-zk-token-sdk", - "spl-memo 3.0.1", - "spl-token 3.5.0", - "thiserror", -] - [[package]] name = "spl-token-2022" version = "0.9.0" @@ -5217,9 +5162,9 @@ dependencies = [ "num_enum 0.7.3", "solana-program", "solana-zk-token-sdk", - "spl-memo 4.0.0", + "spl-memo", "spl-pod", - "spl-token 4.0.0", + "spl-token", "spl-token-metadata-interface", "spl-transfer-hook-interface 0.3.0", "spl-type-length-value", @@ -5240,9 +5185,9 @@ dependencies = [ "solana-program", "solana-security-txt", "solana-zk-token-sdk", - "spl-memo 4.0.0", + "spl-memo", "spl-pod", - "spl-token 4.0.0", + "spl-token", "spl-token-group-interface", "spl-token-metadata-interface", "spl-transfer-hook-interface 0.4.1", @@ -5570,18 +5515,18 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", diff --git a/programs/bubblegum/program/Cargo.toml b/programs/bubblegum/program/Cargo.toml index bc35227a..4680315d 100644 --- a/programs/bubblegum/program/Cargo.toml +++ b/programs/bubblegum/program/Cargo.toml @@ -23,11 +23,15 @@ default = [] anchor-lang = { version = "0.29.0", features = ["init-if-needed"] } anchor-spl = "0.29.0" bytemuck = "1.13.0" +mpl-account-compression = { version = "0.4.2", features = ["cpi"] } +mpl-noop = { version = "0.2.1", features = ["no-entrypoint"] } mpl-token-metadata = "4.1.2" num-traits = "0.2.15" solana-program = "~1.18.15" -spl-account-compression = { version = "0.3.1", features = ["cpi"] } +spl-account-compression = { version = "0.4.2", features = ["cpi"] } spl-associated-token-account = { version = ">= 1.1.3, < 3.0", features = ["no-entrypoint"] } +spl-concurrent-merkle-tree = { version = "0.4.1" } +spl-noop = { version = "0.2.0", features = ["no-entrypoint"] } spl-token = { version = ">= 3.5.0, < 5.0", features = ["no-entrypoint"] } [dev-dependencies] @@ -35,6 +39,4 @@ async-trait = "0.1.71" mpl-token-auth-rules = { version = "1.5.1", features = ["no-entrypoint"] } solana-program-test = "~1.18.15" solana-sdk = "~1.18.15" -spl-concurrent-merkle-tree = "0.3.0" -spl-merkle-tree-reference = "0.1.0" -spl-noop = { version = "0.2.0", features = ["no-entrypoint"] } +spl-merkle-tree-reference = "0.1.1" diff --git a/programs/bubblegum/program/src/error.rs b/programs/bubblegum/program/src/error.rs index 8b4fdce5..71ed4c56 100644 --- a/programs/bubblegum/program/src/error.rs +++ b/programs/bubblegum/program/src/error.rs @@ -88,6 +88,10 @@ pub enum BubblegumError { InvalidTokenStandard, #[msg("Canopy size should be set bigger for this tree")] InvalidCanopySize, + #[msg("Invalid log wrapper program")] + InvalidLogWrapper, + #[msg("Invalid compression program")] + InvalidCompressionProgram, } // Converts certain Token Metadata errors into Bubblegum equivalents diff --git a/programs/bubblegum/program/src/processor/burn.rs b/programs/bubblegum/program/src/processor/burn.rs index 2bde60e3..fe897ee6 100644 --- a/programs/bubblegum/program/src/processor/burn.rs +++ b/programs/bubblegum/program/src/processor/burn.rs @@ -1,10 +1,10 @@ use anchor_lang::prelude::*; -use spl_account_compression::{program::SplAccountCompression, Node, Noop}; +use spl_concurrent_merkle_tree::node::Node; use crate::{ error::BubblegumError, state::{leaf_schema::LeafSchema, TreeConfig}, - utils::{get_asset_id, replace_leaf}, + utils::{get_asset_id, replace_leaf, validate_ownership_and_programs}, }; #[derive(Accounts)] @@ -21,8 +21,10 @@ pub struct Burn<'info> { #[account(mut)] /// CHECK: This account is modified in the downstream program pub merkle_tree: UncheckedAccount<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, + /// CHECK: Program is verified in the instruction + pub log_wrapper: UncheckedAccount<'info>, + /// CHECK: Program is verified in the instruction + pub compression_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } @@ -34,6 +36,11 @@ pub(crate) fn burn<'info>( nonce: u64, index: u32, ) -> Result<()> { + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; let owner = ctx.accounts.leaf_owner.to_account_info(); let delegate = ctx.accounts.leaf_delegate.to_account_info(); diff --git a/programs/bubblegum/program/src/processor/cancel_redeem.rs b/programs/bubblegum/program/src/processor/cancel_redeem.rs index 67862d9e..70373059 100644 --- a/programs/bubblegum/program/src/processor/cancel_redeem.rs +++ b/programs/bubblegum/program/src/processor/cancel_redeem.rs @@ -1,11 +1,10 @@ use anchor_lang::prelude::*; -use spl_account_compression::{program::SplAccountCompression, wrap_application_data_v1, Noop}; use crate::{ asserts::assert_pubkey_equal, error::BubblegumError, state::{leaf_schema::LeafSchema, TreeConfig, Voucher, VOUCHER_PREFIX}, - utils::replace_leaf, + utils::{replace_leaf, validate_ownership_and_programs}, }; #[derive(Accounts)] @@ -32,8 +31,10 @@ pub struct CancelRedeem<'info> { bump )] pub voucher: Account<'info, Voucher>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, + /// CHECK: Program is verified in the instruction + pub log_wrapper: UncheckedAccount<'info>, + /// CHECK: Program is verified in the instruction + pub compression_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } @@ -41,6 +42,11 @@ pub(crate) fn cancel_redeem<'info>( ctx: Context<'_, '_, '_, 'info, CancelRedeem<'info>>, root: [u8; 32], ) -> Result<()> { + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; let voucher = &ctx.accounts.voucher; match ctx.accounts.voucher.leaf_schema { LeafSchema::V1 { owner, .. } => assert_pubkey_equal( @@ -51,7 +57,7 @@ pub(crate) fn cancel_redeem<'info>( }?; let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); - wrap_application_data_v1( + crate::utils::wrap_application_data_v1( voucher.leaf_schema.to_event().try_to_vec()?, &ctx.accounts.log_wrapper, )?; diff --git a/programs/bubblegum/program/src/processor/compress.rs b/programs/bubblegum/program/src/processor/compress.rs index ca7d9bd6..563e4b01 100644 --- a/programs/bubblegum/program/src/processor/compress.rs +++ b/programs/bubblegum/program/src/processor/compress.rs @@ -1,5 +1,4 @@ use anchor_lang::prelude::*; -use spl_account_compression::{program::SplAccountCompression, Noop}; use crate::state::metaplex_anchor::{MasterEdition, TokenMetadata}; @@ -30,8 +29,10 @@ pub struct Compress<'info> { pub master_edition: Box>, #[account(mut)] pub payer: Signer<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, + /// CHECK: + pub log_wrapper: UncheckedAccount<'info>, + /// CHECK: + pub compression_program: UncheckedAccount<'info>, /// CHECK: pub token_program: UncheckedAccount<'info>, /// CHECK: diff --git a/programs/bubblegum/program/src/processor/create_tree.rs b/programs/bubblegum/program/src/processor/create_tree.rs index 20151618..52270bab 100644 --- a/programs/bubblegum/program/src/processor/create_tree.rs +++ b/programs/bubblegum/program/src/processor/create_tree.rs @@ -1,17 +1,11 @@ -use bytemuck::cast_slice; - use anchor_lang::{prelude::*, system_program::System}; -use spl_account_compression::{ - program::SplAccountCompression, - state::{ - merkle_tree_get_size, ConcurrentMerkleTreeHeader, CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1, - }, - Node, Noop, -}; +use bytemuck::cast_slice; +use spl_concurrent_merkle_tree::node::Node; use crate::{ error::BubblegumError, state::{DecompressibleState, TreeConfig, TREE_AUTHORITY_SIZE}, + utils::validate_ownership_and_programs, }; pub const MAX_ACC_PROOFS_SIZE: u32 = 17; @@ -32,8 +26,10 @@ pub struct CreateTree<'info> { #[account(mut)] pub payer: Signer<'info>, pub tree_creator: Signer<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, + /// CHECK: Program is verified in the instruction + pub log_wrapper: UncheckedAccount<'info>, + /// CHECK: Program is verified in the instruction + pub compression_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } @@ -43,11 +39,17 @@ pub(crate) fn create_tree( max_buffer_size: u32, public: Option, ) -> Result<()> { - let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); - + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; + + // Note this uses spl-account-compression to check the canopy size, and is assumed + // to be a valid check for mpl-account-compression. check_canopy_size(&ctx, max_depth, max_buffer_size)?; - let seed = merkle_tree.key(); + let seed = ctx.accounts.merkle_tree.key(); let seeds = &[seed.as_ref(), &[ctx.bumps.tree_authority]]; let authority = &mut ctx.accounts.tree_authority; authority.set_inner(TreeConfig { @@ -59,16 +61,30 @@ pub(crate) fn create_tree( is_decompressible: DecompressibleState::Disabled, }); let authority_pda_signer = &[&seeds[..]]; - let cpi_ctx = CpiContext::new_with_signer( - ctx.accounts.compression_program.to_account_info(), - spl_account_compression::cpi::accounts::Initialize { - authority: ctx.accounts.tree_authority.to_account_info(), - merkle_tree, - noop: ctx.accounts.log_wrapper.to_account_info(), - }, - authority_pda_signer, - ); - spl_account_compression::cpi::init_empty_merkle_tree(cpi_ctx, max_depth, max_buffer_size) + + if ctx.accounts.compression_program.key == &spl_account_compression::id() { + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.compression_program.to_account_info(), + spl_account_compression::cpi::accounts::Initialize { + authority: ctx.accounts.tree_authority.to_account_info(), + merkle_tree: ctx.accounts.merkle_tree.to_account_info(), + noop: ctx.accounts.log_wrapper.to_account_info(), + }, + authority_pda_signer, + ); + spl_account_compression::cpi::init_empty_merkle_tree(cpi_ctx, max_depth, max_buffer_size) + } else { + let cpi_ctx = CpiContext::new_with_signer( + ctx.accounts.compression_program.to_account_info(), + mpl_account_compression::cpi::accounts::Initialize { + authority: ctx.accounts.tree_authority.to_account_info(), + merkle_tree: ctx.accounts.merkle_tree.to_account_info(), + noop: ctx.accounts.log_wrapper.to_account_info(), + }, + authority_pda_signer, + ); + mpl_account_compression::cpi::init_empty_merkle_tree(cpi_ctx, max_depth, max_buffer_size) + } } fn check_canopy_size( @@ -76,6 +92,10 @@ fn check_canopy_size( max_depth: u32, max_buffer_size: u32, ) -> Result<()> { + use spl_account_compression::state::{ + merkle_tree_get_size, ConcurrentMerkleTreeHeader, CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1, + }; + let merkle_tree_bytes = ctx.accounts.merkle_tree.data.borrow(); let (header_bytes, rest) = merkle_tree_bytes.split_at(CONCURRENT_MERKLE_TREE_HEADER_SIZE_V1); diff --git a/programs/bubblegum/program/src/processor/decompress.rs b/programs/bubblegum/program/src/processor/decompress.rs index 7ac03803..476f0bbe 100644 --- a/programs/bubblegum/program/src/processor/decompress.rs +++ b/programs/bubblegum/program/src/processor/decompress.rs @@ -9,7 +9,6 @@ use solana_program::{ program_pack::Pack, system_instruction, }; -use spl_account_compression::Noop; use spl_token::state::Mint; use crate::{ @@ -71,7 +70,8 @@ pub struct DecompressV1<'info> { pub token_metadata_program: Program<'info, MplTokenMetadata>, pub token_program: Program<'info, Token>, pub associated_token_program: Program<'info, AssociatedToken>, - pub log_wrapper: Program<'info, Noop>, + /// CHECK: Program is not used in the instruction + pub log_wrapper: UncheckedAccount<'info>, } pub(crate) fn decompress_v1(ctx: Context, metadata: MetadataArgs) -> Result<()> { diff --git a/programs/bubblegum/program/src/processor/delegate.rs b/programs/bubblegum/program/src/processor/delegate.rs index 8cd70244..8fe5a440 100644 --- a/programs/bubblegum/program/src/processor/delegate.rs +++ b/programs/bubblegum/program/src/processor/delegate.rs @@ -1,9 +1,8 @@ use anchor_lang::prelude::*; -use spl_account_compression::{program::SplAccountCompression, wrap_application_data_v1, Noop}; use crate::{ state::{leaf_schema::LeafSchema, TreeConfig}, - utils::{get_asset_id, replace_leaf}, + utils::{get_asset_id, replace_leaf, validate_ownership_and_programs}, }; #[derive(Accounts)] @@ -22,8 +21,10 @@ pub struct Delegate<'info> { #[account(mut)] /// CHECK: This account is modified in the downstream program pub merkle_tree: UncheckedAccount<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, + /// CHECK: Program is verified in the instruction + pub log_wrapper: UncheckedAccount<'info>, + /// CHECK: Program is verified in the instruction + pub compression_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } @@ -35,6 +36,12 @@ pub(crate) fn delegate<'info>( nonce: u64, index: u32, ) -> Result<()> { + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; + let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); let owner = ctx.accounts.leaf_owner.key(); let previous_delegate = ctx.accounts.previous_leaf_delegate.key(); @@ -57,7 +64,10 @@ pub(crate) fn delegate<'info>( creator_hash, ); - wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; + crate::utils::wrap_application_data_v1( + new_leaf.to_event().try_to_vec()?, + &ctx.accounts.log_wrapper, + )?; replace_leaf( &merkle_tree.key(), diff --git a/programs/bubblegum/program/src/processor/mint.rs b/programs/bubblegum/program/src/processor/mint.rs index 67a865b2..c1604023 100644 --- a/programs/bubblegum/program/src/processor/mint.rs +++ b/programs/bubblegum/program/src/processor/mint.rs @@ -1,13 +1,12 @@ use anchor_lang::prelude::*; use solana_program::keccak; -use spl_account_compression::{program::SplAccountCompression, wrap_application_data_v1, Noop}; use std::collections::HashSet; use crate::{ asserts::{assert_metadata_is_mpl_compatible, assert_metadata_token_standard}, error::BubblegumError, state::{leaf_schema::LeafSchema, metaplex_adapter::MetadataArgs, TreeConfig}, - utils::{append_leaf, get_asset_id}, + utils::{append_leaf, get_asset_id, validate_ownership_and_programs}, }; #[derive(Accounts)] @@ -27,13 +26,20 @@ pub struct MintV1<'info> { pub merkle_tree: UncheckedAccount<'info>, pub payer: Signer<'info>, pub tree_delegate: Signer<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, + /// CHECK: Program is verified in the instruction + pub log_wrapper: UncheckedAccount<'info>, + /// CHECK: Program is verified in the instruction + pub compression_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } pub(crate) fn mint_v1(ctx: Context, message: MetadataArgs) -> Result { - // TODO -> Separate V1 / V1 into seperate instructions + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; + let payer = ctx.accounts.payer.key(); let incoming_tree_delegate = ctx.accounts.tree_delegate.key(); let owner = ctx.accounts.leaf_owner.key(); @@ -94,7 +100,7 @@ pub(crate) fn process_mint_v1<'info>( authority_bump: u8, authority: &mut Account<'info, TreeConfig>, merkle_tree: &AccountInfo<'info>, - wrapper: &Program<'info, Noop>, + wrapper: &AccountInfo<'info>, compression_program: &AccountInfo<'info>, allow_verified_collection: bool, ) -> Result { @@ -150,15 +156,15 @@ pub(crate) fn process_mint_v1<'info>( creator_hash.to_bytes(), ); - wrap_application_data_v1(leaf.to_event().try_to_vec()?, wrapper)?; + crate::utils::wrap_application_data_v1(leaf.to_event().try_to_vec()?, wrapper)?; append_leaf( &merkle_tree.key(), authority_bump, - &compression_program.to_account_info(), + compression_program, &authority.to_account_info(), - &merkle_tree.to_account_info(), - &wrapper.to_account_info(), + merkle_tree, + wrapper, leaf.to_node(), )?; diff --git a/programs/bubblegum/program/src/processor/mint_to_collection.rs b/programs/bubblegum/program/src/processor/mint_to_collection.rs index 3f59d0d6..0e5e9d87 100644 --- a/programs/bubblegum/program/src/processor/mint_to_collection.rs +++ b/programs/bubblegum/program/src/processor/mint_to_collection.rs @@ -1,7 +1,5 @@ -use std::collections::HashSet; - use anchor_lang::prelude::*; -use spl_account_compression::{program::SplAccountCompression, Noop}; +use std::collections::HashSet; use crate::{ error::BubblegumError, @@ -9,6 +7,7 @@ use crate::{ leaf_schema::LeafSchema, metaplex_adapter::MetadataArgs, metaplex_anchor::TokenMetadata, TreeConfig, }, + utils::validate_ownership_and_programs, }; use super::{mint::process_mint_v1, process_collection_verification_mpl_only}; @@ -43,8 +42,10 @@ pub struct MintToCollectionV1<'info> { pub edition_account: UncheckedAccount<'info>, /// CHECK: This is no longer needed but kept for backwards compatibility. pub bubblegum_signer: UncheckedAccount<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, + /// CHECK: Program is verified in the instruction + pub log_wrapper: UncheckedAccount<'info>, + /// CHECK: Program is verified in the instruction + pub compression_program: UncheckedAccount<'info>, /// CHECK: This is no longer needed but kept for backwards compatibility. pub token_metadata_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, @@ -54,6 +55,12 @@ pub(crate) fn mint_to_collection_v1( ctx: Context, metadata_args: MetadataArgs, ) -> Result { + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; + let mut message = metadata_args; // TODO -> Separate V1 / V1 into seperate instructions let payer = ctx.accounts.payer.key(); diff --git a/programs/bubblegum/program/src/processor/mod.rs b/programs/bubblegum/program/src/processor/mod.rs index e3d5e113..b77c0174 100644 --- a/programs/bubblegum/program/src/processor/mod.rs +++ b/programs/bubblegum/program/src/processor/mod.rs @@ -1,7 +1,6 @@ use anchor_lang::prelude::*; use mpl_token_metadata::types::MetadataDelegateRole; use solana_program::{account_info::AccountInfo, pubkey::Pubkey}; -use spl_account_compression::wrap_application_data_v1; use crate::{ asserts::{assert_collection_membership, assert_has_collection_authority}, @@ -136,7 +135,10 @@ fn process_creator_verification<'info>( updated_creator_hash, ); - wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; + crate::utils::wrap_application_data_v1( + new_leaf.to_event().try_to_vec()?, + &ctx.accounts.log_wrapper, + )?; replace_leaf( &merkle_tree.key(), @@ -293,7 +295,10 @@ fn process_collection_verification<'info>( creator_hash, ); - wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; + crate::utils::wrap_application_data_v1( + new_leaf.to_event().try_to_vec()?, + &ctx.accounts.log_wrapper, + )?; replace_leaf( &merkle_tree.key(), diff --git a/programs/bubblegum/program/src/processor/redeem.rs b/programs/bubblegum/program/src/processor/redeem.rs index e01605f1..dff95f5d 100644 --- a/programs/bubblegum/program/src/processor/redeem.rs +++ b/programs/bubblegum/program/src/processor/redeem.rs @@ -1,5 +1,5 @@ use anchor_lang::prelude::*; -use spl_account_compression::{program::SplAccountCompression, Node, Noop}; +use spl_concurrent_merkle_tree::node::Node; use crate::{ error::BubblegumError, @@ -7,7 +7,7 @@ use crate::{ leaf_schema::LeafSchema, DecompressibleState, TreeConfig, Voucher, VOUCHER_PREFIX, VOUCHER_SIZE, }, - utils::{get_asset_id, replace_leaf}, + utils::{get_asset_id, replace_leaf, validate_ownership_and_programs}, }; #[derive(Accounts)] @@ -44,8 +44,10 @@ pub struct Redeem<'info> { bump )] pub voucher: Account<'info, Voucher>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, + /// CHECK: Program is verified in the instruction + pub log_wrapper: UncheckedAccount<'info>, + /// CHECK: Program is verified in the instruction + pub compression_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } @@ -57,6 +59,12 @@ pub(crate) fn redeem<'info>( nonce: u64, index: u32, ) -> Result<()> { + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; + if ctx.accounts.tree_authority.is_decompressible == DecompressibleState::Disabled { return Err(BubblegumError::DecompressionDisabled.into()); } diff --git a/programs/bubblegum/program/src/processor/set_and_verify_collection.rs b/programs/bubblegum/program/src/processor/set_and_verify_collection.rs index 07195923..a751caaf 100644 --- a/programs/bubblegum/program/src/processor/set_and_verify_collection.rs +++ b/programs/bubblegum/program/src/processor/set_and_verify_collection.rs @@ -4,6 +4,7 @@ use crate::{ error::BubblegumError, processor::{process_collection_verification, verify_collection::CollectionVerification}, state::metaplex_adapter::MetadataArgs, + utils::validate_ownership_and_programs, }; pub(crate) fn set_and_verify_collection<'info>( @@ -16,6 +17,12 @@ pub(crate) fn set_and_verify_collection<'info>( message: MetadataArgs, collection: Pubkey, ) -> Result<()> { + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; + let incoming_tree_delegate = &ctx.accounts.tree_delegate; let tree_creator = ctx.accounts.tree_authority.tree_creator; let tree_delegate = ctx.accounts.tree_authority.tree_delegate; diff --git a/programs/bubblegum/program/src/processor/transfer.rs b/programs/bubblegum/program/src/processor/transfer.rs index 1f1d7d24..3a38358c 100644 --- a/programs/bubblegum/program/src/processor/transfer.rs +++ b/programs/bubblegum/program/src/processor/transfer.rs @@ -1,10 +1,9 @@ use anchor_lang::prelude::*; -use spl_account_compression::{program::SplAccountCompression, wrap_application_data_v1, Noop}; use crate::{ error::BubblegumError, state::{leaf_schema::LeafSchema, TreeConfig}, - utils::{get_asset_id, replace_leaf}, + utils::{get_asset_id, replace_leaf, validate_ownership_and_programs}, }; #[derive(Accounts)] @@ -24,8 +23,10 @@ pub struct Transfer<'info> { #[account(mut)] /// CHECK: This account is modified in the downstream program pub merkle_tree: UncheckedAccount<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, + /// CHECK: Program is verified in the instruction + pub log_wrapper: UncheckedAccount<'info>, + /// CHECK: Program is verified in the instruction + pub compression_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } @@ -37,6 +38,12 @@ pub(crate) fn transfer<'info>( nonce: u64, index: u32, ) -> Result<()> { + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; + // TODO add back version to select hash schema let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); let owner = ctx.accounts.leaf_owner.to_account_info(); @@ -67,7 +74,10 @@ pub(crate) fn transfer<'info>( creator_hash, ); - wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; + crate::utils::wrap_application_data_v1( + new_leaf.to_event().try_to_vec()?, + &ctx.accounts.log_wrapper, + )?; replace_leaf( &merkle_tree.key(), diff --git a/programs/bubblegum/program/src/processor/unverify_collection.rs b/programs/bubblegum/program/src/processor/unverify_collection.rs index 3f769afe..2d4baf2f 100644 --- a/programs/bubblegum/program/src/processor/unverify_collection.rs +++ b/programs/bubblegum/program/src/processor/unverify_collection.rs @@ -3,6 +3,7 @@ use anchor_lang::prelude::*; use crate::{ processor::{process_collection_verification, verify_collection::CollectionVerification}, state::metaplex_adapter::MetadataArgs, + utils::validate_ownership_and_programs, }; pub(crate) fn unverify_collection<'info>( @@ -14,6 +15,12 @@ pub(crate) fn unverify_collection<'info>( index: u32, message: MetadataArgs, ) -> Result<()> { + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; + process_collection_verification( ctx, root, diff --git a/programs/bubblegum/program/src/processor/unverify_creator.rs b/programs/bubblegum/program/src/processor/unverify_creator.rs index 0c7c4252..08da9165 100644 --- a/programs/bubblegum/program/src/processor/unverify_creator.rs +++ b/programs/bubblegum/program/src/processor/unverify_creator.rs @@ -3,6 +3,7 @@ use anchor_lang::prelude::*; use crate::{ processor::{process_creator_verification, verify_creator::CreatorVerification}, state::metaplex_adapter::MetadataArgs, + utils::validate_ownership_and_programs, }; pub(crate) fn unverify_creator<'info>( @@ -14,6 +15,12 @@ pub(crate) fn unverify_creator<'info>( index: u32, message: MetadataArgs, ) -> Result<()> { + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; + process_creator_verification( ctx, root, diff --git a/programs/bubblegum/program/src/processor/update_metadata.rs b/programs/bubblegum/program/src/processor/update_metadata.rs index 9a7db643..556f3266 100644 --- a/programs/bubblegum/program/src/processor/update_metadata.rs +++ b/programs/bubblegum/program/src/processor/update_metadata.rs @@ -1,6 +1,5 @@ use anchor_lang::prelude::*; use mpl_token_metadata::types::MetadataDelegateRole; -use spl_account_compression::{program::SplAccountCompression, wrap_application_data_v1, Noop}; use crate::{ asserts::{assert_has_collection_authority, assert_metadata_is_mpl_compatible}, @@ -11,7 +10,9 @@ use crate::{ metaplex_anchor::TokenMetadata, TreeConfig, }, - utils::{get_asset_id, hash_creators, hash_metadata, replace_leaf}, + utils::{ + get_asset_id, hash_creators, hash_metadata, replace_leaf, validate_ownership_and_programs, + }, }; #[derive(Accounts)] @@ -40,8 +41,10 @@ pub struct UpdateMetadata<'info> { #[account(mut)] /// CHECK: This account is modified in the downstream program pub merkle_tree: UncheckedAccount<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, + /// CHECK: Program is verified in the instruction + pub log_wrapper: UncheckedAccount<'info>, + /// CHECK: Program is verified in the instruction + pub compression_program: UncheckedAccount<'info>, /// CHECK: This is no longer needed but kept for backwards compatibility. pub token_metadata_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, @@ -110,7 +113,7 @@ fn process_update_metadata<'info>( compression_program: &AccountInfo<'info>, tree_authority: &AccountInfo<'info>, tree_authority_bump: u8, - log_wrapper: &Program<'info, Noop>, + log_wrapper: &AccountInfo<'info>, remaining_accounts: &[AccountInfo<'info>], root: [u8; 32], current_metadata: MetadataArgs, @@ -203,7 +206,7 @@ fn process_update_metadata<'info>( updated_creator_hash, ); - wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, log_wrapper)?; + crate::utils::wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, log_wrapper)?; replace_leaf( &merkle_tree.key(), @@ -228,6 +231,12 @@ pub fn update_metadata<'info>( current_metadata: MetadataArgs, update_args: UpdateArgs, ) -> Result<()> { + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; + match ¤t_metadata.collection { // Verified collection case. Some(collection) if collection.verified => { diff --git a/programs/bubblegum/program/src/processor/verify_collection.rs b/programs/bubblegum/program/src/processor/verify_collection.rs index 58143754..030e34ba 100644 --- a/programs/bubblegum/program/src/processor/verify_collection.rs +++ b/programs/bubblegum/program/src/processor/verify_collection.rs @@ -1,7 +1,9 @@ use anchor_lang::prelude::*; -use spl_account_compression::{program::SplAccountCompression, Noop}; -use crate::state::{metaplex_adapter::MetadataArgs, metaplex_anchor::TokenMetadata, TreeConfig}; +use crate::{ + state::{metaplex_adapter::MetadataArgs, metaplex_anchor::TokenMetadata, TreeConfig}, + utils::validate_ownership_and_programs, +}; use super::process_collection_verification; @@ -38,8 +40,10 @@ pub struct CollectionVerification<'info> { pub edition_account: UncheckedAccount<'info>, /// CHECK: This is no longer needed but kept for backwards compatibility. pub bubblegum_signer: UncheckedAccount<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, + /// CHECK: Program is verified in the instruction + pub log_wrapper: UncheckedAccount<'info>, + /// CHECK: Program is verified in the instruction + pub compression_program: UncheckedAccount<'info>, /// CHECK: This is no longer needed but kept for backwards compatibility. pub token_metadata_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, @@ -54,6 +58,12 @@ pub(crate) fn verify_collection<'info>( index: u32, message: MetadataArgs, ) -> Result<()> { + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; + process_collection_verification( ctx, root, diff --git a/programs/bubblegum/program/src/processor/verify_creator.rs b/programs/bubblegum/program/src/processor/verify_creator.rs index f34c7be8..83e7d4e6 100644 --- a/programs/bubblegum/program/src/processor/verify_creator.rs +++ b/programs/bubblegum/program/src/processor/verify_creator.rs @@ -1,9 +1,9 @@ use anchor_lang::prelude::*; -use spl_account_compression::{program::SplAccountCompression, Noop}; use crate::{ processor::process_creator_verification, state::{metaplex_adapter::MetadataArgs, TreeConfig}, + utils::validate_ownership_and_programs, }; #[derive(Accounts)] @@ -22,8 +22,10 @@ pub struct CreatorVerification<'info> { pub merkle_tree: UncheckedAccount<'info>, pub payer: Signer<'info>, pub creator: Signer<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, + /// CHECK: Program is verified in the instruction + pub log_wrapper: UncheckedAccount<'info>, + /// CHECK: Program is verified in the instruction + pub compression_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, } @@ -36,6 +38,12 @@ pub(crate) fn verify_creator<'info>( index: u32, message: MetadataArgs, ) -> Result<()> { + validate_ownership_and_programs( + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + )?; + process_creator_verification( ctx, root, diff --git a/programs/bubblegum/program/src/state/leaf_schema.rs b/programs/bubblegum/program/src/state/leaf_schema.rs index 6709efd3..df88feec 100644 --- a/programs/bubblegum/program/src/state/leaf_schema.rs +++ b/programs/bubblegum/program/src/state/leaf_schema.rs @@ -1,7 +1,7 @@ use crate::state::BubblegumEventType; use anchor_lang::{prelude::*, solana_program::keccak}; use borsh::{BorshDeserialize, BorshSerialize}; -use spl_account_compression::Node; +use spl_concurrent_merkle_tree::node::Node; #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub struct LeafSchemaEvent { diff --git a/programs/bubblegum/program/src/utils.rs b/programs/bubblegum/program/src/utils.rs index 3dfc915a..b7e236a1 100644 --- a/programs/bubblegum/program/src/utils.rs +++ b/programs/bubblegum/program/src/utils.rs @@ -1,13 +1,17 @@ -use crate::state::{ - metaplex_adapter::{Creator, MetadataArgs}, - ASSET_PREFIX, -}; use anchor_lang::{ prelude::*, - solana_program::{program_memory::sol_memcmp, pubkey::PUBKEY_BYTES}, + solana_program::{program::invoke, program_memory::sol_memcmp, pubkey::PUBKEY_BYTES}, }; use solana_program::keccak; -use spl_account_compression::Node; +use spl_concurrent_merkle_tree::node::Node; + +use crate::{ + error::BubblegumError, + state::{ + metaplex_adapter::{Creator, MetadataArgs}, + ASSET_PREFIX, + }, +}; pub fn hash_creators(creators: &[Creator]) -> Result<[u8; 32]> { // Convert creator Vec to bytes Vec. @@ -51,17 +55,44 @@ pub fn replace_leaf<'info>( ) -> Result<()> { let seeds = &[seed.as_ref(), &[bump]]; let authority_pda_signer = &[&seeds[..]]; - let cpi_ctx = CpiContext::new_with_signer( - compression_program.clone(), - spl_account_compression::cpi::accounts::Modify { - authority: authority.clone(), - merkle_tree: merkle_tree.clone(), - noop: log_wrapper.clone(), - }, - authority_pda_signer, - ) - .with_remaining_accounts(remaining_accounts.to_vec()); - spl_account_compression::cpi::replace_leaf(cpi_ctx, root_node, previous_leaf, new_leaf, index) + + if compression_program.key == &spl_account_compression::id() { + let cpi_ctx = CpiContext::new_with_signer( + compression_program.clone(), + spl_account_compression::cpi::accounts::Modify { + authority: authority.clone(), + merkle_tree: merkle_tree.clone(), + noop: log_wrapper.clone(), + }, + authority_pda_signer, + ) + .with_remaining_accounts(remaining_accounts.to_vec()); + spl_account_compression::cpi::replace_leaf( + cpi_ctx, + root_node, + previous_leaf, + new_leaf, + index, + ) + } else { + let cpi_ctx = CpiContext::new_with_signer( + compression_program.clone(), + mpl_account_compression::cpi::accounts::Modify { + authority: authority.clone(), + merkle_tree: merkle_tree.clone(), + noop: log_wrapper.clone(), + }, + authority_pda_signer, + ) + .with_remaining_accounts(remaining_accounts.to_vec()); + mpl_account_compression::cpi::replace_leaf( + cpi_ctx, + root_node, + previous_leaf, + new_leaf, + index, + ) + } } pub fn append_leaf<'info>( @@ -75,16 +106,30 @@ pub fn append_leaf<'info>( ) -> Result<()> { let seeds = &[seed.as_ref(), &[bump]]; let authority_pda_signer = &[&seeds[..]]; - let cpi_ctx = CpiContext::new_with_signer( - compression_program.clone(), - spl_account_compression::cpi::accounts::Modify { - authority: authority.clone(), - merkle_tree: merkle_tree.clone(), - noop: log_wrapper.clone(), - }, - authority_pda_signer, - ); - spl_account_compression::cpi::append(cpi_ctx, leaf_node) + + if compression_program.key == &spl_account_compression::id() { + let cpi_ctx = CpiContext::new_with_signer( + compression_program.clone(), + spl_account_compression::cpi::accounts::Modify { + authority: authority.clone(), + merkle_tree: merkle_tree.clone(), + noop: log_wrapper.clone(), + }, + authority_pda_signer, + ); + spl_account_compression::cpi::append(cpi_ctx, leaf_node) + } else { + let cpi_ctx = CpiContext::new_with_signer( + compression_program.clone(), + mpl_account_compression::cpi::accounts::Modify { + authority: authority.clone(), + merkle_tree: merkle_tree.clone(), + noop: log_wrapper.clone(), + }, + authority_pda_signer, + ); + mpl_account_compression::cpi::append(cpi_ctx, leaf_node) + } } pub fn cmp_pubkeys(a: &Pubkey, b: &Pubkey) -> bool { @@ -106,3 +151,79 @@ pub fn get_asset_id(tree_id: &Pubkey, nonce: u64) -> Pubkey { ) .0 } + +/// Wraps a custom event in the most recent version of application event data. +/// Modified from spl-account-compression to allow `noop_program` to be an `UncheckedAccount` +/// and choose the correct one based on program ID. +pub(crate) fn wrap_application_data_v1( + custom_data: Vec, + noop_program: &AccountInfo<'_>, +) -> Result<()> { + if noop_program.key == &spl_noop::id() { + let versioned_data = spl_account_compression::events::ApplicationDataEventV1 { + application_data: custom_data, + }; + let event = spl_account_compression::events::AccountCompressionEvent::ApplicationData( + spl_account_compression::events::ApplicationDataEvent::V1(versioned_data), + ); + + invoke( + &spl_noop::instruction(event.try_to_vec()?), + &[noop_program.to_account_info()], + )?; + } else if noop_program.key == &mpl_noop::id() { + let versioned_data = mpl_account_compression::events::ApplicationDataEventV1 { + application_data: custom_data, + }; + let event = mpl_account_compression::events::AccountCompressionEvent::ApplicationData( + mpl_account_compression::events::ApplicationDataEvent::V1(versioned_data), + ); + + invoke( + &mpl_noop::instruction(event.try_to_vec()?), + &[noop_program.to_account_info()], + )?; + } else { + return Err(BubblegumError::InvalidLogWrapper.into()); + } + + Ok(()) +} + +/// Validate the Merkle tree is owned by one of the valid program choices, and that the provided +/// log wrapper and compression program are one of the valid choices. +pub(crate) fn validate_ownership_and_programs( + merkle_tree: &AccountInfo<'_>, + log_wrapper: &AccountInfo<'_>, + compression_program: &AccountInfo<'_>, +) -> Result<()> { + if merkle_tree.owner == &spl_account_compression::id() { + require!( + log_wrapper.key == &spl_noop::id(), + BubblegumError::InvalidLogWrapper + ); + require!( + compression_program.key == &spl_account_compression::id(), + BubblegumError::InvalidCompressionProgram + ); + } else if merkle_tree.owner == &mpl_account_compression::id() { + require!( + log_wrapper.key == &mpl_noop::id(), + BubblegumError::InvalidLogWrapper + ); + require!( + compression_program.key == &mpl_account_compression::id(), + BubblegumError::InvalidCompressionProgram + ); + } else { + return Err(BubblegumError::IncorrectOwner.into()); + } + + require!(log_wrapper.executable, BubblegumError::InvalidLogWrapper); + require!( + compression_program.executable, + BubblegumError::InvalidCompressionProgram + ); + + Ok(()) +}