From ecd1146ec7e30aac854af98b9c3559d1a5ab1dfd Mon Sep 17 00:00:00 2001 From: Fernando Otero Date: Wed, 27 Sep 2023 22:05:12 +0100 Subject: [PATCH] Move processor implementation (#39) * Move processor implementation * Fix truncated comment * Update generated clients * Fix test compilation * Tweak processor methods visibility * Remove duplicated functions * Change instruction name (#40) * Change instruction name * Update instruction name enum * Update generated code * Add helpers * Update generated clients * Update test import * Update generated clients --------- Co-authored-by: febo --- clients/js-solita/.solitarc.js | 13 +- .../src/generated/accounts/TreeConfig.ts | 12 +- .../src/generated/instructions/index.ts | 2 +- ...ableState.ts => setDecompressibleState.ts} | 42 +- ...essableState.ts => DecompressibleState.ts} | 8 +- .../src/generated/types/InstructionName.ts | 40 - .../js-solita/src/generated/types/index.ts | 3 +- .../js/src/generated/accounts/treeConfig.ts | 16 +- .../js/src/generated/instructions/index.ts | 2 +- ...ableState.ts => setDecompressibleState.ts} | 54 +- .../generated/types/decompressableState.ts | 25 - .../generated/types/decompressibleState.ts | 25 + clients/js/src/generated/types/index.ts | 3 +- .../js/src/generated/types/instructionName.ts | 40 - clients/js/test/_setup.ts | 8 +- .../src/generated/accounts/tree_config.rs | 4 +- .../rust/src/generated/instructions/mod.rs | 4 +- ...e_state.rs => set_decompressible_state.rs} | 72 +- ...sable_state.rs => decompressible_state.rs} | 2 +- .../src/generated/types/instruction_name.rs | 31 - clients/rust/src/generated/types/mod.rs | 6 +- clients/rust/src/lib.rs | 50 + configs/kinobi.cjs | 4 + idls/bubblegum.json | 927 +++++----- programs/bubblegum/program/src/asserts.rs | 152 ++ programs/bubblegum/program/src/lib.rs | 1582 ++--------------- .../bubblegum/program/src/processor/burn.rs | 72 + .../program/src/processor/cancel_redeem.rs | 72 + .../program/src/processor/compress.rs | 45 + .../program/src/processor/create_tree.rs | 56 + .../program/src/processor/decompress.rs | 261 +++ .../program/src/processor/delegate.rs | 75 + .../bubblegum/program/src/processor/mint.rs | 162 ++ .../src/processor/mint_to_collection.rs | 138 ++ .../bubblegum/program/src/processor/mod.rs | 363 ++++ .../bubblegum/program/src/processor/redeem.rs | 91 + .../processor/set_and_verify_collection.rs | 55 + .../src/processor/set_decompressible_state.rs | 19 + .../src/processor/set_tree_delegate.rs | 25 + .../program/src/processor/transfer.rs | 85 + .../src/processor/unverify_collection.rs | 28 + .../program/src/processor/unverify_creator.rs | 27 + .../src/processor/verify_collection.rs | 75 + .../program/src/processor/verify_creator.rs | 49 + programs/bubblegum/program/src/state/mod.rs | 4 +- programs/bubblegum/program/src/utils.rs | 177 +- .../bubblegum/program/tests/utils/context.rs | 1 + programs/bubblegum/program/tests/utils/mod.rs | 5 +- .../bubblegum/program/tests/utils/tree.rs | 10 +- .../program/tests/utils/tx_builder.rs | 2 +- 50 files changed, 2708 insertions(+), 2316 deletions(-) rename clients/js-solita/src/generated/instructions/{setDecompressableState.ts => setDecompressibleState.ts} (59%) rename clients/js-solita/src/generated/types/{DecompressableState.ts => DecompressibleState.ts} (69%) delete mode 100644 clients/js-solita/src/generated/types/InstructionName.ts rename clients/js/src/generated/instructions/{setDecompressableState.ts => setDecompressibleState.ts} (61%) delete mode 100644 clients/js/src/generated/types/decompressableState.ts create mode 100644 clients/js/src/generated/types/decompressibleState.ts delete mode 100644 clients/js/src/generated/types/instructionName.ts rename clients/rust/src/generated/instructions/{set_decompressable_state.rs => set_decompressible_state.rs} (85%) rename clients/rust/src/generated/types/{decompressable_state.rs => decompressible_state.rs} (93%) delete mode 100644 clients/rust/src/generated/types/instruction_name.rs create mode 100644 programs/bubblegum/program/src/asserts.rs create mode 100644 programs/bubblegum/program/src/processor/burn.rs create mode 100644 programs/bubblegum/program/src/processor/cancel_redeem.rs create mode 100644 programs/bubblegum/program/src/processor/compress.rs create mode 100644 programs/bubblegum/program/src/processor/create_tree.rs create mode 100644 programs/bubblegum/program/src/processor/decompress.rs create mode 100644 programs/bubblegum/program/src/processor/delegate.rs create mode 100644 programs/bubblegum/program/src/processor/mint.rs create mode 100644 programs/bubblegum/program/src/processor/mint_to_collection.rs create mode 100644 programs/bubblegum/program/src/processor/mod.rs create mode 100644 programs/bubblegum/program/src/processor/redeem.rs create mode 100644 programs/bubblegum/program/src/processor/set_and_verify_collection.rs create mode 100644 programs/bubblegum/program/src/processor/set_decompressible_state.rs create mode 100644 programs/bubblegum/program/src/processor/set_tree_delegate.rs create mode 100644 programs/bubblegum/program/src/processor/transfer.rs create mode 100644 programs/bubblegum/program/src/processor/unverify_collection.rs create mode 100644 programs/bubblegum/program/src/processor/unverify_creator.rs create mode 100644 programs/bubblegum/program/src/processor/verify_collection.rs create mode 100644 programs/bubblegum/program/src/processor/verify_creator.rs diff --git a/clients/js-solita/.solitarc.js b/clients/js-solita/.solitarc.js index 6ff60860..1ae15231 100644 --- a/clients/js-solita/.solitarc.js +++ b/clients/js-solita/.solitarc.js @@ -15,6 +15,15 @@ module.exports = { programDir, rustbin: { locked: true, - versionRangeFallback: "0.27.0", + versionRangeFallback: '0.27.0', }, -}; \ No newline at end of file + idlHook: (idl) => { + const instructions = idl.instructions.filter((ix) => { + return ix.name !== 'setDecompressableState'; + }); + const types = idl.types.filter((ty) => { + return ty.name !== 'InstructionName'; + }); + return { ...idl, instructions, types }; + }, +}; diff --git a/clients/js-solita/src/generated/accounts/TreeConfig.ts b/clients/js-solita/src/generated/accounts/TreeConfig.ts index b01c8c5d..c82e6a1f 100644 --- a/clients/js-solita/src/generated/accounts/TreeConfig.ts +++ b/clients/js-solita/src/generated/accounts/TreeConfig.ts @@ -8,7 +8,7 @@ import * as web3 from '@solana/web3.js'; import * as beet from '@metaplex-foundation/beet'; import * as beetSolana from '@metaplex-foundation/beet-solana'; -import { DecompressableState, decompressableStateBeet } from '../types/DecompressableState'; +import { DecompressibleState, decompressibleStateBeet } from '../types/DecompressibleState'; /** * Arguments used to create {@link TreeConfig} @@ -21,7 +21,7 @@ export type TreeConfigArgs = { totalMintCapacity: beet.bignum; numMinted: beet.bignum; isPublic: boolean; - isDecompressable: DecompressableState; + isDecompressible: DecompressibleState; }; export const treeConfigDiscriminator = [122, 245, 175, 248, 171, 34, 0, 207]; @@ -39,7 +39,7 @@ export class TreeConfig implements TreeConfigArgs { readonly totalMintCapacity: beet.bignum, readonly numMinted: beet.bignum, readonly isPublic: boolean, - readonly isDecompressable: DecompressableState, + readonly isDecompressible: DecompressibleState, ) {} /** @@ -52,7 +52,7 @@ export class TreeConfig implements TreeConfigArgs { args.totalMintCapacity, args.numMinted, args.isPublic, - args.isDecompressable, + args.isDecompressible, ); } @@ -173,7 +173,7 @@ export class TreeConfig implements TreeConfigArgs { return x; })(), isPublic: this.isPublic, - isDecompressable: 'DecompressableState.' + DecompressableState[this.isDecompressable], + isDecompressible: 'DecompressibleState.' + DecompressibleState[this.isDecompressible], }; } } @@ -195,7 +195,7 @@ export const treeConfigBeet = new beet.BeetStruct< ['totalMintCapacity', beet.u64], ['numMinted', beet.u64], ['isPublic', beet.bool], - ['isDecompressable', decompressableStateBeet], + ['isDecompressible', decompressibleStateBeet], ], TreeConfig.fromArgs, 'TreeConfig', diff --git a/clients/js-solita/src/generated/instructions/index.ts b/clients/js-solita/src/generated/instructions/index.ts index 72604411..be72b10c 100644 --- a/clients/js-solita/src/generated/instructions/index.ts +++ b/clients/js-solita/src/generated/instructions/index.ts @@ -8,7 +8,7 @@ export * from './mintToCollectionV1'; export * from './mintV1'; export * from './redeem'; export * from './setAndVerifyCollection'; -export * from './setDecompressableState'; +export * from './setDecompressibleState'; export * from './setTreeDelegate'; export * from './transfer'; export * from './unverifyCollection'; diff --git a/clients/js-solita/src/generated/instructions/setDecompressableState.ts b/clients/js-solita/src/generated/instructions/setDecompressibleState.ts similarity index 59% rename from clients/js-solita/src/generated/instructions/setDecompressableState.ts rename to clients/js-solita/src/generated/instructions/setDecompressibleState.ts index 224bf475..915d96a5 100644 --- a/clients/js-solita/src/generated/instructions/setDecompressableState.ts +++ b/clients/js-solita/src/generated/instructions/setDecompressibleState.ts @@ -7,68 +7,66 @@ import * as beet from '@metaplex-foundation/beet'; import * as web3 from '@solana/web3.js'; -import { DecompressableState, decompressableStateBeet } from '../types/DecompressableState'; +import { DecompressibleState, decompressibleStateBeet } from '../types/DecompressibleState'; /** * @category Instructions - * @category SetDecompressableState + * @category SetDecompressibleState * @category generated */ -export type SetDecompressableStateInstructionArgs = { - decompressableState: DecompressableState; +export type SetDecompressibleStateInstructionArgs = { + decompressableState: DecompressibleState; }; /** * @category Instructions - * @category SetDecompressableState + * @category SetDecompressibleState * @category generated */ -export const setDecompressableStateStruct = new beet.BeetArgsStruct< - SetDecompressableStateInstructionArgs & { +export const setDecompressibleStateStruct = new beet.BeetArgsStruct< + SetDecompressibleStateInstructionArgs & { instructionDiscriminator: number[] /* size: 8 */; } >( [ ['instructionDiscriminator', beet.uniformFixedSizeArray(beet.u8, 8)], - ['decompressableState', decompressableStateBeet], + ['decompressableState', decompressibleStateBeet], ], - 'SetDecompressableStateInstructionArgs', + 'SetDecompressibleStateInstructionArgs', ); /** - * Accounts required by the _setDecompressableState_ instruction + * Accounts required by the _setDecompressibleState_ instruction * * @property [_writable_] treeAuthority * @property [**signer**] treeCreator * @category Instructions - * @category SetDecompressableState + * @category SetDecompressibleState * @category generated */ -export type SetDecompressableStateInstructionAccounts = { +export type SetDecompressibleStateInstructionAccounts = { treeAuthority: web3.PublicKey; treeCreator: web3.PublicKey; anchorRemainingAccounts?: web3.AccountMeta[]; }; -export const setDecompressableStateInstructionDiscriminator = [ - 18, 135, 238, 168, 246, 195, 61, 115, -]; +export const setDecompressibleStateInstructionDiscriminator = [82, 104, 152, 6, 149, 111, 100, 13]; /** - * Creates a _SetDecompressableState_ instruction. + * Creates a _SetDecompressibleState_ 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 SetDecompressableState + * @category SetDecompressibleState * @category generated */ -export function createSetDecompressableStateInstruction( - accounts: SetDecompressableStateInstructionAccounts, - args: SetDecompressableStateInstructionArgs, +export function createSetDecompressibleStateInstruction( + accounts: SetDecompressibleStateInstructionAccounts, + args: SetDecompressibleStateInstructionArgs, programId = new web3.PublicKey('BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY'), ) { - const [data] = setDecompressableStateStruct.serialize({ - instructionDiscriminator: setDecompressableStateInstructionDiscriminator, + const [data] = setDecompressibleStateStruct.serialize({ + instructionDiscriminator: setDecompressibleStateInstructionDiscriminator, ...args, }); const keys: web3.AccountMeta[] = [ diff --git a/clients/js-solita/src/generated/types/DecompressableState.ts b/clients/js-solita/src/generated/types/DecompressibleState.ts similarity index 69% rename from clients/js-solita/src/generated/types/DecompressableState.ts rename to clients/js-solita/src/generated/types/DecompressibleState.ts index 3a80bcef..8811da07 100644 --- a/clients/js-solita/src/generated/types/DecompressableState.ts +++ b/clients/js-solita/src/generated/types/DecompressibleState.ts @@ -10,7 +10,7 @@ import * as beet from '@metaplex-foundation/beet'; * @category enums * @category generated */ -export enum DecompressableState { +export enum DecompressibleState { Enabled, Disabled, } @@ -19,6 +19,6 @@ export enum DecompressableState { * @category userTypes * @category generated */ -export const decompressableStateBeet = beet.fixedScalarEnum( - DecompressableState, -) as beet.FixedSizeBeet; +export const decompressibleStateBeet = beet.fixedScalarEnum( + DecompressibleState, +) as beet.FixedSizeBeet; diff --git a/clients/js-solita/src/generated/types/InstructionName.ts b/clients/js-solita/src/generated/types/InstructionName.ts deleted file mode 100644 index 1c925099..00000000 --- a/clients/js-solita/src/generated/types/InstructionName.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * 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 beet from '@metaplex-foundation/beet'; -/** - * @category enums - * @category generated - */ -export enum InstructionName { - Unknown, - MintV1, - Redeem, - CancelRedeem, - Transfer, - Delegate, - DecompressV1, - Compress, - Burn, - CreateTree, - VerifyCreator, - UnverifyCreator, - VerifyCollection, - UnverifyCollection, - SetAndVerifyCollection, - MintToCollectionV1, - SetDecompressableState, -} - -/** - * @category userTypes - * @category generated - */ -export const instructionNameBeet = beet.fixedScalarEnum(InstructionName) as beet.FixedSizeBeet< - InstructionName, - InstructionName ->; diff --git a/clients/js-solita/src/generated/types/index.ts b/clients/js-solita/src/generated/types/index.ts index d89ada2e..bc7fae93 100644 --- a/clients/js-solita/src/generated/types/index.ts +++ b/clients/js-solita/src/generated/types/index.ts @@ -1,8 +1,7 @@ export * from './BubblegumEventType'; export * from './Collection'; export * from './Creator'; -export * from './DecompressableState'; -export * from './InstructionName'; +export * from './DecompressibleState'; export * from './LeafSchema'; export * from './MetadataArgs'; export * from './TokenProgramVersion'; diff --git a/clients/js/src/generated/accounts/treeConfig.ts b/clients/js/src/generated/accounts/treeConfig.ts index f1f4c77a..c05ecac0 100644 --- a/clients/js/src/generated/accounts/treeConfig.ts +++ b/clients/js/src/generated/accounts/treeConfig.ts @@ -30,9 +30,9 @@ import { u8, } from '@metaplex-foundation/umi/serializers'; import { - DecompressableState, - DecompressableStateArgs, - getDecompressableStateSerializer, + DecompressibleState, + DecompressibleStateArgs, + getDecompressibleStateSerializer, } from '../types'; export type TreeConfig = Account; @@ -44,7 +44,7 @@ export type TreeConfigAccountData = { totalMintCapacity: bigint; numMinted: bigint; isPublic: boolean; - isDecompressable: DecompressableState; + isDecompressible: DecompressibleState; }; export type TreeConfigAccountDataArgs = { @@ -53,7 +53,7 @@ export type TreeConfigAccountDataArgs = { totalMintCapacity: number | bigint; numMinted: number | bigint; isPublic: boolean; - isDecompressable: DecompressableStateArgs; + isDecompressible: DecompressibleStateArgs; }; export function getTreeConfigAccountDataSerializer(): Serializer< @@ -69,7 +69,7 @@ export function getTreeConfigAccountDataSerializer(): Serializer< ['totalMintCapacity', u64()], ['numMinted', u64()], ['isPublic', bool()], - ['isDecompressable', getDecompressableStateSerializer()], + ['isDecompressible', getDecompressibleStateSerializer()], ], { description: 'TreeConfigAccountData' } ), @@ -153,7 +153,7 @@ export function getTreeConfigGpaBuilder( totalMintCapacity: number | bigint; numMinted: number | bigint; isPublic: boolean; - isDecompressable: DecompressableStateArgs; + isDecompressible: DecompressibleStateArgs; }>({ discriminator: [0, array(u8(), { size: 8 })], treeCreator: [8, publicKeySerializer()], @@ -161,7 +161,7 @@ export function getTreeConfigGpaBuilder( totalMintCapacity: [72, u64()], numMinted: [80, u64()], isPublic: [88, bool()], - isDecompressable: [89, getDecompressableStateSerializer()], + isDecompressible: [89, getDecompressibleStateSerializer()], }) .deserializeUsing((account) => deserializeTreeConfig(account)) .whereField('discriminator', [122, 245, 175, 248, 171, 34, 0, 207]); diff --git a/clients/js/src/generated/instructions/index.ts b/clients/js/src/generated/instructions/index.ts index 30a20fe1..58083c6e 100644 --- a/clients/js/src/generated/instructions/index.ts +++ b/clients/js/src/generated/instructions/index.ts @@ -15,7 +15,7 @@ export * from './mintToCollectionV1'; export * from './mintV1'; export * from './redeem'; export * from './setAndVerifyCollection'; -export * from './setDecompressableState'; +export * from './setDecompressibleState'; export * from './setTreeDelegate'; export * from './transfer'; export * from './unverifyCollection'; diff --git a/clients/js/src/generated/instructions/setDecompressableState.ts b/clients/js/src/generated/instructions/setDecompressibleState.ts similarity index 61% rename from clients/js/src/generated/instructions/setDecompressableState.ts rename to clients/js/src/generated/instructions/setDecompressibleState.ts index a6122266..896350e0 100644 --- a/clients/js/src/generated/instructions/setDecompressableState.ts +++ b/clients/js/src/generated/instructions/setDecompressibleState.ts @@ -27,62 +27,62 @@ import { getAccountMetasAndSigners, } from '../shared'; import { - DecompressableState, - DecompressableStateArgs, - getDecompressableStateSerializer, + DecompressibleState, + DecompressibleStateArgs, + getDecompressibleStateSerializer, } from '../types'; // Accounts. -export type SetDecompressableStateInstructionAccounts = { +export type SetDecompressibleStateInstructionAccounts = { treeConfig: PublicKey | Pda; treeCreator?: Signer; }; // Data. -export type SetDecompressableStateInstructionData = { +export type SetDecompressibleStateInstructionData = { discriminator: Array; - decompressableState: DecompressableState; + decompressableState: DecompressibleState; }; -export type SetDecompressableStateInstructionDataArgs = { - decompressableState: DecompressableStateArgs; +export type SetDecompressibleStateInstructionDataArgs = { + decompressableState: DecompressibleStateArgs; }; -export function getSetDecompressableStateInstructionDataSerializer(): Serializer< - SetDecompressableStateInstructionDataArgs, - SetDecompressableStateInstructionData +export function getSetDecompressibleStateInstructionDataSerializer(): Serializer< + SetDecompressibleStateInstructionDataArgs, + SetDecompressibleStateInstructionData > { return mapSerializer< - SetDecompressableStateInstructionDataArgs, + SetDecompressibleStateInstructionDataArgs, any, - SetDecompressableStateInstructionData + SetDecompressibleStateInstructionData >( - struct( + struct( [ ['discriminator', array(u8(), { size: 8 })], - ['decompressableState', getDecompressableStateSerializer()], + ['decompressableState', getDecompressibleStateSerializer()], ], - { description: 'SetDecompressableStateInstructionData' } + { description: 'SetDecompressibleStateInstructionData' } ), (value) => ({ ...value, - discriminator: [18, 135, 238, 168, 246, 195, 61, 115], + discriminator: [82, 104, 152, 6, 149, 111, 100, 13], }) ) as Serializer< - SetDecompressableStateInstructionDataArgs, - SetDecompressableStateInstructionData + SetDecompressibleStateInstructionDataArgs, + SetDecompressibleStateInstructionData >; } // Args. -export type SetDecompressableStateInstructionArgs = - SetDecompressableStateInstructionDataArgs; +export type SetDecompressibleStateInstructionArgs = + SetDecompressibleStateInstructionDataArgs; // Instruction. -export function setDecompressableState( +export function setDecompressibleState( context: Pick, - input: SetDecompressableStateInstructionAccounts & - SetDecompressableStateInstructionArgs + input: SetDecompressibleStateInstructionAccounts & + SetDecompressibleStateInstructionArgs ): TransactionBuilder { // Program ID. const programId = context.programs.getPublicKey( @@ -101,7 +101,7 @@ export function setDecompressableState( }; // Arguments. - const resolvedArgs: SetDecompressableStateInstructionArgs = { ...input }; + const resolvedArgs: SetDecompressibleStateInstructionArgs = { ...input }; // Default values. if (!resolvedAccounts.treeCreator.value) { @@ -121,8 +121,8 @@ export function setDecompressableState( ); // Data. - const data = getSetDecompressableStateInstructionDataSerializer().serialize( - resolvedArgs as SetDecompressableStateInstructionDataArgs + const data = getSetDecompressibleStateInstructionDataSerializer().serialize( + resolvedArgs as SetDecompressibleStateInstructionDataArgs ); // Bytes Created On Chain. diff --git a/clients/js/src/generated/types/decompressableState.ts b/clients/js/src/generated/types/decompressableState.ts deleted file mode 100644 index 956726a2..00000000 --- a/clients/js/src/generated/types/decompressableState.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * This code was AUTOGENERATED using the kinobi library. - * Please DO NOT EDIT THIS FILE, instead use visitors - * to add features, then rerun kinobi to update it. - * - * @see https://github.com/metaplex-foundation/kinobi - */ - -import { Serializer, scalarEnum } from '@metaplex-foundation/umi/serializers'; - -export enum DecompressableState { - Enabled, - Disabled, -} - -export type DecompressableStateArgs = DecompressableState; - -export function getDecompressableStateSerializer(): Serializer< - DecompressableStateArgs, - DecompressableState -> { - return scalarEnum(DecompressableState, { - description: 'DecompressableState', - }) as Serializer; -} diff --git a/clients/js/src/generated/types/decompressibleState.ts b/clients/js/src/generated/types/decompressibleState.ts new file mode 100644 index 00000000..550ca649 --- /dev/null +++ b/clients/js/src/generated/types/decompressibleState.ts @@ -0,0 +1,25 @@ +/** + * This code was AUTOGENERATED using the kinobi library. + * Please DO NOT EDIT THIS FILE, instead use visitors + * to add features, then rerun kinobi to update it. + * + * @see https://github.com/metaplex-foundation/kinobi + */ + +import { Serializer, scalarEnum } from '@metaplex-foundation/umi/serializers'; + +export enum DecompressibleState { + Enabled, + Disabled, +} + +export type DecompressibleStateArgs = DecompressibleState; + +export function getDecompressibleStateSerializer(): Serializer< + DecompressibleStateArgs, + DecompressibleState +> { + return scalarEnum(DecompressibleState, { + description: 'DecompressibleState', + }) as Serializer; +} diff --git a/clients/js/src/generated/types/index.ts b/clients/js/src/generated/types/index.ts index c1dfbb76..cfd6944c 100644 --- a/clients/js/src/generated/types/index.ts +++ b/clients/js/src/generated/types/index.ts @@ -12,8 +12,7 @@ export * from './compressionAccountType'; export * from './concurrentMerkleTreeHeader'; export * from './concurrentMerkleTreeHeaderData'; export * from './creator'; -export * from './decompressableState'; -export * from './instructionName'; +export * from './decompressibleState'; export * from './leafSchema'; export * from './metadataArgs'; export * from './tokenProgramVersion'; diff --git a/clients/js/src/generated/types/instructionName.ts b/clients/js/src/generated/types/instructionName.ts deleted file mode 100644 index 0d698ee1..00000000 --- a/clients/js/src/generated/types/instructionName.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * This code was AUTOGENERATED using the kinobi library. - * Please DO NOT EDIT THIS FILE, instead use visitors - * to add features, then rerun kinobi to update it. - * - * @see https://github.com/metaplex-foundation/kinobi - */ - -import { Serializer, scalarEnum } from '@metaplex-foundation/umi/serializers'; - -export enum InstructionName { - Unknown, - MintV1, - Redeem, - CancelRedeem, - Transfer, - Delegate, - DecompressV1, - Compress, - Burn, - CreateTree, - VerifyCreator, - UnverifyCreator, - VerifyCollection, - UnverifyCollection, - SetAndVerifyCollection, - MintToCollectionV1, - SetDecompressableState, -} - -export type InstructionNameArgs = InstructionName; - -export function getInstructionNameSerializer(): Serializer< - InstructionNameArgs, - InstructionName -> { - return scalarEnum(InstructionName, { - description: 'InstructionName', - }) as Serializer; -} diff --git a/clients/js/test/_setup.ts b/clients/js/test/_setup.ts index 7112fe89..683840d9 100644 --- a/clients/js/test/_setup.ts +++ b/clients/js/test/_setup.ts @@ -11,7 +11,7 @@ import { } from '@metaplex-foundation/umi'; import { createUmi as baseCreateUmi } from '@metaplex-foundation/umi-bundle-tests'; import { - DecompressableState, + DecompressibleState, MetadataArgsArgs, createTree as baseCreateTree, mintV1 as baseMintV1, @@ -20,7 +20,7 @@ import { findTreeConfigPda, hashLeaf, mplBubblegum, - setDecompressableState, + setDecompressibleState, } from '../src'; export const createUmi = async (endpoint?: string, airdropAmount?: SolAmount) => @@ -40,12 +40,12 @@ export const createTree = async ( ...input, }); builder = builder.append( - setDecompressableState(context, { + setDecompressibleState(context, { treeConfig: input.treeConfig ?? findTreeConfigPda(context, { merkleTree: merkleTree.publicKey }), treeCreator: input.treeCreator ?? context.identity, - decompressableState: DecompressableState.Enabled, + decompressableState: DecompressibleState.Enabled, }) ); await builder.sendAndConfirm(context); diff --git a/clients/rust/src/generated/accounts/tree_config.rs b/clients/rust/src/generated/accounts/tree_config.rs index 1eed48a9..dfdfe35e 100644 --- a/clients/rust/src/generated/accounts/tree_config.rs +++ b/clients/rust/src/generated/accounts/tree_config.rs @@ -5,7 +5,7 @@ //! [https://github.com/metaplex-foundation/kinobi] //! -use crate::generated::types::DecompressableState; +use crate::generated::types::DecompressibleState; use borsh::BorshDeserialize; use borsh::BorshSerialize; use solana_program::pubkey::Pubkey; @@ -27,7 +27,7 @@ pub struct TreeConfig { pub total_mint_capacity: u64, pub num_minted: u64, pub is_public: bool, - pub is_decompressable: DecompressableState, + pub is_decompressible: DecompressibleState, } impl TreeConfig { diff --git a/clients/rust/src/generated/instructions/mod.rs b/clients/rust/src/generated/instructions/mod.rs index b165cf28..015d145a 100644 --- a/clients/rust/src/generated/instructions/mod.rs +++ b/clients/rust/src/generated/instructions/mod.rs @@ -14,7 +14,7 @@ pub(crate) mod mint_to_collection_v1; pub(crate) mod mint_v1; pub(crate) mod redeem; pub(crate) mod set_and_verify_collection; -pub(crate) mod set_decompressable_state; +pub(crate) mod set_decompressible_state; pub(crate) mod set_tree_delegate; pub(crate) mod transfer; pub(crate) mod unverify_collection; @@ -32,7 +32,7 @@ pub use self::mint_to_collection_v1::*; pub use self::mint_v1::*; pub use self::redeem::*; pub use self::set_and_verify_collection::*; -pub use self::set_decompressable_state::*; +pub use self::set_decompressible_state::*; pub use self::set_tree_delegate::*; pub use self::transfer::*; pub use self::unverify_collection::*; diff --git a/clients/rust/src/generated/instructions/set_decompressable_state.rs b/clients/rust/src/generated/instructions/set_decompressible_state.rs similarity index 85% rename from clients/rust/src/generated/instructions/set_decompressable_state.rs rename to clients/rust/src/generated/instructions/set_decompressible_state.rs index 89d37bf7..fa95a914 100644 --- a/clients/rust/src/generated/instructions/set_decompressable_state.rs +++ b/clients/rust/src/generated/instructions/set_decompressible_state.rs @@ -5,28 +5,28 @@ //! [https://github.com/metaplex-foundation/kinobi] //! -use crate::generated::types::DecompressableState; +use crate::generated::types::DecompressibleState; use borsh::BorshDeserialize; use borsh::BorshSerialize; /// Accounts. -pub struct SetDecompressableState { +pub struct SetDecompressibleState { pub tree_config: solana_program::pubkey::Pubkey, pub tree_creator: solana_program::pubkey::Pubkey, } -impl SetDecompressableState { +impl SetDecompressibleState { pub fn instruction( &self, - args: SetDecompressableStateInstructionArgs, + args: SetDecompressibleStateInstructionArgs, ) -> solana_program::instruction::Instruction { self.instruction_with_remaining_accounts(args, &[]) } #[allow(clippy::vec_init_then_push)] pub fn instruction_with_remaining_accounts( &self, - args: SetDecompressableStateInstructionArgs, + args: SetDecompressibleStateInstructionArgs, remaining_accounts: &[solana_program::instruction::AccountMeta], ) -> solana_program::instruction::Instruction { let mut accounts = Vec::with_capacity(2 + remaining_accounts.len()); @@ -39,7 +39,7 @@ impl SetDecompressableState { true, )); accounts.extend_from_slice(remaining_accounts); - let mut data = SetDecompressableStateInstructionData::new() + let mut data = SetDecompressibleStateInstructionData::new() .try_to_vec() .unwrap(); let mut args = args.try_to_vec().unwrap(); @@ -54,34 +54,34 @@ impl SetDecompressableState { } #[derive(BorshDeserialize, BorshSerialize)] -struct SetDecompressableStateInstructionData { +struct SetDecompressibleStateInstructionData { discriminator: [u8; 8], } -impl SetDecompressableStateInstructionData { +impl SetDecompressibleStateInstructionData { fn new() -> Self { Self { - discriminator: [18, 135, 238, 168, 246, 195, 61, 115], + discriminator: [82, 104, 152, 6, 149, 111, 100, 13], } } } #[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct SetDecompressableStateInstructionArgs { - pub decompressable_state: DecompressableState, +pub struct SetDecompressibleStateInstructionArgs { + pub decompressable_state: DecompressibleState, } /// Instruction builder. #[derive(Default)] -pub struct SetDecompressableStateBuilder { +pub struct SetDecompressibleStateBuilder { tree_config: Option, tree_creator: Option, - decompressable_state: Option, + decompressable_state: Option, __remaining_accounts: Vec, } -impl SetDecompressableStateBuilder { +impl SetDecompressibleStateBuilder { pub fn new() -> Self { Self::default() } @@ -96,7 +96,7 @@ impl SetDecompressableStateBuilder { self } #[inline(always)] - pub fn decompressable_state(&mut self, decompressable_state: DecompressableState) -> &mut Self { + pub fn decompressable_state(&mut self, decompressable_state: DecompressibleState) -> &mut Self { self.decompressable_state = Some(decompressable_state); self } @@ -120,11 +120,11 @@ impl SetDecompressableStateBuilder { } #[allow(clippy::clone_on_copy)] pub fn instruction(&self) -> solana_program::instruction::Instruction { - let accounts = SetDecompressableState { + let accounts = SetDecompressibleState { tree_config: self.tree_config.expect("tree_config is not set"), tree_creator: self.tree_creator.expect("tree_creator is not set"), }; - let args = SetDecompressableStateInstructionArgs { + let args = SetDecompressibleStateInstructionArgs { decompressable_state: self .decompressable_state .clone() @@ -135,15 +135,15 @@ impl SetDecompressableStateBuilder { } } -/// `set_decompressable_state` CPI accounts. -pub struct SetDecompressableStateCpiAccounts<'a, 'b> { +/// `set_decompressible_state` CPI accounts. +pub struct SetDecompressibleStateCpiAccounts<'a, 'b> { pub tree_config: &'b solana_program::account_info::AccountInfo<'a>, pub tree_creator: &'b solana_program::account_info::AccountInfo<'a>, } -/// `set_decompressable_state` CPI instruction. -pub struct SetDecompressableStateCpi<'a, 'b> { +/// `set_decompressible_state` CPI instruction. +pub struct SetDecompressibleStateCpi<'a, 'b> { /// The program to invoke. pub __program: &'b solana_program::account_info::AccountInfo<'a>, @@ -151,14 +151,14 @@ pub struct SetDecompressableStateCpi<'a, 'b> { pub tree_creator: &'b solana_program::account_info::AccountInfo<'a>, /// The arguments for the instruction. - pub __args: SetDecompressableStateInstructionArgs, + pub __args: SetDecompressibleStateInstructionArgs, } -impl<'a, 'b> SetDecompressableStateCpi<'a, 'b> { +impl<'a, 'b> SetDecompressibleStateCpi<'a, 'b> { pub fn new( program: &'b solana_program::account_info::AccountInfo<'a>, - accounts: SetDecompressableStateCpiAccounts<'a, 'b>, - args: SetDecompressableStateInstructionArgs, + accounts: SetDecompressibleStateCpiAccounts<'a, 'b>, + args: SetDecompressibleStateInstructionArgs, ) -> Self { Self { __program: program, @@ -216,7 +216,7 @@ impl<'a, 'b> SetDecompressableStateCpi<'a, 'b> { is_writable: remaining_account.2, }) }); - let mut data = SetDecompressableStateInstructionData::new() + let mut data = SetDecompressibleStateInstructionData::new() .try_to_vec() .unwrap(); let mut args = self.__args.try_to_vec().unwrap(); @@ -243,14 +243,14 @@ impl<'a, 'b> SetDecompressableStateCpi<'a, 'b> { } } -/// `set_decompressable_state` CPI instruction builder. -pub struct SetDecompressableStateCpiBuilder<'a, 'b> { - instruction: Box>, +/// `set_decompressible_state` CPI instruction builder. +pub struct SetDecompressibleStateCpiBuilder<'a, 'b> { + instruction: Box>, } -impl<'a, 'b> SetDecompressableStateCpiBuilder<'a, 'b> { +impl<'a, 'b> SetDecompressibleStateCpiBuilder<'a, 'b> { pub fn new(program: &'b solana_program::account_info::AccountInfo<'a>) -> Self { - let instruction = Box::new(SetDecompressableStateCpiBuilderInstruction { + let instruction = Box::new(SetDecompressibleStateCpiBuilderInstruction { __program: program, tree_config: None, tree_creator: None, @@ -276,7 +276,7 @@ impl<'a, 'b> SetDecompressableStateCpiBuilder<'a, 'b> { self } #[inline(always)] - pub fn decompressable_state(&mut self, decompressable_state: DecompressableState) -> &mut Self { + pub fn decompressable_state(&mut self, decompressable_state: DecompressibleState) -> &mut Self { self.instruction.decompressable_state = Some(decompressable_state); self } @@ -321,14 +321,14 @@ impl<'a, 'b> SetDecompressableStateCpiBuilder<'a, 'b> { &self, signers_seeds: &[&[&[u8]]], ) -> solana_program::entrypoint::ProgramResult { - let args = SetDecompressableStateInstructionArgs { + let args = SetDecompressibleStateInstructionArgs { decompressable_state: self .instruction .decompressable_state .clone() .expect("decompressable_state is not set"), }; - let instruction = SetDecompressableStateCpi { + let instruction = SetDecompressibleStateCpi { __program: self.instruction.__program, tree_config: self @@ -349,11 +349,11 @@ impl<'a, 'b> SetDecompressableStateCpiBuilder<'a, 'b> { } } -struct SetDecompressableStateCpiBuilderInstruction<'a, 'b> { +struct SetDecompressibleStateCpiBuilderInstruction<'a, 'b> { __program: &'b solana_program::account_info::AccountInfo<'a>, tree_config: Option<&'b solana_program::account_info::AccountInfo<'a>>, tree_creator: Option<&'b solana_program::account_info::AccountInfo<'a>>, - decompressable_state: Option, + decompressable_state: Option, /// Additional instruction accounts `(AccountInfo, is_writable, is_signer)`. __remaining_accounts: Vec<( &'b solana_program::account_info::AccountInfo<'a>, diff --git a/clients/rust/src/generated/types/decompressable_state.rs b/clients/rust/src/generated/types/decompressible_state.rs similarity index 93% rename from clients/rust/src/generated/types/decompressable_state.rs rename to clients/rust/src/generated/types/decompressible_state.rs index c1d136ad..8b2517a5 100644 --- a/clients/rust/src/generated/types/decompressable_state.rs +++ b/clients/rust/src/generated/types/decompressible_state.rs @@ -10,7 +10,7 @@ use borsh::BorshSerialize; #[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum DecompressableState { +pub enum DecompressibleState { Enabled, Disabled, } diff --git a/clients/rust/src/generated/types/instruction_name.rs b/clients/rust/src/generated/types/instruction_name.rs deleted file mode 100644 index 7f2fd2c5..00000000 --- a/clients/rust/src/generated/types/instruction_name.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! This code was AUTOGENERATED using the kinobi library. -//! Please DO NOT EDIT THIS FILE, instead use visitors -//! to add features, then rerun kinobi to update it. -//! -//! [https://github.com/metaplex-foundation/kinobi] -//! - -use borsh::BorshDeserialize; -use borsh::BorshSerialize; - -#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum InstructionName { - Unknown, - MintV1, - Redeem, - CancelRedeem, - Transfer, - Delegate, - DecompressV1, - Compress, - Burn, - CreateTree, - VerifyCreator, - UnverifyCreator, - VerifyCollection, - UnverifyCollection, - SetAndVerifyCollection, - MintToCollectionV1, - SetDecompressableState, -} diff --git a/clients/rust/src/generated/types/mod.rs b/clients/rust/src/generated/types/mod.rs index 7500eb99..f12d568b 100644 --- a/clients/rust/src/generated/types/mod.rs +++ b/clients/rust/src/generated/types/mod.rs @@ -11,8 +11,7 @@ pub(crate) mod compression_account_type; pub(crate) mod concurrent_merkle_tree_header; pub(crate) mod concurrent_merkle_tree_header_data; pub(crate) mod creator; -pub(crate) mod decompressable_state; -pub(crate) mod instruction_name; +pub(crate) mod decompressible_state; pub(crate) mod leaf_schema; pub(crate) mod metadata_args; pub(crate) mod token_program_version; @@ -27,8 +26,7 @@ pub use self::compression_account_type::*; pub use self::concurrent_merkle_tree_header::*; pub use self::concurrent_merkle_tree_header_data::*; pub use self::creator::*; -pub use self::decompressable_state::*; -pub use self::instruction_name::*; +pub use self::decompressible_state::*; pub use self::leaf_schema::*; pub use self::metadata_args::*; pub use self::token_program_version::*; diff --git a/clients/rust/src/lib.rs b/clients/rust/src/lib.rs index b05af6d1..d2deb547 100644 --- a/clients/rust/src/lib.rs +++ b/clients/rust/src/lib.rs @@ -5,3 +5,53 @@ pub mod utils; pub use generated::programs::MPL_BUBBLEGUM_ID as ID; pub use generated::*; + +pub enum InstructionName { + Unknown, + MintV1, + Redeem, + CancelRedeem, + Transfer, + Delegate, + DecompressV1, + Compress, + Burn, + CreateTree, + VerifyCreator, + UnverifyCreator, + VerifyCollection, + UnverifyCollection, + SetAndVerifyCollection, + MintToCollectionV1, + SetDecompressibleState, +} + +pub fn get_instruction_type(full_bytes: &[u8]) -> InstructionName { + let disc: [u8; 8] = { + let mut disc = [0; 8]; + disc.copy_from_slice(&full_bytes[..8]); + disc + }; + match disc { + [145, 98, 192, 118, 184, 147, 118, 104] => InstructionName::MintV1, + [153, 18, 178, 47, 197, 158, 86, 15] => InstructionName::MintToCollectionV1, + [111, 76, 232, 50, 39, 175, 48, 242] => InstructionName::CancelRedeem, + [184, 12, 86, 149, 70, 196, 97, 225] => InstructionName::Redeem, + [163, 52, 200, 231, 140, 3, 69, 186] => InstructionName::Transfer, + [90, 147, 75, 178, 85, 88, 4, 137] => InstructionName::Delegate, + [54, 85, 76, 70, 228, 250, 164, 81] => InstructionName::DecompressV1, + [116, 110, 29, 56, 107, 219, 42, 93] => InstructionName::Burn, + [82, 193, 176, 117, 176, 21, 115, 253] => InstructionName::Compress, + [165, 83, 136, 142, 89, 202, 47, 220] => InstructionName::CreateTree, + [52, 17, 96, 132, 71, 4, 85, 194] => InstructionName::VerifyCreator, + [107, 178, 57, 39, 105, 115, 112, 152] => InstructionName::UnverifyCreator, + [56, 113, 101, 253, 79, 55, 122, 169] => InstructionName::VerifyCollection, + [250, 251, 42, 106, 41, 137, 186, 168] => InstructionName::UnverifyCollection, + [235, 242, 121, 216, 158, 234, 180, 234] => InstructionName::SetAndVerifyCollection, + [82, 104, 152, 6, 149, 111, 100, 13] => InstructionName::SetDecompressibleState, + // `SetDecompressableState` instruction mapped to `SetDecompressibleState` instruction + [18, 135, 238, 168, 246, 195, 61, 115] => InstructionName::SetDecompressibleState, + + _ => InstructionName::Unknown, + } +} diff --git a/configs/kinobi.cjs b/configs/kinobi.cjs index 59b40363..33c209c2 100755 --- a/configs/kinobi.cjs +++ b/configs/kinobi.cjs @@ -39,6 +39,8 @@ kinobi.update( // Update types. kinobi.update( new k.UpdateDefinedTypesVisitor({ + // Remove unnecessary types. + InstructionName: { delete: true }, // Remove unnecessary spl_account_compression type. ApplicationDataEventV1: { delete: true }, ChangeLogEventV1: { delete: true }, @@ -280,6 +282,8 @@ kinobi.update( unverifyCollection: { args: { ...hashDefaults } }, verifyCreator: { args: { ...hashDefaults } }, unverifyCreator: { args: { ...hashDefaults } }, + // Remove deprecated instructions. + setDecompressableState: { delete: true }, // Remove unnecessary spl_account_compression instructions. append: { delete: true }, closeEmptyTree: { delete: true }, diff --git a/idls/bubblegum.json b/idls/bubblegum.json index 8677b0dd..ac7652f4 100644 --- a/idls/bubblegum.json +++ b/idls/bubblegum.json @@ -3,27 +3,30 @@ "name": "bubblegum", "instructions": [ { - "name": "createTree", + "name": "burn", + "docs": [ + "Burns a leaf node from the tree." + ], "accounts": [ { "name": "treeAuthority", - "isMut": true, + "isMut": false, "isSigner": false }, { - "name": "merkleTree", - "isMut": true, + "name": "leafOwner", + "isMut": false, "isSigner": false }, { - "name": "payer", - "isMut": true, - "isSigner": true + "name": "leafDelegate", + "isMut": false, + "isSigner": false }, { - "name": "treeCreator", - "isMut": false, - "isSigner": true + "name": "merkleTree", + "isMut": true, + "isSigner": false }, { "name": "logWrapper", @@ -43,69 +46,57 @@ ], "args": [ { - "name": "maxDepth", - "type": "u32" - }, - { - "name": "maxBufferSize", - "type": "u32" - }, - { - "name": "public", + "name": "root", "type": { - "option": "bool" + "array": [ + "u8", + 32 + ] } - } - ] - }, - { - "name": "setTreeDelegate", - "accounts": [ - { - "name": "treeAuthority", - "isMut": true, - "isSigner": false }, { - "name": "treeCreator", - "isMut": false, - "isSigner": true + "name": "dataHash", + "type": { + "array": [ + "u8", + 32 + ] + } }, { - "name": "newTreeDelegate", - "isMut": false, - "isSigner": false + "name": "creatorHash", + "type": { + "array": [ + "u8", + 32 + ] + } }, { - "name": "merkleTree", - "isMut": false, - "isSigner": false + "name": "nonce", + "type": "u64" }, { - "name": "systemProgram", - "isMut": false, - "isSigner": false + "name": "index", + "type": "u32" } - ], - "args": [] + ] }, { - "name": "mintV1", + "name": "cancelRedeem", + "docs": [ + "Cancels a redeem." + ], "accounts": [ { "name": "treeAuthority", - "isMut": true, - "isSigner": false - }, - { - "name": "leafOwner", "isMut": false, "isSigner": false }, { - "name": "leafDelegate", - "isMut": false, - "isSigner": false + "name": "leafOwner", + "isMut": true, + "isSigner": true }, { "name": "merkleTree", @@ -113,14 +104,9 @@ "isSigner": false }, { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "treeDelegate", - "isMut": false, - "isSigner": true + "name": "voucher", + "isMut": true, + "isSigner": false }, { "name": "logWrapper", @@ -140,25 +126,31 @@ ], "args": [ { - "name": "message", + "name": "root", "type": { - "defined": "MetadataArgs" + "array": [ + "u8", + 32 + ] } } ] }, { - "name": "mintToCollectionV1", + "name": "compress", + "docs": [ + "Compresses a metadata account." + ], "accounts": [ { "name": "treeAuthority", - "isMut": true, + "isMut": false, "isSigner": false }, { "name": "leafOwner", "isMut": false, - "isSigner": false + "isSigner": true }, { "name": "leafDelegate", @@ -167,65 +159,95 @@ }, { "name": "merkleTree", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "metadata", + "isMut": true, + "isSigner": false + }, + { + "name": "masterEdition", "isMut": true, "isSigner": false }, { "name": "payer", - "isMut": false, + "isMut": true, "isSigner": true }, { - "name": "treeDelegate", + "name": "logWrapper", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "collectionAuthority", + "name": "compressionProgram", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "collectionAuthorityRecordPda", + "name": "tokenProgram", "isMut": false, - "isSigner": false, - "docs": [ - "If there is no collecton authority record PDA then", - "this must be the Bubblegum program address." - ] + "isSigner": false }, { - "name": "collectionMint", + "name": "tokenMetadataProgram", "isMut": false, "isSigner": false }, { - "name": "collectionMetadata", + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "createTree", + "docs": [ + "Creates a new tree." + ], + "accounts": [ + { + "name": "treeAuthority", "isMut": true, "isSigner": false }, { - "name": "editionAccount", - "isMut": false, + "name": "merkleTree", + "isMut": true, "isSigner": false }, { - "name": "bubblegumSigner", - "isMut": false, - "isSigner": false + "name": "payer", + "isMut": true, + "isSigner": true }, { - "name": "logWrapper", + "name": "treeCreator", "isMut": false, - "isSigner": false + "isSigner": true }, { - "name": "compressionProgram", + "name": "logWrapper", "isMut": false, "isSigner": false }, { - "name": "tokenMetadataProgram", + "name": "compressionProgram", "isMut": false, "isSigner": false }, @@ -237,100 +259,96 @@ ], "args": [ { - "name": "metadataArgs", + "name": "maxDepth", + "type": "u32" + }, + { + "name": "maxBufferSize", + "type": "u32" + }, + { + "name": "public", "type": { - "defined": "MetadataArgs" + "option": "bool" } } ] }, { - "name": "verifyCreator", + "name": "decompressV1", + "docs": [ + "Decompresses a leaf node from the tree." + ], "accounts": [ { - "name": "treeAuthority", - "isMut": false, + "name": "voucher", + "isMut": true, "isSigner": false }, { "name": "leafOwner", - "isMut": false, - "isSigner": false + "isMut": true, + "isSigner": true }, { - "name": "leafDelegate", - "isMut": false, + "name": "tokenAccount", + "isMut": true, "isSigner": false }, { - "name": "merkleTree", + "name": "mint", "isMut": true, "isSigner": false }, { - "name": "payer", - "isMut": false, - "isSigner": true + "name": "mintAuthority", + "isMut": true, + "isSigner": false }, { - "name": "creator", - "isMut": false, - "isSigner": true + "name": "metadata", + "isMut": true, + "isSigner": false }, { - "name": "logWrapper", - "isMut": false, + "name": "masterEdition", + "isMut": true, "isSigner": false }, { - "name": "compressionProgram", + "name": "systemProgram", "isMut": false, "isSigner": false }, { - "name": "systemProgram", + "name": "sysvarRent", "isMut": false, "isSigner": false - } - ], - "args": [ - { - "name": "root", - "type": { - "array": [ - "u8", - 32 - ] - } }, { - "name": "dataHash", - "type": { - "array": [ - "u8", - 32 - ] - } + "name": "tokenMetadataProgram", + "isMut": false, + "isSigner": false }, { - "name": "creatorHash", - "type": { - "array": [ - "u8", - 32 - ] - } + "name": "tokenProgram", + "isMut": false, + "isSigner": false }, { - "name": "nonce", - "type": "u64" + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false }, { - "name": "index", - "type": "u32" - }, + "name": "logWrapper", + "isMut": false, + "isSigner": false + } + ], + "args": [ { - "name": "message", + "name": "metadata", "type": { "defined": "MetadataArgs" } @@ -338,7 +356,10 @@ ] }, { - "name": "unverifyCreator", + "name": "delegate", + "docs": [ + "Sets a delegate for a leaf node." + ], "accounts": [ { "name": "treeAuthority", @@ -348,10 +369,15 @@ { "name": "leafOwner", "isMut": false, + "isSigner": true + }, + { + "name": "previousLeafDelegate", + "isMut": false, "isSigner": false }, { - "name": "leafDelegate", + "name": "newLeafDelegate", "isMut": false, "isSigner": false }, @@ -360,16 +386,6 @@ "isMut": true, "isSigner": false }, - { - "name": "payer", - "isMut": false, - "isSigner": true - }, - { - "name": "creator", - "isMut": false, - "isSigner": true - }, { "name": "logWrapper", "isMut": false, @@ -421,21 +437,18 @@ { "name": "index", "type": "u32" - }, - { - "name": "message", - "type": { - "defined": "MetadataArgs" - } } ] }, { - "name": "verifyCollection", + "name": "mintToCollectionV1", + "docs": [ + "Mints a new asset and adds it to a collection." + ], "accounts": [ { "name": "treeAuthority", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -461,12 +474,7 @@ { "name": "treeDelegate", "isMut": false, - "isSigner": false, - "docs": [ - "This account is checked to be a signer in", - "the case of `set_and_verify_collection` where", - "we are actually changing the NFT metadata." - ] + "isSigner": true }, { "name": "collectionAuthority", @@ -500,67 +508,32 @@ { "name": "bubblegumSigner", "isMut": false, - "isSigner": false - }, - { - "name": "logWrapper", - "isMut": false, - "isSigner": false - }, - { - "name": "compressionProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenMetadataProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "root", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "dataHash", - "type": { - "array": [ - "u8", - 32 - ] - } + "isSigner": false }, { - "name": "creatorHash", - "type": { - "array": [ - "u8", - 32 - ] - } + "name": "logWrapper", + "isMut": false, + "isSigner": false }, { - "name": "nonce", - "type": "u64" + "name": "compressionProgram", + "isMut": false, + "isSigner": false }, { - "name": "index", - "type": "u32" + "name": "tokenMetadataProgram", + "isMut": false, + "isSigner": false }, { - "name": "message", + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "metadataArgs", "type": { "defined": "MetadataArgs" } @@ -568,11 +541,14 @@ ] }, { - "name": "unverifyCollection", + "name": "mintV1", + "docs": [ + "Mints a new asset." + ], "accounts": [ { "name": "treeAuthority", - "isMut": false, + "isMut": true, "isSigner": false }, { @@ -598,59 +574,73 @@ { "name": "treeDelegate", "isMut": false, - "isSigner": false, - "docs": [ - "This account is checked to be a signer in", - "the case of `set_and_verify_collection` where", - "we are actually changing the NFT metadata." - ] + "isSigner": true }, { - "name": "collectionAuthority", + "name": "logWrapper", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "collectionAuthorityRecordPda", + "name": "compressionProgram", "isMut": false, - "isSigner": false, - "docs": [ - "If there is no collecton authority record PDA then", - "this must be the Bubblegum program address." - ] + "isSigner": false }, { - "name": "collectionMint", + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "message", + "type": { + "defined": "MetadataArgs" + } + } + ] + }, + { + "name": "redeem", + "docs": [ + "Redeems a vouches.", + "", + "Once a vouch is redeemed, the corresponding leaf node is removed from the tree." + ], + "accounts": [ + { + "name": "treeAuthority", "isMut": false, "isSigner": false }, { - "name": "collectionMetadata", + "name": "leafOwner", "isMut": true, - "isSigner": false + "isSigner": true }, { - "name": "editionAccount", + "name": "leafDelegate", "isMut": false, "isSigner": false }, { - "name": "bubblegumSigner", - "isMut": false, + "name": "merkleTree", + "isMut": true, "isSigner": false }, { - "name": "logWrapper", - "isMut": false, + "name": "voucher", + "isMut": true, "isSigner": false }, { - "name": "compressionProgram", + "name": "logWrapper", "isMut": false, "isSigner": false }, { - "name": "tokenMetadataProgram", + "name": "compressionProgram", "isMut": false, "isSigner": false }, @@ -695,17 +685,14 @@ { "name": "index", "type": "u32" - }, - { - "name": "message", - "type": { - "defined": "MetadataArgs" - } } ] }, { "name": "setAndVerifyCollection", + "docs": [ + "Sets and verifies a collection to a leaf node" + ], "accounts": [ { "name": "treeAuthority", @@ -846,40 +833,80 @@ ] }, { - "name": "transfer", + "name": "setDecompressableState", + "docs": [ + "Sets the `decompressible_state` of a tree." + ], "accounts": [ { "name": "treeAuthority", - "isMut": false, + "isMut": true, "isSigner": false }, { - "name": "leafOwner", + "name": "treeCreator", "isMut": false, - "isSigner": false - }, + "isSigner": true + } + ], + "args": [ { - "name": "leafDelegate", - "isMut": false, + "name": "decompressableState", + "type": { + "defined": "DecompressibleState" + } + } + ] + }, + { + "name": "setDecompressibleState", + "docs": [ + "Sets the `decompressible_state` of a tree." + ], + "accounts": [ + { + "name": "treeAuthority", + "isMut": true, "isSigner": false }, { - "name": "newLeafOwner", + "name": "treeCreator", "isMut": false, - "isSigner": false - }, + "isSigner": true + } + ], + "args": [ { - "name": "merkleTree", + "name": "decompressableState", + "type": { + "defined": "DecompressibleState" + } + } + ] + }, + { + "name": "setTreeDelegate", + "docs": [ + "Sets a delegate for a tree." + ], + "accounts": [ + { + "name": "treeAuthority", "isMut": true, "isSigner": false }, { - "name": "logWrapper", + "name": "treeCreator", + "isMut": false, + "isSigner": true + }, + { + "name": "newTreeDelegate", "isMut": false, "isSigner": false }, { - "name": "compressionProgram", + "name": "merkleTree", "isMut": false, "isSigner": false }, @@ -889,46 +916,13 @@ "isSigner": false } ], - "args": [ - { - "name": "root", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "dataHash", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "creatorHash", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "nonce", - "type": "u64" - }, - { - "name": "index", - "type": "u32" - } - ] + "args": [] }, { - "name": "delegate", + "name": "transfer", + "docs": [ + "Transfers a leaf node from one account to another." + ], "accounts": [ { "name": "treeAuthority", @@ -938,15 +932,15 @@ { "name": "leafOwner", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "previousLeafDelegate", + "name": "leafDelegate", "isMut": false, "isSigner": false }, { - "name": "newLeafDelegate", + "name": "newLeafOwner", "isMut": false, "isSigner": false }, @@ -1010,7 +1004,10 @@ ] }, { - "name": "burn", + "name": "unverifyCollection", + "docs": [ + "Unverifies a collection from a leaf node." + ], "accounts": [ { "name": "treeAuthority", @@ -1028,8 +1025,57 @@ "isSigner": false }, { - "name": "merkleTree", - "isMut": true, + "name": "merkleTree", + "isMut": true, + "isSigner": false + }, + { + "name": "payer", + "isMut": false, + "isSigner": true + }, + { + "name": "treeDelegate", + "isMut": false, + "isSigner": false, + "docs": [ + "This account is checked to be a signer in", + "the case of `set_and_verify_collection` where", + "we are actually changing the NFT metadata." + ] + }, + { + "name": "collectionAuthority", + "isMut": false, + "isSigner": true + }, + { + "name": "collectionAuthorityRecordPda", + "isMut": false, + "isSigner": false, + "docs": [ + "If there is no collecton authority record PDA then", + "this must be the Bubblegum program address." + ] + }, + { + "name": "collectionMint", + "isMut": false, + "isSigner": false + }, + { + "name": "collectionMetadata", + "isMut": true, + "isSigner": false + }, + { + "name": "editionAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "bubblegumSigner", + "isMut": false, "isSigner": false }, { @@ -1042,6 +1088,11 @@ "isMut": false, "isSigner": false }, + { + "name": "tokenMetadataProgram", + "isMut": false, + "isSigner": false + }, { "name": "systemProgram", "isMut": false, @@ -1083,11 +1134,20 @@ { "name": "index", "type": "u32" + }, + { + "name": "message", + "type": { + "defined": "MetadataArgs" + } } ] }, { - "name": "redeem", + "name": "unverifyCreator", + "docs": [ + "Unverifies a creator from a leaf node." + ], "accounts": [ { "name": "treeAuthority", @@ -1096,8 +1156,8 @@ }, { "name": "leafOwner", - "isMut": true, - "isSigner": true + "isMut": false, + "isSigner": false }, { "name": "leafDelegate", @@ -1110,9 +1170,14 @@ "isSigner": false }, { - "name": "voucher", - "isMut": true, - "isSigner": false + "name": "payer", + "isMut": false, + "isSigner": true + }, + { + "name": "creator", + "isMut": false, + "isSigner": true }, { "name": "logWrapper", @@ -1165,11 +1230,20 @@ { "name": "index", "type": "u32" + }, + { + "name": "message", + "type": { + "defined": "MetadataArgs" + } } ] }, { - "name": "cancelRedeem", + "name": "verifyCollection", + "docs": [ + "Verifies a collection for a leaf node." + ], "accounts": [ { "name": "treeAuthority", @@ -1178,119 +1252,127 @@ }, { "name": "leafOwner", - "isMut": true, - "isSigner": true - }, - { - "name": "merkleTree", - "isMut": true, - "isSigner": false - }, - { - "name": "voucher", - "isMut": true, - "isSigner": false - }, - { - "name": "logWrapper", "isMut": false, "isSigner": false }, { - "name": "compressionProgram", + "name": "leafDelegate", "isMut": false, "isSigner": false }, { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "root", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ] - }, - { - "name": "decompressV1", - "accounts": [ - { - "name": "voucher", + "name": "merkleTree", "isMut": true, "isSigner": false }, { - "name": "leafOwner", - "isMut": true, + "name": "payer", + "isMut": false, "isSigner": true }, { - "name": "tokenAccount", - "isMut": true, - "isSigner": false + "name": "treeDelegate", + "isMut": false, + "isSigner": false, + "docs": [ + "This account is checked to be a signer in", + "the case of `set_and_verify_collection` where", + "we are actually changing the NFT metadata." + ] }, { - "name": "mint", - "isMut": true, - "isSigner": false + "name": "collectionAuthority", + "isMut": false, + "isSigner": true }, { - "name": "mintAuthority", - "isMut": true, - "isSigner": false + "name": "collectionAuthorityRecordPda", + "isMut": false, + "isSigner": false, + "docs": [ + "If there is no collecton authority record PDA then", + "this must be the Bubblegum program address." + ] }, { - "name": "metadata", - "isMut": true, + "name": "collectionMint", + "isMut": false, "isSigner": false }, { - "name": "masterEdition", + "name": "collectionMetadata", "isMut": true, "isSigner": false }, { - "name": "systemProgram", + "name": "editionAccount", "isMut": false, "isSigner": false }, { - "name": "sysvarRent", + "name": "bubblegumSigner", "isMut": false, "isSigner": false }, { - "name": "tokenMetadataProgram", + "name": "logWrapper", "isMut": false, "isSigner": false }, { - "name": "tokenProgram", + "name": "compressionProgram", "isMut": false, "isSigner": false }, { - "name": "associatedTokenProgram", + "name": "tokenMetadataProgram", "isMut": false, "isSigner": false }, { - "name": "logWrapper", + "name": "systemProgram", "isMut": false, "isSigner": false } ], "args": [ { - "name": "metadata", + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "dataHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "creatorHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "index", + "type": "u32" + }, + { + "name": "message", "type": { "defined": "MetadataArgs" } @@ -1298,7 +1380,10 @@ ] }, { - "name": "compress", + "name": "verifyCreator", + "docs": [ + "Verifies a creator for a leaf node." + ], "accounts": [ { "name": "treeAuthority", @@ -1308,7 +1393,7 @@ { "name": "leafOwner", "isMut": false, - "isSigner": true + "isSigner": false }, { "name": "leafDelegate", @@ -1317,32 +1402,17 @@ }, { "name": "merkleTree", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenAccount", - "isMut": true, - "isSigner": false - }, - { - "name": "mint", - "isMut": true, - "isSigner": false - }, - { - "name": "metadata", "isMut": true, "isSigner": false }, { - "name": "masterEdition", - "isMut": true, - "isSigner": false + "name": "payer", + "isMut": false, + "isSigner": true }, { - "name": "payer", - "isMut": true, + "name": "creator", + "isMut": false, "isSigner": true }, { @@ -1356,42 +1426,51 @@ "isSigner": false }, { - "name": "tokenProgram", + "name": "systemProgram", "isMut": false, "isSigner": false + } + ], + "args": [ + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } }, { - "name": "tokenMetadataProgram", - "isMut": false, - "isSigner": false + "name": "dataHash", + "type": { + "array": [ + "u8", + 32 + ] + } }, { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "setDecompressableState", - "accounts": [ + "name": "creatorHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, { - "name": "treeAuthority", - "isMut": true, - "isSigner": false + "name": "nonce", + "type": "u64" }, { - "name": "treeCreator", - "isMut": false, - "isSigner": true - } - ], - "args": [ + "name": "index", + "type": "u32" + }, { - "name": "decompressableState", + "name": "message", "type": { - "defined": "DecompressableState" + "defined": "MetadataArgs" } } ] @@ -1424,9 +1503,9 @@ "type": "bool" }, { - "name": "isDecompressable", + "name": "isDecompressible", "type": { - "defined": "DecompressableState" + "defined": "DecompressibleState" } } ] @@ -1743,7 +1822,7 @@ } }, { - "name": "DecompressableState", + "name": "DecompressibleState", "type": { "kind": "enum", "variants": [ @@ -1810,7 +1889,7 @@ "name": "MintToCollectionV1" }, { - "name": "SetDecompressableState" + "name": "SetDecompressibleState" } ] } diff --git a/programs/bubblegum/program/src/asserts.rs b/programs/bubblegum/program/src/asserts.rs new file mode 100644 index 00000000..aef05bca --- /dev/null +++ b/programs/bubblegum/program/src/asserts.rs @@ -0,0 +1,152 @@ +use crate::{error::BubblegumError, state::metaplex_adapter::MetadataArgs, utils::cmp_pubkeys}; +use anchor_lang::prelude::*; +use mpl_token_metadata::{ + instruction::MetadataDelegateRole, + pda::{find_collection_authority_account, find_metadata_delegate_record_account}, + state::{CollectionAuthorityRecord, Metadata, MetadataDelegateRecord, TokenMetadataAccount}, +}; + +/// Assert that the provided MetadataArgs are compatible with MPL `Data` +pub fn assert_metadata_is_mpl_compatible(metadata: &MetadataArgs) -> Result<()> { + if metadata.name.len() > mpl_token_metadata::state::MAX_NAME_LENGTH { + return Err(BubblegumError::MetadataNameTooLong.into()); + } + + if metadata.symbol.len() > mpl_token_metadata::state::MAX_SYMBOL_LENGTH { + return Err(BubblegumError::MetadataSymbolTooLong.into()); + } + + if metadata.uri.len() > mpl_token_metadata::state::MAX_URI_LENGTH { + return Err(BubblegumError::MetadataUriTooLong.into()); + } + + if metadata.seller_fee_basis_points > 10000 { + return Err(BubblegumError::MetadataBasisPointsTooHigh.into()); + } + if !metadata.creators.is_empty() { + if metadata.creators.len() > mpl_token_metadata::state::MAX_CREATOR_LIMIT { + return Err(BubblegumError::CreatorsTooLong.into()); + } + + let mut total: u8 = 0; + for i in 0..metadata.creators.len() { + let creator = &metadata.creators[i]; + for iter in metadata.creators.iter().skip(i + 1) { + if iter.address == creator.address { + return Err(BubblegumError::DuplicateCreatorAddress.into()); + } + } + total = total + .checked_add(creator.share) + .ok_or(BubblegumError::CreatorShareTotalMustBe100)?; + } + if total != 100 { + return Err(BubblegumError::CreatorShareTotalMustBe100.into()); + } + } + Ok(()) +} + +pub fn assert_pubkey_equal( + a: &Pubkey, + b: &Pubkey, + error: Option, +) -> Result<()> { + if !cmp_pubkeys(a, b) { + if let Some(err) = error { + Err(err) + } else { + Err(BubblegumError::PublicKeyMismatch.into()) + } + } else { + Ok(()) + } +} + +pub fn assert_derivation( + program_id: &Pubkey, + account: &AccountInfo, + path: &[&[u8]], + error: Option, +) -> Result { + let (key, bump) = Pubkey::find_program_address(path, program_id); + if !cmp_pubkeys(&key, account.key) { + if let Some(err) = error { + msg!("Derivation {:?}", err); + Err(err) + } else { + msg!("DerivedKeyInvalid"); + Err(ProgramError::InvalidInstructionData.into()) + } + } else { + Ok(bump) + } +} + +pub fn assert_owned_by(account: &AccountInfo, owner: &Pubkey) -> Result<()> { + if !cmp_pubkeys(account.owner, owner) { + //todo add better errors + Err(ProgramError::IllegalOwner.into()) + } else { + Ok(()) + } +} + +// Checks both delegate types: old collection_authority_record and newer +// metadata_delegate +pub fn assert_has_collection_authority( + collection_data: &Metadata, + mint: &Pubkey, + collection_authority: &Pubkey, + delegate_record: Option<&AccountInfo>, +) -> Result<()> { + // Mint is the correct one for the metadata account. + if collection_data.mint != *mint { + return Err(BubblegumError::MetadataMintMismatch.into()); + } + + if let Some(record_info) = delegate_record { + let (ca_pda, ca_bump) = find_collection_authority_account(mint, collection_authority); + let (md_pda, md_bump) = find_metadata_delegate_record_account( + mint, + MetadataDelegateRole::Collection, + &collection_data.update_authority, + collection_authority, + ); + + let data = record_info.try_borrow_data()?; + if data.len() == 0 { + return Err(BubblegumError::InvalidCollectionAuthority.into()); + } + + if record_info.key == &ca_pda { + let record = CollectionAuthorityRecord::safe_deserialize(&data)?; + if record.bump != ca_bump { + return Err(BubblegumError::InvalidCollectionAuthority.into()); + } + + match record.update_authority { + Some(update_authority) => { + if update_authority != collection_data.update_authority { + return Err(BubblegumError::InvalidCollectionAuthority.into()); + } + } + None => return Err(BubblegumError::InvalidCollectionAuthority.into()), + } + } else if record_info.key == &md_pda { + let record = MetadataDelegateRecord::safe_deserialize(&data)?; + if record.bump != md_bump { + return Err(BubblegumError::InvalidCollectionAuthority.into()); + } + + if record.update_authority != collection_data.update_authority { + return Err(BubblegumError::InvalidCollectionAuthority.into()); + } + } else { + return Err(BubblegumError::InvalidDelegateRecord.into()); + } + } else if collection_data.update_authority != *collection_authority { + return Err(BubblegumError::InvalidCollectionAuthority.into()); + } + Ok(()) +} diff --git a/programs/bubblegum/program/src/lib.rs b/programs/bubblegum/program/src/lib.rs index 9373d7ae..4883a79e 100644 --- a/programs/bubblegum/program/src/lib.rs +++ b/programs/bubblegum/program/src/lib.rs @@ -1,462 +1,18 @@ #![allow(clippy::result_large_err)] #![allow(clippy::too_many_arguments)] -use crate::{ - error::{metadata_error_into_bubblegum, BubblegumError}, - state::{ - leaf_schema::LeafSchema, - metaplex_adapter::{self, Creator, MetadataArgs, TokenProgramVersion}, - metaplex_anchor::{MasterEdition, MplTokenMetadata, TokenMetadata}, - DecompressableState, TreeConfig, Voucher, ASSET_PREFIX, COLLECTION_CPI_PREFIX, - TREE_AUTHORITY_SIZE, VOUCHER_PREFIX, VOUCHER_SIZE, - }, - utils::{ - append_leaf, assert_has_collection_authority, assert_metadata_is_mpl_compatible, - assert_pubkey_equal, cmp_bytes, cmp_pubkeys, get_asset_id, replace_leaf, - }, -}; -use anchor_lang::{ - prelude::*, - solana_program::{ - account_info::AccountInfo, - keccak, - program::{invoke, invoke_signed}, - program_error::ProgramError, - program_pack::Pack, - system_instruction, - }, - system_program::System, -}; -use anchor_spl::{associated_token::AssociatedToken, token::Token}; -use mpl_token_metadata::{ - assertions::collection::assert_collection_verify_is_valid, state::CollectionDetails, -}; -use spl_account_compression::{ - program::SplAccountCompression, wrap_application_data_v1, Node, Noop, -}; -use spl_token::state::Mint as SplMint; -use std::collections::HashSet; +use anchor_lang::prelude::*; +pub mod asserts; pub mod error; +mod processor; pub mod state; pub mod utils; -declare_id!("BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY"); - -#[derive(Accounts)] -pub struct CreateTree<'info> { - #[account( - init, - seeds = [merkle_tree.key().as_ref()], - payer = payer, - space = TREE_AUTHORITY_SIZE, - bump, - )] - pub tree_authority: Account<'info, TreeConfig>, - #[account(zero)] - /// CHECK: This account must be all zeros - pub merkle_tree: UncheckedAccount<'info>, - #[account(mut)] - pub payer: Signer<'info>, - pub tree_creator: Signer<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct MintV1<'info> { - #[account( - mut, - seeds = [merkle_tree.key().as_ref()], - bump, - )] - pub tree_authority: Account<'info, TreeConfig>, - /// CHECK: This account is neither written to nor read from. - pub leaf_owner: AccountInfo<'info>, - /// CHECK: This account is neither written to nor read from. - pub leaf_delegate: AccountInfo<'info>, - #[account(mut)] - /// CHECK: unsafe - 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>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct MintToCollectionV1<'info> { - #[account( - mut, - seeds = [merkle_tree.key().as_ref()], - bump, - )] - pub tree_authority: Account<'info, TreeConfig>, - /// CHECK: This account is neither written to nor read from. - pub leaf_owner: AccountInfo<'info>, - /// CHECK: This account is neither written to nor read from. - pub leaf_delegate: AccountInfo<'info>, - #[account(mut)] - /// CHECK: unsafe - pub merkle_tree: UncheckedAccount<'info>, - pub payer: Signer<'info>, - pub tree_delegate: Signer<'info>, - pub collection_authority: Signer<'info>, - /// CHECK: Optional collection authority record PDA. - /// If there is no collecton authority record PDA then - /// this must be the Bubblegum program address. - pub collection_authority_record_pda: UncheckedAccount<'info>, - /// CHECK: This account is checked in the instruction - pub collection_mint: UncheckedAccount<'info>, - #[account(mut)] - pub collection_metadata: Box>, - /// CHECK: This account is checked in the instruction - pub edition_account: UncheckedAccount<'info>, - /// CHECK: This is just used as a signing PDA. - #[account( - seeds = [COLLECTION_CPI_PREFIX.as_ref()], - bump, - )] - pub bubblegum_signer: UncheckedAccount<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, - pub token_metadata_program: Program<'info, MplTokenMetadata>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct Burn<'info> { - #[account( - seeds = [merkle_tree.key().as_ref()], - bump, - )] - pub tree_authority: Account<'info, TreeConfig>, - /// CHECK: This account is checked in the instruction - pub leaf_owner: UncheckedAccount<'info>, - /// CHECK: This account is checked in the instruction - pub leaf_delegate: UncheckedAccount<'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>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct CreatorVerification<'info> { - #[account( - seeds = [merkle_tree.key().as_ref()], - bump, - )] - pub tree_authority: Account<'info, TreeConfig>, - /// CHECK: This account is checked in the instruction - pub leaf_owner: UncheckedAccount<'info>, - /// CHECK: This account is checked in the instruction - pub leaf_delegate: UncheckedAccount<'info>, - #[account(mut)] - /// CHECK: This account is modified in the downstream program - 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>, - pub system_program: Program<'info, System>, -} +use processor::*; +use state::{metaplex_adapter::MetadataArgs, DecompressibleState}; -#[derive(Accounts)] -pub struct CollectionVerification<'info> { - #[account( - seeds = [merkle_tree.key().as_ref()], - bump, - )] - pub tree_authority: Account<'info, TreeConfig>, - /// CHECK: This account is checked in the instruction - pub leaf_owner: UncheckedAccount<'info>, - /// CHECK: This account is checked in the instruction - pub leaf_delegate: UncheckedAccount<'info>, - #[account(mut)] - /// CHECK: This account is modified in the downstream program - pub merkle_tree: UncheckedAccount<'info>, - pub payer: Signer<'info>, - /// CHECK: This account is checked in the instruction - /// This account is checked to be a signer in - /// the case of `set_and_verify_collection` where - /// we are actually changing the NFT metadata. - pub tree_delegate: UncheckedAccount<'info>, - pub collection_authority: Signer<'info>, - /// CHECK: Optional collection authority record PDA. - /// If there is no collecton authority record PDA then - /// this must be the Bubblegum program address. - pub collection_authority_record_pda: UncheckedAccount<'info>, - /// CHECK: This account is checked in the instruction - pub collection_mint: UncheckedAccount<'info>, - #[account(mut)] - pub collection_metadata: Box>, - /// CHECK: This account is checked in the instruction - pub edition_account: UncheckedAccount<'info>, - /// CHECK: This is just used as a signing PDA. - #[account( - seeds = [COLLECTION_CPI_PREFIX.as_ref()], - bump, - )] - pub bubblegum_signer: UncheckedAccount<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, - pub token_metadata_program: Program<'info, MplTokenMetadata>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct Transfer<'info> { - #[account( - seeds = [merkle_tree.key().as_ref()], - bump, - )] - /// CHECK: This account is neither written to nor read from. - pub tree_authority: Account<'info, TreeConfig>, - /// CHECK: This account is checked in the instruction - pub leaf_owner: UncheckedAccount<'info>, - /// CHECK: This account is chekced in the instruction - pub leaf_delegate: UncheckedAccount<'info>, - /// CHECK: This account is neither written to nor read from. - pub new_leaf_owner: UncheckedAccount<'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>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct Delegate<'info> { - #[account( - seeds = [merkle_tree.key().as_ref()], - bump, - )] - /// CHECK: This account is neither written to nor read from. - pub tree_authority: Account<'info, TreeConfig>, - pub leaf_owner: Signer<'info>, - /// CHECK: This account is neither written to nor read from. - pub previous_leaf_delegate: UncheckedAccount<'info>, - /// CHECK: This account is neither written to nor read from. - pub new_leaf_delegate: UncheckedAccount<'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>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -#[instruction( - _root: [u8; 32], - _data_hash: [u8; 32], - _creator_hash: [u8; 32], - nonce: u64, - _index: u32, -)] -pub struct Redeem<'info> { - #[account( - seeds = [merkle_tree.key().as_ref()], - bump, - )] - /// CHECK: This account is neither written to nor read from. - pub tree_authority: Account<'info, TreeConfig>, - #[account(mut)] - pub leaf_owner: Signer<'info>, - /// CHECK: This account is chekced in the instruction - pub leaf_delegate: UncheckedAccount<'info>, - #[account(mut)] - /// CHECK: checked in cpi - pub merkle_tree: UncheckedAccount<'info>, - #[account( - init, - seeds = [ - VOUCHER_PREFIX.as_ref(), - merkle_tree.key().as_ref(), - & nonce.to_le_bytes() - ], - payer = leaf_owner, - space = VOUCHER_SIZE, - bump - )] - pub voucher: Account<'info, Voucher>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct CancelRedeem<'info> { - #[account( - seeds = [merkle_tree.key().as_ref()], - bump, - )] - /// CHECK: This account is neither written to nor read from. - pub tree_authority: Account<'info, TreeConfig>, - #[account(mut)] - pub leaf_owner: Signer<'info>, - #[account(mut)] - /// CHECK: unsafe - pub merkle_tree: UncheckedAccount<'info>, - #[account( - mut, - close = leaf_owner, - seeds = [ - VOUCHER_PREFIX.as_ref(), - merkle_tree.key().as_ref(), - & voucher.leaf_schema.nonce().to_le_bytes() - ], - bump - )] - pub voucher: Account<'info, Voucher>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct DecompressV1<'info> { - #[account( - mut, - close = leaf_owner, - seeds = [ - VOUCHER_PREFIX.as_ref(), - voucher.merkle_tree.as_ref(), - voucher.leaf_schema.nonce().to_le_bytes().as_ref() - ], - bump - )] - pub voucher: Box>, - #[account(mut)] - pub leaf_owner: Signer<'info>, - /// CHECK: versioning is handled in the instruction - #[account(mut)] - pub token_account: UncheckedAccount<'info>, - /// CHECK: versioning is handled in the instruction - #[account( - mut, - seeds = [ - ASSET_PREFIX.as_ref(), - voucher.merkle_tree.as_ref(), - voucher.leaf_schema.nonce().to_le_bytes().as_ref(), - ], - bump - )] - pub mint: UncheckedAccount<'info>, - /// CHECK: - #[account( - mut, - seeds = [mint.key().as_ref()], - bump, - )] - pub mint_authority: UncheckedAccount<'info>, - /// CHECK: - #[account(mut)] - pub metadata: UncheckedAccount<'info>, - /// CHECK: Initialized in Token Metadata Program - #[account(mut)] - pub master_edition: UncheckedAccount<'info>, - pub system_program: Program<'info, System>, - pub sysvar_rent: Sysvar<'info, Rent>, - /// CHECK: - 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>, -} - -#[derive(Accounts)] -pub struct Compress<'info> { - #[account( - seeds = [merkle_tree.key().as_ref()], - bump, - )] - /// CHECK: This account is neither written to nor read from. - pub tree_authority: UncheckedAccount<'info>, - /// CHECK: This account is checked in the instruction - pub leaf_owner: Signer<'info>, - /// CHECK: This account is chekced in the instruction - pub leaf_delegate: UncheckedAccount<'info>, - /// CHECK: This account is not read - pub merkle_tree: UncheckedAccount<'info>, - - /// CHECK: versioning is handled in the instruction - #[account(mut)] - pub token_account: AccountInfo<'info>, - /// CHECK: versioning is handled in the instruction - #[account(mut)] - pub mint: AccountInfo<'info>, - #[account(mut)] - pub metadata: Box>, - #[account(mut)] - pub master_edition: Box>, - #[account(mut)] - pub payer: Signer<'info>, - pub log_wrapper: Program<'info, Noop>, - pub compression_program: Program<'info, SplAccountCompression>, - /// CHECK: - pub token_program: UncheckedAccount<'info>, - /// CHECK: - pub token_metadata_program: UncheckedAccount<'info>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct SetTreeDelegate<'info> { - #[account( - mut, - seeds = [merkle_tree.key().as_ref()], - bump, - has_one = tree_creator - )] - pub tree_authority: Account<'info, TreeConfig>, - pub tree_creator: Signer<'info>, - /// CHECK: this account is neither read from or written to - pub new_tree_delegate: UncheckedAccount<'info>, - /// CHECK: this account is neither read from or written to - pub merkle_tree: UncheckedAccount<'info>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct SetDecompressableState<'info> { - #[account(mut, has_one = tree_creator)] - pub tree_authority: Account<'info, TreeConfig>, - pub tree_creator: Signer<'info>, -} - -pub fn hash_creators(creators: &[Creator]) -> Result<[u8; 32]> { - // Convert creator Vec to bytes Vec. - let creator_data = creators - .iter() - .map(|c| [c.address.as_ref(), &[c.verified as u8], &[c.share]].concat()) - .collect::>(); - // Calculate new creator hash. - Ok(keccak::hashv( - creator_data - .iter() - .map(|c| c.as_slice()) - .collect::>() - .as_ref(), - ) - .to_bytes()) -} - -pub fn hash_metadata(metadata: &MetadataArgs) -> Result<[u8; 32]> { - let metadata_args_hash = keccak::hashv(&[metadata.try_to_vec()?.as_slice()]); - // Calculate new data hash. - Ok(keccak::hashv(&[ - &metadata_args_hash.to_bytes(), - &metadata.seller_fee_basis_points.to_le_bytes(), - ]) - .to_bytes()) -} +declare_id!("BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY"); pub enum InstructionName { Unknown, @@ -475,7 +31,7 @@ pub enum InstructionName { UnverifyCollection, SetAndVerifyCollection, MintToCollectionV1, - SetDecompressableState, + SetDecompressibleState, } pub fn get_instruction_type(full_bytes: &[u8]) -> InstructionName { @@ -500,616 +56,100 @@ pub fn get_instruction_type(full_bytes: &[u8]) -> InstructionName { [56, 113, 101, 253, 79, 55, 122, 169] => InstructionName::VerifyCollection, [250, 251, 42, 106, 41, 137, 186, 168] => InstructionName::UnverifyCollection, [235, 242, 121, 216, 158, 234, 180, 234] => InstructionName::SetAndVerifyCollection, - [37, 232, 198, 199, 64, 102, 128, 49] => InstructionName::SetDecompressableState, + [82, 104, 152, 6, 149, 111, 100, 13] => InstructionName::SetDecompressibleState, + // `SetDecompressableState` instruction mapped to `SetDecompressibleState` instruction + [18, 135, 238, 168, 246, 195, 61, 115] => InstructionName::SetDecompressibleState, _ => InstructionName::Unknown, } } -fn process_mint_v1<'info>( - message: MetadataArgs, - owner: Pubkey, - delegate: Pubkey, - metadata_auth: HashSet, - authority_bump: u8, - authority: &mut Account<'info, TreeConfig>, - merkle_tree: &AccountInfo<'info>, - wrapper: &Program<'info, Noop>, - compression_program: &AccountInfo<'info>, - allow_verified_collection: bool, -) -> Result<()> { - assert_metadata_is_mpl_compatible(&message)?; - if !allow_verified_collection { - if let Some(collection) = &message.collection { - if collection.verified { - return Err(BubblegumError::CollectionCannotBeVerifiedInThisInstruction.into()); - } - } - } - - // @dev: seller_fee_basis points is encoded twice so that it can be passed to marketplace - // instructions, without passing the entire, un-hashed MetadataArgs struct - let metadata_args_hash = keccak::hashv(&[message.try_to_vec()?.as_slice()]); - let data_hash = keccak::hashv(&[ - &metadata_args_hash.to_bytes(), - &message.seller_fee_basis_points.to_le_bytes(), - ]); - - // Use the metadata auth to check whether we can allow `verified` to be set to true in the - // creator Vec. - let creator_data = message - .creators - .iter() - .map(|c| { - if c.verified && !metadata_auth.contains(&c.address) { - Err(BubblegumError::CreatorDidNotVerify.into()) - } else { - Ok([c.address.as_ref(), &[c.verified as u8], &[c.share]].concat()) - } - }) - .collect::>>()?; - - // Calculate creator hash. - let creator_hash = keccak::hashv( - creator_data - .iter() - .map(|c| c.as_slice()) - .collect::>() - .as_ref(), - ); - - let asset_id = get_asset_id(&merkle_tree.key(), authority.num_minted); - let leaf = LeafSchema::new_v0( - asset_id, - owner, - delegate, - authority.num_minted, - data_hash.to_bytes(), - creator_hash.to_bytes(), - ); - - wrap_application_data_v1(leaf.to_event().try_to_vec()?, wrapper)?; - - append_leaf( - &merkle_tree.key(), - authority_bump, - &compression_program.to_account_info(), - &authority.to_account_info(), - &merkle_tree.to_account_info(), - &wrapper.to_account_info(), - leaf.to_node(), - ) -} - -fn process_creator_verification<'info>( - ctx: Context<'_, '_, '_, 'info, CreatorVerification<'info>>, - root: [u8; 32], - data_hash: [u8; 32], - creator_hash: [u8; 32], - nonce: u64, - index: u32, - mut message: MetadataArgs, - verify: bool, -) -> Result<()> { - let owner = ctx.accounts.leaf_owner.to_account_info(); - let delegate = ctx.accounts.leaf_delegate.to_account_info(); - let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); - - let creator = ctx.accounts.creator.key(); - - // Creator Vec must contain creators. - if message.creators.is_empty() { - return Err(BubblegumError::NoCreatorsPresent.into()); - } - - // Creator must be in user-provided creator Vec. - if !message.creators.iter().any(|c| c.address == creator) { - return Err(BubblegumError::CreatorNotFound.into()); - } - - // User-provided creator Vec must result in same user-provided creator hash. - let incoming_creator_hash = hash_creators(&message.creators)?; - if creator_hash != incoming_creator_hash { - return Err(BubblegumError::CreatorHashMismatch.into()); - } - - // User-provided metadata must result in same user-provided data hash. - let incoming_data_hash = hash_metadata(&message)?; - if data_hash != incoming_data_hash { - return Err(BubblegumError::DataHashMismatch.into()); - } - - // Calculate new creator Vec with `verified` set to true for signing creator. - let updated_creator_vec = message - .creators - .iter() - .map(|c| { - let verified = if c.address == creator.key() { - verify - } else { - c.verified - }; - Creator { - address: c.address, - verified, - share: c.share, - } - }) - .collect::>(); - - // Calculate new creator hash. - let updated_creator_hash = hash_creators(&updated_creator_vec)?; - - // Update creator Vec in metadata args. - message.creators = updated_creator_vec; - - // Calculate new data hash. - let updated_data_hash = hash_metadata(&message)?; - - // Build previous leaf struct, new leaf struct, and replace the leaf in the tree. - let asset_id = get_asset_id(&merkle_tree.key(), nonce); - let previous_leaf = LeafSchema::new_v0( - asset_id, - owner.key(), - delegate.key(), - nonce, - data_hash, - creator_hash, - ); - let new_leaf = LeafSchema::new_v0( - asset_id, - owner.key(), - delegate.key(), - nonce, - updated_data_hash, - updated_creator_hash, - ); - - wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; - - replace_leaf( - &merkle_tree.key(), - *ctx.bumps.get("tree_authority").unwrap(), - &ctx.accounts.compression_program.to_account_info(), - &ctx.accounts.tree_authority.to_account_info(), - &ctx.accounts.merkle_tree.to_account_info(), - &ctx.accounts.log_wrapper.to_account_info(), - ctx.remaining_accounts, - root, - previous_leaf.to_node(), - new_leaf.to_node(), - index, - ) -} - -fn process_collection_verification_mpl_only<'info>( - collection_metadata: &Account<'info, TokenMetadata>, - collection_mint: &AccountInfo<'info>, - collection_authority: &AccountInfo<'info>, - collection_authority_record_pda: &AccountInfo<'info>, - edition_account: &AccountInfo<'info>, - bubblegum_signer: &AccountInfo<'info>, - bubblegum_bump: u8, - token_metadata_program: &AccountInfo<'info>, - message: &mut MetadataArgs, - verify: bool, - new_collection: Option, -) -> Result<()> { - // See if a collection authority record PDA was provided. - let collection_authority_record = if collection_authority_record_pda.key() == crate::id() { - None - } else { - Some(collection_authority_record_pda) - }; - - // Verify correct account ownerships. - require!( - *collection_metadata.to_account_info().owner == token_metadata_program.key(), - BubblegumError::IncorrectOwner - ); - require!( - *collection_mint.owner == spl_token::id(), - BubblegumError::IncorrectOwner - ); - require!( - *edition_account.owner == token_metadata_program.key(), - BubblegumError::IncorrectOwner - ); - - // If new collection was provided, set it in the NFT metadata. - if new_collection.is_some() { - message.collection = new_collection.map(|key| metaplex_adapter::Collection { - verified: false, // Set to true below. - key, - }); - } - - // If the NFT has collection data, we set it to the correct value after doing some validation. - if let Some(collection) = &mut message.collection { - // Don't verify already verified items, or unverify unverified items, otherwise for sized - // collections we end up with invalid size data. - if verify && collection.verified { - return Err(BubblegumError::AlreadyVerified.into()); - } else if !verify && !collection.verified { - return Err(BubblegumError::AlreadyUnverified.into()); - } - - // Collection verify assert from token-metadata program. - assert_collection_verify_is_valid( - &Some(collection.adapt()), - collection_metadata, - collection_mint, - edition_account, - ) - .map_err(metadata_error_into_bubblegum)?; +#[program] +pub mod bubblegum { - assert_has_collection_authority( - collection_metadata, - collection_mint.key, - collection_authority.key, - collection_authority_record, - )?; + use super::*; - // Update collection in metadata args. Note since this is a mutable reference, - // it is still updating `message.collection` after being destructured. - collection.verified = verify; - } else { - return Err(BubblegumError::CollectionNotFound.into()); + /// Burns a leaf node from the tree. + pub fn burn<'info>( + ctx: Context<'_, '_, '_, 'info, Burn<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + ) -> Result<()> { + processor::burn(ctx, root, data_hash, creator_hash, nonce, index) } - // If this is a sized collection, then increment or decrement collection size. - if let Some(details) = &collection_metadata.collection_details { - // Increment or decrement existing size. - let new_size = match details { - CollectionDetails::V1 { size } => { - if verify { - size.checked_add(1) - .ok_or(BubblegumError::NumericalOverflowError)? - } else { - size.checked_sub(1) - .ok_or(BubblegumError::NumericalOverflowError)? - } - } - }; - - // CPI into to token-metadata program to change the collection size. - let mut bubblegum_set_collection_size_infos = vec![ - collection_metadata.to_account_info(), - collection_authority.clone(), - collection_mint.clone(), - bubblegum_signer.clone(), - ]; - - if let Some(record) = collection_authority_record { - bubblegum_set_collection_size_infos.push(record.clone()); - } - - invoke_signed( - &mpl_token_metadata::instruction::bubblegum_set_collection_size( - token_metadata_program.key(), - collection_metadata.to_account_info().key(), - collection_authority.key(), - collection_mint.key(), - bubblegum_signer.key(), - collection_authority_record.map(|r| r.key()), - new_size, - ), - bubblegum_set_collection_size_infos.as_slice(), - &[&[COLLECTION_CPI_PREFIX.as_bytes(), &[bubblegum_bump]]], - )?; - } else { - return Err(BubblegumError::CollectionMustBeSized.into()); + /// Cancels a redeem. + pub fn cancel_redeem<'info>( + ctx: Context<'_, '_, '_, 'info, CancelRedeem<'info>>, + root: [u8; 32], + ) -> Result<()> { + processor::cancel_redeem(ctx, root) } - Ok(()) -} - -fn process_collection_verification<'info>( - ctx: Context<'_, '_, '_, 'info, CollectionVerification<'info>>, - root: [u8; 32], - data_hash: [u8; 32], - creator_hash: [u8; 32], - nonce: u64, - index: u32, - mut message: MetadataArgs, - verify: bool, - new_collection: Option, -) -> Result<()> { - let owner = ctx.accounts.leaf_owner.to_account_info(); - let delegate = ctx.accounts.leaf_delegate.to_account_info(); - let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); - let collection_metadata = &ctx.accounts.collection_metadata; - let collection_mint = ctx.accounts.collection_mint.to_account_info(); - let edition_account = ctx.accounts.edition_account.to_account_info(); - let collection_authority = ctx.accounts.collection_authority.to_account_info(); - let collection_authority_record_pda = ctx - .accounts - .collection_authority_record_pda - .to_account_info(); - let bubblegum_signer = ctx.accounts.bubblegum_signer.to_account_info(); - let token_metadata_program = ctx.accounts.token_metadata_program.to_account_info(); - - // User-provided metadata must result in same user-provided data hash. - let incoming_data_hash = hash_metadata(&message)?; - if data_hash != incoming_data_hash { - return Err(BubblegumError::DataHashMismatch.into()); + /// Compresses a metadata account. + pub fn compress(ctx: Context) -> Result<()> { + processor::compress(ctx) } - // Note this call mutates message. - process_collection_verification_mpl_only( - collection_metadata, - &collection_mint, - &collection_authority, - &collection_authority_record_pda, - &edition_account, - &bubblegum_signer, - ctx.bumps["bubblegum_signer"], - &token_metadata_program, - &mut message, - verify, - new_collection, - )?; - - // Calculate new data hash. - let updated_data_hash = hash_metadata(&message)?; - - // Build previous leaf struct, new leaf struct, and replace the leaf in the tree. - let asset_id = get_asset_id(&merkle_tree.key(), nonce); - let previous_leaf = LeafSchema::new_v0( - asset_id, - owner.key(), - delegate.key(), - nonce, - data_hash, - creator_hash, - ); - let new_leaf = LeafSchema::new_v0( - asset_id, - owner.key(), - delegate.key(), - nonce, - updated_data_hash, - creator_hash, - ); - - wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; - - replace_leaf( - &merkle_tree.key(), - *ctx.bumps.get("tree_authority").unwrap(), - &ctx.accounts.compression_program.to_account_info(), - &ctx.accounts.tree_authority.to_account_info(), - &ctx.accounts.merkle_tree.to_account_info(), - &ctx.accounts.log_wrapper.to_account_info(), - ctx.remaining_accounts, - root, - previous_leaf.to_node(), - new_leaf.to_node(), - index, - ) -} - -#[program] -pub mod bubblegum { - use state::DecompressableState; - - use super::*; - + /// Creates a new tree. pub fn create_tree( ctx: Context, max_depth: u32, max_buffer_size: u32, public: Option, ) -> Result<()> { - let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); - let seed = merkle_tree.key(); - let seeds = &[seed.as_ref(), &[*ctx.bumps.get("tree_authority").unwrap()]]; - let authority = &mut ctx.accounts.tree_authority; - authority.set_inner(TreeConfig { - tree_creator: ctx.accounts.tree_creator.key(), - tree_delegate: ctx.accounts.tree_creator.key(), - total_mint_capacity: 1 << max_depth, - num_minted: 0, - is_public: public.unwrap_or(false), - is_decompressable: DecompressableState::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) + processor::create_tree(ctx, max_depth, max_buffer_size, public) } - pub fn set_tree_delegate(ctx: Context) -> Result<()> { - ctx.accounts.tree_authority.tree_delegate = ctx.accounts.new_tree_delegate.key(); - Ok(()) + /// Decompresses a leaf node from the tree. + pub fn decompress_v1(ctx: Context, metadata: MetadataArgs) -> Result<()> { + processor::decompress_v1(ctx, metadata) } - pub fn mint_v1(ctx: Context, message: MetadataArgs) -> Result<()> { - // TODO -> Separate V1 / V1 into seperate instructions - let payer = ctx.accounts.payer.key(); - let incoming_tree_delegate = ctx.accounts.tree_delegate.key(); - let owner = ctx.accounts.leaf_owner.key(); - let delegate = ctx.accounts.leaf_delegate.key(); - let authority = &mut ctx.accounts.tree_authority; - let merkle_tree = &ctx.accounts.merkle_tree; - if !authority.is_public { - require!( - incoming_tree_delegate == authority.tree_creator - || incoming_tree_delegate == authority.tree_delegate, - BubblegumError::TreeAuthorityIncorrect, - ); - } - - if !authority.contains_mint_capacity(1) { - return Err(BubblegumError::InsufficientMintCapacity.into()); - } - - // Create a HashSet to store signers to use with creator validation. Any signer can be - // counted as a validated creator. - let mut metadata_auth = HashSet::::new(); - metadata_auth.insert(payer); - metadata_auth.insert(incoming_tree_delegate); - - // If there are any remaining accounts that are also signers, they can also be used for - // creator validation. - metadata_auth.extend( - ctx.remaining_accounts - .iter() - .filter(|a| a.is_signer) - .map(|a| a.key()), - ); - - process_mint_v1( - message, - owner, - delegate, - metadata_auth, - *ctx.bumps.get("tree_authority").unwrap(), - authority, - merkle_tree, - &ctx.accounts.log_wrapper, - &ctx.accounts.compression_program, - false, - )?; - - authority.increment_mint_count(); - - Ok(()) + /// Sets a delegate for a leaf node. + pub fn delegate<'info>( + ctx: Context<'_, '_, '_, 'info, Delegate<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + ) -> Result<()> { + processor::delegate(ctx, root, data_hash, creator_hash, nonce, index) } + /// Mints a new asset and adds it to a collection. pub fn mint_to_collection_v1( ctx: Context, metadata_args: MetadataArgs, ) -> Result<()> { - let mut message = metadata_args; - // TODO -> Separate V1 / V1 into seperate instructions - let payer = ctx.accounts.payer.key(); - let incoming_tree_delegate = ctx.accounts.tree_delegate.key(); - let owner = ctx.accounts.leaf_owner.key(); - let delegate = ctx.accounts.leaf_delegate.key(); - let authority = &mut ctx.accounts.tree_authority; - let merkle_tree = &ctx.accounts.merkle_tree; - - let collection_metadata = &ctx.accounts.collection_metadata; - let collection_mint = ctx.accounts.collection_mint.to_account_info(); - let edition_account = ctx.accounts.edition_account.to_account_info(); - let collection_authority = ctx.accounts.collection_authority.to_account_info(); - let collection_authority_record_pda = ctx - .accounts - .collection_authority_record_pda - .to_account_info(); - let bubblegum_signer = ctx.accounts.bubblegum_signer.to_account_info(); - let token_metadata_program = ctx.accounts.token_metadata_program.to_account_info(); - - if !authority.is_public { - require!( - incoming_tree_delegate == authority.tree_creator - || incoming_tree_delegate == authority.tree_delegate, - BubblegumError::TreeAuthorityIncorrect, - ); - } - - if !authority.contains_mint_capacity(1) { - return Err(BubblegumError::InsufficientMintCapacity.into()); - } - - // Create a HashSet to store signers to use with creator validation. Any signer can be - // counted as a validated creator. - let mut metadata_auth = HashSet::::new(); - metadata_auth.insert(payer); - metadata_auth.insert(incoming_tree_delegate); - - // If there are any remaining accounts that are also signers, they can also be used for - // creator validation. - metadata_auth.extend( - ctx.remaining_accounts - .iter() - .filter(|a| a.is_signer) - .map(|a| a.key()), - ); - - process_collection_verification_mpl_only( - collection_metadata, - &collection_mint, - &collection_authority, - &collection_authority_record_pda, - &edition_account, - &bubblegum_signer, - ctx.bumps["bubblegum_signer"], - &token_metadata_program, - &mut message, - true, - None, - )?; - - process_mint_v1( - message, - owner, - delegate, - metadata_auth, - *ctx.bumps.get("tree_authority").unwrap(), - authority, - merkle_tree, - &ctx.accounts.log_wrapper, - &ctx.accounts.compression_program, - true, - )?; - - authority.increment_mint_count(); - - Ok(()) + processor::mint_to_collection_v1(ctx, metadata_args) } - pub fn verify_creator<'info>( - ctx: Context<'_, '_, '_, 'info, CreatorVerification<'info>>, - root: [u8; 32], - data_hash: [u8; 32], - creator_hash: [u8; 32], - nonce: u64, - index: u32, - message: MetadataArgs, - ) -> Result<()> { - process_creator_verification( - ctx, - root, - data_hash, - creator_hash, - nonce, - index, - message, - true, - ) + /// Mints a new asset. + pub fn mint_v1(ctx: Context, message: MetadataArgs) -> Result<()> { + processor::mint_v1(ctx, message) } - pub fn unverify_creator<'info>( - ctx: Context<'_, '_, '_, 'info, CreatorVerification<'info>>, + /// Redeems a vouches. + /// + /// Once a vouch is redeemed, the corresponding leaf node is removed from the tree. + pub fn redeem<'info>( + ctx: Context<'_, '_, '_, 'info, Redeem<'info>>, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], nonce: u64, index: u32, - message: MetadataArgs, ) -> Result<()> { - process_creator_verification( - ctx, - root, - data_hash, - creator_hash, - nonce, - index, - message, - false, - ) + processor::redeem(ctx, root, data_hash, creator_hash, nonce, index) } - pub fn verify_collection<'info>( + /// Sets and verifies a collection to a leaf node + pub fn set_and_verify_collection<'info>( ctx: Context<'_, '_, '_, 'info, CollectionVerification<'info>>, root: [u8; 32], data_hash: [u8; 32], @@ -1117,8 +157,9 @@ pub mod bubblegum { nonce: u64, index: u32, message: MetadataArgs, + collection: Pubkey, ) -> Result<()> { - process_collection_verification( + processor::set_and_verify_collection( ctx, root, data_hash, @@ -1126,82 +167,36 @@ pub mod bubblegum { nonce, index, message, - true, - None, + collection, ) } - pub fn unverify_collection<'info>( - ctx: Context<'_, '_, '_, 'info, CollectionVerification<'info>>, - root: [u8; 32], - data_hash: [u8; 32], - creator_hash: [u8; 32], - nonce: u64, - index: u32, - message: MetadataArgs, + /// Sets the `decompressible_state` of a tree. + #[deprecated( + since = "0.11.1", + note = "Please use `set_decompressible_state` instead" + )] + pub fn set_decompressable_state( + ctx: Context, + decompressable_state: DecompressibleState, ) -> Result<()> { - process_collection_verification( - ctx, - root, - data_hash, - creator_hash, - nonce, - index, - message, - false, - None, - ) + processor::set_decompressible_state(ctx, decompressable_state) } - pub fn set_and_verify_collection<'info>( - ctx: Context<'_, '_, '_, 'info, CollectionVerification<'info>>, - root: [u8; 32], - data_hash: [u8; 32], - creator_hash: [u8; 32], - nonce: u64, - index: u32, - message: MetadataArgs, - collection: Pubkey, + /// Sets the `decompressible_state` of a tree. + pub fn set_decompressible_state( + ctx: Context, + decompressable_state: DecompressibleState, ) -> Result<()> { - 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; - let collection_metadata = &ctx.accounts.collection_metadata; - - // Require that either the tree authority signed this transaction, or the tree authority is - // the collection update authority which means the leaf update is approved via proxy, when - // we later call `assert_has_collection_authority()`. - // - // This is similar to logic in token-metadata for `set_and_verify_collection()` except - // this logic also allows the tree authority (which we are treating as the leaf metadata - // authority) to be different than the collection authority (actual or delegated). The - // token-metadata program required them to be the same. - let tree_authority_signed = incoming_tree_delegate.is_signer - && (incoming_tree_delegate.key() == tree_creator - || incoming_tree_delegate.key() == tree_delegate); - - let tree_authority_is_collection_update_authority = collection_metadata.update_authority - == tree_creator - || collection_metadata.update_authority == tree_delegate; - - require!( - tree_authority_signed || tree_authority_is_collection_update_authority, - BubblegumError::UpdateAuthorityIncorrect - ); + processor::set_decompressible_state(ctx, decompressable_state) + } - process_collection_verification( - ctx, - root, - data_hash, - creator_hash, - nonce, - index, - message, - true, - Some(collection), - ) + /// Sets a delegate for a tree. + pub fn set_tree_delegate(ctx: Context) -> Result<()> { + processor::set_tree_delegate(ctx) } + /// Transfers a leaf node from one account to another. pub fn transfer<'info>( ctx: Context<'_, '_, '_, 'info, Transfer<'info>>, root: [u8; 32], @@ -1210,421 +205,58 @@ pub mod bubblegum { nonce: u64, index: u32, ) -> Result<()> { - // 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(); - let delegate = ctx.accounts.leaf_delegate.to_account_info(); - - // Transfers must be initiated by either the leaf owner or leaf delegate. - require!( - owner.is_signer || delegate.is_signer, - BubblegumError::LeafAuthorityMustSign - ); - let new_owner = ctx.accounts.new_leaf_owner.key(); - let asset_id = get_asset_id(&merkle_tree.key(), nonce); - let previous_leaf = LeafSchema::new_v0( - asset_id, - owner.key(), - delegate.key(), - nonce, - data_hash, - creator_hash, - ); - // New leafs are instantiated with no delegate - let new_leaf = LeafSchema::new_v0( - asset_id, - new_owner, - new_owner, - nonce, - data_hash, - creator_hash, - ); - - wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; - - replace_leaf( - &merkle_tree.key(), - *ctx.bumps.get("tree_authority").unwrap(), - &ctx.accounts.compression_program.to_account_info(), - &ctx.accounts.tree_authority.to_account_info(), - &ctx.accounts.merkle_tree.to_account_info(), - &ctx.accounts.log_wrapper.to_account_info(), - ctx.remaining_accounts, - root, - previous_leaf.to_node(), - new_leaf.to_node(), - index, - ) + processor::transfer(ctx, root, data_hash, creator_hash, nonce, index) } - pub fn delegate<'info>( - ctx: Context<'_, '_, '_, 'info, Delegate<'info>>, + /// Unverifies a collection from a leaf node. + pub fn unverify_collection<'info>( + ctx: Context<'_, '_, '_, 'info, CollectionVerification<'info>>, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], nonce: u64, index: u32, + message: MetadataArgs, ) -> Result<()> { - 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(); - let new_delegate = ctx.accounts.new_leaf_delegate.key(); - let asset_id = get_asset_id(&merkle_tree.key(), nonce); - let previous_leaf = LeafSchema::new_v0( - asset_id, - owner, - previous_delegate, - nonce, - data_hash, - creator_hash, - ); - let new_leaf = LeafSchema::new_v0( - asset_id, - owner, - new_delegate, - nonce, - data_hash, - creator_hash, - ); - - wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; - - replace_leaf( - &merkle_tree.key(), - *ctx.bumps.get("tree_authority").unwrap(), - &ctx.accounts.compression_program.to_account_info(), - &ctx.accounts.tree_authority.to_account_info(), - &ctx.accounts.merkle_tree.to_account_info(), - &ctx.accounts.log_wrapper.to_account_info(), - ctx.remaining_accounts, - root, - previous_leaf.to_node(), - new_leaf.to_node(), - index, - ) + processor::unverify_collection(ctx, root, data_hash, creator_hash, nonce, index, message) } - pub fn burn<'info>( - ctx: Context<'_, '_, '_, 'info, Burn<'info>>, + /// Unverifies a creator from a leaf node. + pub fn unverify_creator<'info>( + ctx: Context<'_, '_, '_, 'info, CreatorVerification<'info>>, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], nonce: u64, index: u32, + message: MetadataArgs, ) -> Result<()> { - let owner = ctx.accounts.leaf_owner.to_account_info(); - let delegate = ctx.accounts.leaf_delegate.to_account_info(); - - // Burn must be initiated by either the leaf owner or leaf delegate. - require!( - owner.is_signer || delegate.is_signer, - BubblegumError::LeafAuthorityMustSign - ); - let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); - let asset_id = get_asset_id(&merkle_tree.key(), nonce); - - let previous_leaf = LeafSchema::new_v0( - asset_id, - owner.key(), - delegate.key(), - nonce, - data_hash, - creator_hash, - ); - - let new_leaf = Node::default(); - - replace_leaf( - &merkle_tree.key(), - *ctx.bumps.get("tree_authority").unwrap(), - &ctx.accounts.compression_program.to_account_info(), - &ctx.accounts.tree_authority.to_account_info(), - &ctx.accounts.merkle_tree.to_account_info(), - &ctx.accounts.log_wrapper.to_account_info(), - ctx.remaining_accounts, - root, - previous_leaf.to_node(), - new_leaf, - index, - ) + processor::unverify_creator(ctx, root, data_hash, creator_hash, nonce, index, message) } - pub fn redeem<'info>( - ctx: Context<'_, '_, '_, 'info, Redeem<'info>>, + /// Verifies a collection for a leaf node. + pub fn verify_collection<'info>( + ctx: Context<'_, '_, '_, 'info, CollectionVerification<'info>>, root: [u8; 32], data_hash: [u8; 32], creator_hash: [u8; 32], nonce: u64, index: u32, + message: MetadataArgs, ) -> Result<()> { - if ctx.accounts.tree_authority.is_decompressable == DecompressableState::Disabled { - return Err(BubblegumError::DecompressionDisabled.into()); - } - - let owner = ctx.accounts.leaf_owner.key(); - let delegate = ctx.accounts.leaf_delegate.key(); - let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); - let asset_id = get_asset_id(&merkle_tree.key(), nonce); - let previous_leaf = - LeafSchema::new_v0(asset_id, owner, delegate, nonce, data_hash, creator_hash); - - let new_leaf = Node::default(); - - replace_leaf( - &merkle_tree.key(), - *ctx.bumps.get("tree_authority").unwrap(), - &ctx.accounts.compression_program.to_account_info(), - &ctx.accounts.tree_authority.to_account_info(), - &ctx.accounts.merkle_tree.to_account_info(), - &ctx.accounts.log_wrapper.to_account_info(), - ctx.remaining_accounts, - root, - previous_leaf.to_node(), - new_leaf, - index, - )?; - ctx.accounts - .voucher - .set_inner(Voucher::new(previous_leaf, index, merkle_tree.key())); - - Ok(()) + processor::verify_collection(ctx, root, data_hash, creator_hash, nonce, index, message) } - pub fn cancel_redeem<'info>( - ctx: Context<'_, '_, '_, 'info, CancelRedeem<'info>>, + /// Verifies a creator for a leaf node. + pub fn verify_creator<'info>( + ctx: Context<'_, '_, '_, 'info, CreatorVerification<'info>>, root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + message: MetadataArgs, ) -> Result<()> { - let voucher = &ctx.accounts.voucher; - match ctx.accounts.voucher.leaf_schema { - LeafSchema::V1 { owner, .. } => assert_pubkey_equal( - &ctx.accounts.leaf_owner.key(), - &owner, - Some(BubblegumError::AssetOwnerMismatch.into()), - ), - }?; - let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); - - wrap_application_data_v1( - voucher.leaf_schema.to_event().try_to_vec()?, - &ctx.accounts.log_wrapper, - )?; - - replace_leaf( - &merkle_tree.key(), - *ctx.bumps.get("tree_authority").unwrap(), - &ctx.accounts.compression_program.to_account_info(), - &ctx.accounts.tree_authority.to_account_info(), - &ctx.accounts.merkle_tree.to_account_info(), - &ctx.accounts.log_wrapper.to_account_info(), - ctx.remaining_accounts, - root, - [0; 32], - voucher.leaf_schema.to_node(), - voucher.index, - ) - } - - pub fn decompress_v1(ctx: Context, metadata: MetadataArgs) -> Result<()> { - // Allocate and create mint - let incoming_data_hash = hash_metadata(&metadata)?; - match ctx.accounts.voucher.leaf_schema { - LeafSchema::V1 { - owner, data_hash, .. - } => { - if !cmp_bytes(&data_hash, &incoming_data_hash, 32) { - return Err(BubblegumError::HashingMismatch.into()); - } - if !cmp_pubkeys(&owner, ctx.accounts.leaf_owner.key) { - return Err(BubblegumError::AssetOwnerMismatch.into()); - } - } - } - - let voucher = &ctx.accounts.voucher; - match metadata.token_program_version { - TokenProgramVersion::Original => { - if ctx.accounts.mint.data_is_empty() { - invoke_signed( - &system_instruction::create_account( - &ctx.accounts.leaf_owner.key(), - &ctx.accounts.mint.key(), - Rent::get()?.minimum_balance(SplMint::LEN), - SplMint::LEN as u64, - &spl_token::id(), - ), - &[ - ctx.accounts.leaf_owner.to_account_info(), - ctx.accounts.mint.to_account_info(), - ctx.accounts.system_program.to_account_info(), - ], - &[&[ - ASSET_PREFIX.as_bytes(), - voucher.merkle_tree.key().as_ref(), - voucher.leaf_schema.nonce().to_le_bytes().as_ref(), - &[*ctx.bumps.get("mint").unwrap()], - ]], - )?; - invoke( - &spl_token::instruction::initialize_mint2( - &spl_token::id(), - &ctx.accounts.mint.key(), - &ctx.accounts.mint_authority.key(), - Some(&ctx.accounts.mint_authority.key()), - 0, - )?, - &[ - ctx.accounts.token_program.to_account_info(), - ctx.accounts.mint.to_account_info(), - ], - )?; - } - if ctx.accounts.token_account.data_is_empty() { - invoke( - &spl_associated_token_account::instruction::create_associated_token_account( - &ctx.accounts.leaf_owner.key(), - &ctx.accounts.leaf_owner.key(), - &ctx.accounts.mint.key(), - &spl_token::ID, - ), - &[ - ctx.accounts.leaf_owner.to_account_info(), - ctx.accounts.mint.to_account_info(), - ctx.accounts.token_account.to_account_info(), - ctx.accounts.token_program.to_account_info(), - ctx.accounts.associated_token_program.to_account_info(), - ctx.accounts.system_program.to_account_info(), - ctx.accounts.sysvar_rent.to_account_info(), - ], - )?; - } - // SPL token will check that the associated token account is initialized, that it - // has the correct owner, and that the mint (which is a PDA of this program) - // matches. - - invoke_signed( - &spl_token::instruction::mint_to( - &spl_token::id(), - &ctx.accounts.mint.key(), - &ctx.accounts.token_account.key(), - &ctx.accounts.mint_authority.key(), - &[], - 1, - )?, - &[ - ctx.accounts.mint.to_account_info(), - ctx.accounts.token_account.to_account_info(), - ctx.accounts.mint_authority.to_account_info(), - ctx.accounts.token_program.to_account_info(), - ], - &[&[ - ctx.accounts.mint.key().as_ref(), - &[ctx.bumps["mint_authority"]], - ]], - )?; - } - TokenProgramVersion::Token2022 => return Err(ProgramError::InvalidArgument.into()), - } - - invoke_signed( - &system_instruction::assign(&ctx.accounts.mint_authority.key(), &crate::id()), - &[ctx.accounts.mint_authority.to_account_info()], - &[&[ - ctx.accounts.mint.key().as_ref(), - &[*ctx.bumps.get("mint_authority").unwrap()], - ]], - )?; - - let metadata_infos = vec![ - ctx.accounts.metadata.to_account_info(), - ctx.accounts.mint.to_account_info(), - ctx.accounts.mint_authority.to_account_info(), - ctx.accounts.leaf_owner.to_account_info(), - ctx.accounts.token_metadata_program.to_account_info(), - ctx.accounts.token_program.to_account_info(), - ctx.accounts.system_program.to_account_info(), - ctx.accounts.sysvar_rent.to_account_info(), - ]; - - let master_edition_infos = vec![ - ctx.accounts.master_edition.to_account_info(), - ctx.accounts.mint.to_account_info(), - ctx.accounts.mint_authority.to_account_info(), - ctx.accounts.leaf_owner.to_account_info(), - ctx.accounts.metadata.to_account_info(), - ctx.accounts.token_metadata_program.to_account_info(), - ctx.accounts.token_program.to_account_info(), - ctx.accounts.system_program.to_account_info(), - ctx.accounts.sysvar_rent.to_account_info(), - ]; - - msg!("Creating metadata!"); - invoke_signed( - &mpl_token_metadata::instruction::create_metadata_accounts_v3( - ctx.accounts.token_metadata_program.key(), - ctx.accounts.metadata.key(), - ctx.accounts.mint.key(), - ctx.accounts.mint_authority.key(), - ctx.accounts.leaf_owner.key(), - ctx.accounts.mint_authority.key(), - metadata.name.clone(), - metadata.symbol.clone(), - metadata.uri.clone(), - if !metadata.creators.is_empty() { - Some(metadata.creators.iter().map(|c| c.adapt()).collect()) - } else { - None - }, - metadata.seller_fee_basis_points, - true, - metadata.is_mutable, - metadata.collection.map(|c| c.adapt()), - metadata.uses.map(|u| u.adapt()), - None, - ), - metadata_infos.as_slice(), - &[&[ - ctx.accounts.mint.key().as_ref(), - &[ctx.bumps["mint_authority"]], - ]], - )?; - - msg!("Creating master edition!"); - invoke_signed( - &mpl_token_metadata::instruction::create_master_edition_v3( - ctx.accounts.token_metadata_program.key(), - ctx.accounts.master_edition.key(), - ctx.accounts.mint.key(), - ctx.accounts.mint_authority.key(), - ctx.accounts.mint_authority.key(), - ctx.accounts.metadata.key(), - ctx.accounts.leaf_owner.key(), - Some(0), - ), - master_edition_infos.as_slice(), - &[&[ - ctx.accounts.mint.key().as_ref(), - &[ctx.bumps["mint_authority"]], - ]], - )?; - - ctx.accounts - .mint_authority - .to_account_info() - .assign(&System::id()); - Ok(()) - } - - pub fn compress(_ctx: Context) -> Result<()> { - // TODO - Ok(()) - } - - pub fn set_decompressable_state( - ctx: Context, - decompressable_state: DecompressableState, - ) -> Result<()> { - ctx.accounts.tree_authority.is_decompressable = decompressable_state; - - Ok(()) + processor::verify_creator(ctx, root, data_hash, creator_hash, nonce, index, message) } } diff --git a/programs/bubblegum/program/src/processor/burn.rs b/programs/bubblegum/program/src/processor/burn.rs new file mode 100644 index 00000000..0dc49149 --- /dev/null +++ b/programs/bubblegum/program/src/processor/burn.rs @@ -0,0 +1,72 @@ +use anchor_lang::prelude::*; +use spl_account_compression::{program::SplAccountCompression, Node, Noop}; + +use crate::{ + error::BubblegumError, + state::{leaf_schema::LeafSchema, TreeConfig}, + utils::{get_asset_id, replace_leaf}, +}; + +#[derive(Accounts)] +pub struct Burn<'info> { + #[account( + seeds = [merkle_tree.key().as_ref()], + bump, + )] + pub tree_authority: Account<'info, TreeConfig>, + /// CHECK: This account is checked in the instruction + pub leaf_owner: UncheckedAccount<'info>, + /// CHECK: This account is checked in the instruction + pub leaf_delegate: UncheckedAccount<'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>, + pub system_program: Program<'info, System>, +} + +pub(crate) fn burn<'info>( + ctx: Context<'_, '_, '_, 'info, Burn<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, +) -> Result<()> { + let owner = ctx.accounts.leaf_owner.to_account_info(); + let delegate = ctx.accounts.leaf_delegate.to_account_info(); + + // Burn must be initiated by either the leaf owner or leaf delegate. + require!( + owner.is_signer || delegate.is_signer, + BubblegumError::LeafAuthorityMustSign + ); + let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); + let asset_id = get_asset_id(&merkle_tree.key(), nonce); + + let previous_leaf = LeafSchema::new_v0( + asset_id, + owner.key(), + delegate.key(), + nonce, + data_hash, + creator_hash, + ); + + let new_leaf = Node::default(); + + replace_leaf( + &merkle_tree.key(), + *ctx.bumps.get("tree_authority").unwrap(), + &ctx.accounts.compression_program.to_account_info(), + &ctx.accounts.tree_authority.to_account_info(), + &ctx.accounts.merkle_tree.to_account_info(), + &ctx.accounts.log_wrapper.to_account_info(), + ctx.remaining_accounts, + root, + previous_leaf.to_node(), + new_leaf, + index, + ) +} diff --git a/programs/bubblegum/program/src/processor/cancel_redeem.rs b/programs/bubblegum/program/src/processor/cancel_redeem.rs new file mode 100644 index 00000000..558b7a0e --- /dev/null +++ b/programs/bubblegum/program/src/processor/cancel_redeem.rs @@ -0,0 +1,72 @@ +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, +}; + +#[derive(Accounts)] +pub struct CancelRedeem<'info> { + #[account( + seeds = [merkle_tree.key().as_ref()], + bump, + )] + /// CHECK: This account is neither written to nor read from. + pub tree_authority: Account<'info, TreeConfig>, + #[account(mut)] + pub leaf_owner: Signer<'info>, + #[account(mut)] + /// CHECK: unsafe + pub merkle_tree: UncheckedAccount<'info>, + #[account( + mut, + close = leaf_owner, + seeds = [ + VOUCHER_PREFIX.as_ref(), + merkle_tree.key().as_ref(), + & voucher.leaf_schema.nonce().to_le_bytes() + ], + bump + )] + pub voucher: Account<'info, Voucher>, + pub log_wrapper: Program<'info, Noop>, + pub compression_program: Program<'info, SplAccountCompression>, + pub system_program: Program<'info, System>, +} + +pub(crate) fn cancel_redeem<'info>( + ctx: Context<'_, '_, '_, 'info, CancelRedeem<'info>>, + root: [u8; 32], +) -> Result<()> { + let voucher = &ctx.accounts.voucher; + match ctx.accounts.voucher.leaf_schema { + LeafSchema::V1 { owner, .. } => assert_pubkey_equal( + &ctx.accounts.leaf_owner.key(), + &owner, + Some(BubblegumError::AssetOwnerMismatch.into()), + ), + }?; + let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); + + wrap_application_data_v1( + voucher.leaf_schema.to_event().try_to_vec()?, + &ctx.accounts.log_wrapper, + )?; + + replace_leaf( + &merkle_tree.key(), + *ctx.bumps.get("tree_authority").unwrap(), + &ctx.accounts.compression_program.to_account_info(), + &ctx.accounts.tree_authority.to_account_info(), + &ctx.accounts.merkle_tree.to_account_info(), + &ctx.accounts.log_wrapper.to_account_info(), + ctx.remaining_accounts, + root, + [0; 32], + voucher.leaf_schema.to_node(), + voucher.index, + ) +} diff --git a/programs/bubblegum/program/src/processor/compress.rs b/programs/bubblegum/program/src/processor/compress.rs new file mode 100644 index 00000000..6c2e9e27 --- /dev/null +++ b/programs/bubblegum/program/src/processor/compress.rs @@ -0,0 +1,45 @@ +use anchor_lang::prelude::*; +use spl_account_compression::{program::SplAccountCompression, Noop}; + +use crate::state::metaplex_anchor::{MasterEdition, TokenMetadata}; + +#[derive(Accounts)] +pub struct Compress<'info> { + #[account( + seeds = [merkle_tree.key().as_ref()], + bump, + )] + /// CHECK: This account is neither written to nor read from. + pub tree_authority: UncheckedAccount<'info>, + /// CHECK: This account is checked in the instruction + pub leaf_owner: Signer<'info>, + /// CHECK: This account is chekced in the instruction + pub leaf_delegate: UncheckedAccount<'info>, + /// CHECK: This account is not read + pub merkle_tree: UncheckedAccount<'info>, + + /// CHECK: versioning is handled in the instruction + #[account(mut)] + pub token_account: AccountInfo<'info>, + /// CHECK: versioning is handled in the instruction + #[account(mut)] + pub mint: AccountInfo<'info>, + #[account(mut)] + pub metadata: Box>, + #[account(mut)] + pub master_edition: Box>, + #[account(mut)] + pub payer: Signer<'info>, + pub log_wrapper: Program<'info, Noop>, + pub compression_program: Program<'info, SplAccountCompression>, + /// CHECK: + pub token_program: UncheckedAccount<'info>, + /// CHECK: + pub token_metadata_program: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, +} + +pub(crate) fn compress(_ctx: Context) -> Result<()> { + // TODO + Ok(()) +} diff --git a/programs/bubblegum/program/src/processor/create_tree.rs b/programs/bubblegum/program/src/processor/create_tree.rs new file mode 100644 index 00000000..e115f017 --- /dev/null +++ b/programs/bubblegum/program/src/processor/create_tree.rs @@ -0,0 +1,56 @@ +use anchor_lang::{prelude::*, system_program::System}; +use spl_account_compression::{program::SplAccountCompression, Noop}; + +use crate::state::{DecompressibleState, TreeConfig, TREE_AUTHORITY_SIZE}; + +#[derive(Accounts)] +pub struct CreateTree<'info> { + #[account( + init, + seeds = [merkle_tree.key().as_ref()], + payer = payer, + space = TREE_AUTHORITY_SIZE, + bump, + )] + pub tree_authority: Account<'info, TreeConfig>, + #[account(zero)] + /// CHECK: This account must be all zeros + pub merkle_tree: UncheckedAccount<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub tree_creator: Signer<'info>, + pub log_wrapper: Program<'info, Noop>, + pub compression_program: Program<'info, SplAccountCompression>, + pub system_program: Program<'info, System>, +} + +pub(crate) fn create_tree( + ctx: Context, + max_depth: u32, + max_buffer_size: u32, + public: Option, +) -> Result<()> { + let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); + let seed = merkle_tree.key(); + let seeds = &[seed.as_ref(), &[*ctx.bumps.get("tree_authority").unwrap()]]; + let authority = &mut ctx.accounts.tree_authority; + authority.set_inner(TreeConfig { + tree_creator: ctx.accounts.tree_creator.key(), + tree_delegate: ctx.accounts.tree_creator.key(), + total_mint_capacity: 1 << max_depth, + num_minted: 0, + is_public: public.unwrap_or(false), + 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) +} diff --git a/programs/bubblegum/program/src/processor/decompress.rs b/programs/bubblegum/program/src/processor/decompress.rs new file mode 100644 index 00000000..01310386 --- /dev/null +++ b/programs/bubblegum/program/src/processor/decompress.rs @@ -0,0 +1,261 @@ +use anchor_lang::prelude::*; +use anchor_spl::{associated_token::AssociatedToken, token::Token}; +use solana_program::{ + program::{invoke, invoke_signed}, + program_pack::Pack, + system_instruction, +}; +use spl_account_compression::Noop; +use spl_token::state::Mint; + +use crate::{ + error::BubblegumError, + state::{ + leaf_schema::LeafSchema, + metaplex_adapter::{MetadataArgs, TokenProgramVersion}, + metaplex_anchor::MplTokenMetadata, + Voucher, ASSET_PREFIX, VOUCHER_PREFIX, + }, + utils::{cmp_bytes, cmp_pubkeys, hash_metadata}, +}; + +#[derive(Accounts)] +pub struct DecompressV1<'info> { + #[account( + mut, + close = leaf_owner, + seeds = [ + VOUCHER_PREFIX.as_ref(), + voucher.merkle_tree.as_ref(), + voucher.leaf_schema.nonce().to_le_bytes().as_ref() + ], + bump + )] + pub voucher: Box>, + #[account(mut)] + pub leaf_owner: Signer<'info>, + /// CHECK: versioning is handled in the instruction + #[account(mut)] + pub token_account: UncheckedAccount<'info>, + /// CHECK: versioning is handled in the instruction + #[account( + mut, + seeds = [ + ASSET_PREFIX.as_ref(), + voucher.merkle_tree.as_ref(), + voucher.leaf_schema.nonce().to_le_bytes().as_ref(), + ], + bump + )] + pub mint: UncheckedAccount<'info>, + /// CHECK: + #[account( + mut, + seeds = [mint.key().as_ref()], + bump, + )] + pub mint_authority: UncheckedAccount<'info>, + /// CHECK: + #[account(mut)] + pub metadata: UncheckedAccount<'info>, + /// CHECK: Initialized in Token Metadata Program + #[account(mut)] + pub master_edition: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, + pub sysvar_rent: Sysvar<'info, Rent>, + /// CHECK: + 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>, +} + +pub(crate) fn decompress_v1(ctx: Context, metadata: MetadataArgs) -> Result<()> { + // Allocate and create mint + let incoming_data_hash = hash_metadata(&metadata)?; + match ctx.accounts.voucher.leaf_schema { + LeafSchema::V1 { + owner, data_hash, .. + } => { + if !cmp_bytes(&data_hash, &incoming_data_hash, 32) { + return Err(BubblegumError::HashingMismatch.into()); + } + if !cmp_pubkeys(&owner, ctx.accounts.leaf_owner.key) { + return Err(BubblegumError::AssetOwnerMismatch.into()); + } + } + } + + let voucher = &ctx.accounts.voucher; + match metadata.token_program_version { + TokenProgramVersion::Original => { + if ctx.accounts.mint.data_is_empty() { + invoke_signed( + &system_instruction::create_account( + &ctx.accounts.leaf_owner.key(), + &ctx.accounts.mint.key(), + Rent::get()?.minimum_balance(Mint::LEN), + Mint::LEN as u64, + &spl_token::id(), + ), + &[ + ctx.accounts.leaf_owner.to_account_info(), + ctx.accounts.mint.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ], + &[&[ + ASSET_PREFIX.as_bytes(), + voucher.merkle_tree.key().as_ref(), + voucher.leaf_schema.nonce().to_le_bytes().as_ref(), + &[*ctx.bumps.get("mint").unwrap()], + ]], + )?; + invoke( + &spl_token::instruction::initialize_mint2( + &spl_token::id(), + &ctx.accounts.mint.key(), + &ctx.accounts.mint_authority.key(), + Some(&ctx.accounts.mint_authority.key()), + 0, + )?, + &[ + ctx.accounts.token_program.to_account_info(), + ctx.accounts.mint.to_account_info(), + ], + )?; + } + if ctx.accounts.token_account.data_is_empty() { + invoke( + &spl_associated_token_account::instruction::create_associated_token_account( + &ctx.accounts.leaf_owner.key(), + &ctx.accounts.leaf_owner.key(), + &ctx.accounts.mint.key(), + &spl_token::ID, + ), + &[ + ctx.accounts.leaf_owner.to_account_info(), + ctx.accounts.mint.to_account_info(), + ctx.accounts.token_account.to_account_info(), + ctx.accounts.token_program.to_account_info(), + ctx.accounts.associated_token_program.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ctx.accounts.sysvar_rent.to_account_info(), + ], + )?; + } + // SPL token will check that the associated token account is initialized, that it + // has the correct owner, and that the mint (which is a PDA of this program) + // matches. + + invoke_signed( + &spl_token::instruction::mint_to( + &spl_token::id(), + &ctx.accounts.mint.key(), + &ctx.accounts.token_account.key(), + &ctx.accounts.mint_authority.key(), + &[], + 1, + )?, + &[ + ctx.accounts.mint.to_account_info(), + ctx.accounts.token_account.to_account_info(), + ctx.accounts.mint_authority.to_account_info(), + ctx.accounts.token_program.to_account_info(), + ], + &[&[ + ctx.accounts.mint.key().as_ref(), + &[ctx.bumps["mint_authority"]], + ]], + )?; + } + TokenProgramVersion::Token2022 => return Err(ProgramError::InvalidArgument.into()), + } + + invoke_signed( + &system_instruction::assign(&ctx.accounts.mint_authority.key(), &crate::id()), + &[ctx.accounts.mint_authority.to_account_info()], + &[&[ + ctx.accounts.mint.key().as_ref(), + &[*ctx.bumps.get("mint_authority").unwrap()], + ]], + )?; + + let metadata_infos = vec![ + ctx.accounts.metadata.to_account_info(), + ctx.accounts.mint.to_account_info(), + ctx.accounts.mint_authority.to_account_info(), + ctx.accounts.leaf_owner.to_account_info(), + ctx.accounts.token_metadata_program.to_account_info(), + ctx.accounts.token_program.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ctx.accounts.sysvar_rent.to_account_info(), + ]; + + let master_edition_infos = vec![ + ctx.accounts.master_edition.to_account_info(), + ctx.accounts.mint.to_account_info(), + ctx.accounts.mint_authority.to_account_info(), + ctx.accounts.leaf_owner.to_account_info(), + ctx.accounts.metadata.to_account_info(), + ctx.accounts.token_metadata_program.to_account_info(), + ctx.accounts.token_program.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ctx.accounts.sysvar_rent.to_account_info(), + ]; + + msg!("Creating metadata!"); + invoke_signed( + &mpl_token_metadata::instruction::create_metadata_accounts_v3( + ctx.accounts.token_metadata_program.key(), + ctx.accounts.metadata.key(), + ctx.accounts.mint.key(), + ctx.accounts.mint_authority.key(), + ctx.accounts.leaf_owner.key(), + ctx.accounts.mint_authority.key(), + metadata.name.clone(), + metadata.symbol.clone(), + metadata.uri.clone(), + if !metadata.creators.is_empty() { + Some(metadata.creators.iter().map(|c| c.adapt()).collect()) + } else { + None + }, + metadata.seller_fee_basis_points, + true, + metadata.is_mutable, + metadata.collection.map(|c| c.adapt()), + metadata.uses.map(|u| u.adapt()), + None, + ), + metadata_infos.as_slice(), + &[&[ + ctx.accounts.mint.key().as_ref(), + &[ctx.bumps["mint_authority"]], + ]], + )?; + + msg!("Creating master edition!"); + invoke_signed( + &mpl_token_metadata::instruction::create_master_edition_v3( + ctx.accounts.token_metadata_program.key(), + ctx.accounts.master_edition.key(), + ctx.accounts.mint.key(), + ctx.accounts.mint_authority.key(), + ctx.accounts.mint_authority.key(), + ctx.accounts.metadata.key(), + ctx.accounts.leaf_owner.key(), + Some(0), + ), + master_edition_infos.as_slice(), + &[&[ + ctx.accounts.mint.key().as_ref(), + &[ctx.bumps["mint_authority"]], + ]], + )?; + + ctx.accounts + .mint_authority + .to_account_info() + .assign(&System::id()); + Ok(()) +} diff --git a/programs/bubblegum/program/src/processor/delegate.rs b/programs/bubblegum/program/src/processor/delegate.rs new file mode 100644 index 00000000..7313aa0c --- /dev/null +++ b/programs/bubblegum/program/src/processor/delegate.rs @@ -0,0 +1,75 @@ +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}, +}; + +#[derive(Accounts)] +pub struct Delegate<'info> { + #[account( + seeds = [merkle_tree.key().as_ref()], + bump, + )] + /// CHECK: This account is neither written to nor read from. + pub tree_authority: Account<'info, TreeConfig>, + pub leaf_owner: Signer<'info>, + /// CHECK: This account is neither written to nor read from. + pub previous_leaf_delegate: UncheckedAccount<'info>, + /// CHECK: This account is neither written to nor read from. + pub new_leaf_delegate: UncheckedAccount<'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>, + pub system_program: Program<'info, System>, +} + +pub(crate) fn delegate<'info>( + ctx: Context<'_, '_, '_, 'info, Delegate<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, +) -> Result<()> { + 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(); + let new_delegate = ctx.accounts.new_leaf_delegate.key(); + let asset_id = get_asset_id(&merkle_tree.key(), nonce); + let previous_leaf = LeafSchema::new_v0( + asset_id, + owner, + previous_delegate, + nonce, + data_hash, + creator_hash, + ); + let new_leaf = LeafSchema::new_v0( + asset_id, + owner, + new_delegate, + nonce, + data_hash, + creator_hash, + ); + + wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; + + replace_leaf( + &merkle_tree.key(), + *ctx.bumps.get("tree_authority").unwrap(), + &ctx.accounts.compression_program.to_account_info(), + &ctx.accounts.tree_authority.to_account_info(), + &ctx.accounts.merkle_tree.to_account_info(), + &ctx.accounts.log_wrapper.to_account_info(), + ctx.remaining_accounts, + root, + previous_leaf.to_node(), + new_leaf.to_node(), + index, + ) +} diff --git a/programs/bubblegum/program/src/processor/mint.rs b/programs/bubblegum/program/src/processor/mint.rs new file mode 100644 index 00000000..4020c089 --- /dev/null +++ b/programs/bubblegum/program/src/processor/mint.rs @@ -0,0 +1,162 @@ +use std::collections::HashSet; + +use anchor_lang::prelude::*; +use solana_program::keccak; +use spl_account_compression::{program::SplAccountCompression, wrap_application_data_v1, Noop}; + +use crate::{ + asserts::assert_metadata_is_mpl_compatible, + error::BubblegumError, + state::{leaf_schema::LeafSchema, metaplex_adapter::MetadataArgs, TreeConfig}, + utils::{append_leaf, get_asset_id}, +}; + +#[derive(Accounts)] +pub struct MintV1<'info> { + #[account( + mut, + seeds = [merkle_tree.key().as_ref()], + bump, + )] + pub tree_authority: Account<'info, TreeConfig>, + /// CHECK: This account is neither written to nor read from. + pub leaf_owner: AccountInfo<'info>, + /// CHECK: This account is neither written to nor read from. + pub leaf_delegate: AccountInfo<'info>, + #[account(mut)] + /// CHECK: unsafe + 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>, + pub system_program: Program<'info, System>, +} + +pub(crate) fn mint_v1(ctx: Context, message: MetadataArgs) -> Result<()> { + // TODO -> Separate V1 / V1 into seperate instructions + let payer = ctx.accounts.payer.key(); + let incoming_tree_delegate = ctx.accounts.tree_delegate.key(); + let owner = ctx.accounts.leaf_owner.key(); + let delegate = ctx.accounts.leaf_delegate.key(); + let authority = &mut ctx.accounts.tree_authority; + let merkle_tree = &ctx.accounts.merkle_tree; + if !authority.is_public { + require!( + incoming_tree_delegate == authority.tree_creator + || incoming_tree_delegate == authority.tree_delegate, + BubblegumError::TreeAuthorityIncorrect, + ); + } + + if !authority.contains_mint_capacity(1) { + return Err(BubblegumError::InsufficientMintCapacity.into()); + } + + // Create a HashSet to store signers to use with creator validation. Any signer can be + // counted as a validated creator. + let mut metadata_auth = HashSet::::new(); + metadata_auth.insert(payer); + metadata_auth.insert(incoming_tree_delegate); + + // If there are any remaining accounts that are also signers, they can also be used for + // creator validation. + metadata_auth.extend( + ctx.remaining_accounts + .iter() + .filter(|a| a.is_signer) + .map(|a| a.key()), + ); + + process_mint_v1( + message, + owner, + delegate, + metadata_auth, + *ctx.bumps.get("tree_authority").unwrap(), + authority, + merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + false, + )?; + + authority.increment_mint_count(); + + Ok(()) +} + +pub(crate) fn process_mint_v1<'info>( + message: MetadataArgs, + owner: Pubkey, + delegate: Pubkey, + metadata_auth: HashSet, + authority_bump: u8, + authority: &mut Account<'info, TreeConfig>, + merkle_tree: &AccountInfo<'info>, + wrapper: &Program<'info, Noop>, + compression_program: &AccountInfo<'info>, + allow_verified_collection: bool, +) -> Result<()> { + assert_metadata_is_mpl_compatible(&message)?; + if !allow_verified_collection { + if let Some(collection) = &message.collection { + if collection.verified { + return Err(BubblegumError::CollectionCannotBeVerifiedInThisInstruction.into()); + } + } + } + + // @dev: seller_fee_basis points is encoded twice so that it can be passed to marketplace + // instructions, without passing the entire, un-hashed MetadataArgs struct + let metadata_args_hash = keccak::hashv(&[message.try_to_vec()?.as_slice()]); + let data_hash = keccak::hashv(&[ + &metadata_args_hash.to_bytes(), + &message.seller_fee_basis_points.to_le_bytes(), + ]); + + // Use the metadata auth to check whether we can allow `verified` to be set to true in the + // creator Vec. + let creator_data = message + .creators + .iter() + .map(|c| { + if c.verified && !metadata_auth.contains(&c.address) { + Err(BubblegumError::CreatorDidNotVerify.into()) + } else { + Ok([c.address.as_ref(), &[c.verified as u8], &[c.share]].concat()) + } + }) + .collect::>>()?; + + // Calculate creator hash. + let creator_hash = keccak::hashv( + creator_data + .iter() + .map(|c| c.as_slice()) + .collect::>() + .as_ref(), + ); + + let asset_id = get_asset_id(&merkle_tree.key(), authority.num_minted); + let leaf = LeafSchema::new_v0( + asset_id, + owner, + delegate, + authority.num_minted, + data_hash.to_bytes(), + creator_hash.to_bytes(), + ); + + wrap_application_data_v1(leaf.to_event().try_to_vec()?, wrapper)?; + + append_leaf( + &merkle_tree.key(), + authority_bump, + &compression_program.to_account_info(), + &authority.to_account_info(), + &merkle_tree.to_account_info(), + &wrapper.to_account_info(), + 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 new file mode 100644 index 00000000..ea9a3ca9 --- /dev/null +++ b/programs/bubblegum/program/src/processor/mint_to_collection.rs @@ -0,0 +1,138 @@ +use std::collections::HashSet; + +use anchor_lang::prelude::*; +use spl_account_compression::{program::SplAccountCompression, Noop}; + +use crate::{ + error::BubblegumError, + state::{ + metaplex_adapter::MetadataArgs, + metaplex_anchor::{MplTokenMetadata, TokenMetadata}, + TreeConfig, COLLECTION_CPI_PREFIX, + }, +}; + +use super::{mint::process_mint_v1, process_collection_verification_mpl_only}; + +#[derive(Accounts)] +pub struct MintToCollectionV1<'info> { + #[account( + mut, + seeds = [merkle_tree.key().as_ref()], + bump, + )] + pub tree_authority: Account<'info, TreeConfig>, + /// CHECK: This account is neither written to nor read from. + pub leaf_owner: AccountInfo<'info>, + /// CHECK: This account is neither written to nor read from. + pub leaf_delegate: AccountInfo<'info>, + #[account(mut)] + /// CHECK: unsafe + pub merkle_tree: UncheckedAccount<'info>, + pub payer: Signer<'info>, + pub tree_delegate: Signer<'info>, + pub collection_authority: Signer<'info>, + /// CHECK: Optional collection authority record PDA. + /// If there is no collecton authority record PDA then + /// this must be the Bubblegum program address. + pub collection_authority_record_pda: UncheckedAccount<'info>, + /// CHECK: This account is checked in the instruction + pub collection_mint: UncheckedAccount<'info>, + #[account(mut)] + pub collection_metadata: Box>, + /// CHECK: This account is checked in the instruction + pub edition_account: UncheckedAccount<'info>, + /// CHECK: This is just used as a signing PDA. + #[account( + seeds = [COLLECTION_CPI_PREFIX.as_ref()], + bump, + )] + pub bubblegum_signer: UncheckedAccount<'info>, + pub log_wrapper: Program<'info, Noop>, + pub compression_program: Program<'info, SplAccountCompression>, + pub token_metadata_program: Program<'info, MplTokenMetadata>, + pub system_program: Program<'info, System>, +} + +pub(crate) fn mint_to_collection_v1( + ctx: Context, + metadata_args: MetadataArgs, +) -> Result<()> { + let mut message = metadata_args; + // TODO -> Separate V1 / V1 into seperate instructions + let payer = ctx.accounts.payer.key(); + let incoming_tree_delegate = ctx.accounts.tree_delegate.key(); + let owner = ctx.accounts.leaf_owner.key(); + let delegate = ctx.accounts.leaf_delegate.key(); + let authority = &mut ctx.accounts.tree_authority; + let merkle_tree = &ctx.accounts.merkle_tree; + + let collection_metadata = &ctx.accounts.collection_metadata; + let collection_mint = ctx.accounts.collection_mint.to_account_info(); + let edition_account = ctx.accounts.edition_account.to_account_info(); + let collection_authority = ctx.accounts.collection_authority.to_account_info(); + let collection_authority_record_pda = ctx + .accounts + .collection_authority_record_pda + .to_account_info(); + let bubblegum_signer = ctx.accounts.bubblegum_signer.to_account_info(); + let token_metadata_program = ctx.accounts.token_metadata_program.to_account_info(); + + if !authority.is_public { + require!( + incoming_tree_delegate == authority.tree_creator + || incoming_tree_delegate == authority.tree_delegate, + BubblegumError::TreeAuthorityIncorrect, + ); + } + + if !authority.contains_mint_capacity(1) { + return Err(BubblegumError::InsufficientMintCapacity.into()); + } + + // Create a HashSet to store signers to use with creator validation. Any signer can be + // counted as a validated creator. + let mut metadata_auth = HashSet::::new(); + metadata_auth.insert(payer); + metadata_auth.insert(incoming_tree_delegate); + + // If there are any remaining accounts that are also signers, they can also be used for + // creator validation. + metadata_auth.extend( + ctx.remaining_accounts + .iter() + .filter(|a| a.is_signer) + .map(|a| a.key()), + ); + + process_collection_verification_mpl_only( + collection_metadata, + &collection_mint, + &collection_authority, + &collection_authority_record_pda, + &edition_account, + &bubblegum_signer, + ctx.bumps["bubblegum_signer"], + &token_metadata_program, + &mut message, + true, + None, + )?; + + process_mint_v1( + message, + owner, + delegate, + metadata_auth, + *ctx.bumps.get("tree_authority").unwrap(), + authority, + merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + true, + )?; + + authority.increment_mint_count(); + + Ok(()) +} diff --git a/programs/bubblegum/program/src/processor/mod.rs b/programs/bubblegum/program/src/processor/mod.rs new file mode 100644 index 00000000..57b2b040 --- /dev/null +++ b/programs/bubblegum/program/src/processor/mod.rs @@ -0,0 +1,363 @@ +use anchor_lang::prelude::*; +use mpl_token_metadata::{ + assertions::collection::assert_collection_verify_is_valid, state::CollectionDetails, +}; +use solana_program::{account_info::AccountInfo, program::invoke_signed, pubkey::Pubkey}; +use spl_account_compression::wrap_application_data_v1; + +use crate::{ + asserts::assert_has_collection_authority, + error::{metadata_error_into_bubblegum, BubblegumError}, + state::{ + leaf_schema::LeafSchema, + metaplex_adapter::{self, Creator, MetadataArgs}, + metaplex_anchor::TokenMetadata, + COLLECTION_CPI_PREFIX, + }, + utils::{get_asset_id, hash_creators, hash_metadata, replace_leaf}, +}; + +mod burn; +mod cancel_redeem; +mod compress; +mod create_tree; +mod decompress; +mod delegate; +mod mint; +mod mint_to_collection; +mod redeem; +mod set_and_verify_collection; +mod set_decompressible_state; +mod set_tree_delegate; +mod transfer; +mod unverify_collection; +mod unverify_creator; +mod verify_collection; +mod verify_creator; + +pub(crate) use burn::*; +pub(crate) use cancel_redeem::*; +pub(crate) use compress::*; +pub(crate) use create_tree::*; +pub(crate) use decompress::*; +pub(crate) use delegate::*; +pub(crate) use mint::*; +pub(crate) use mint_to_collection::*; +pub(crate) use redeem::*; +pub(crate) use set_and_verify_collection::*; +pub(crate) use set_decompressible_state::*; +pub(crate) use set_tree_delegate::*; +pub(crate) use transfer::*; +pub(crate) use unverify_collection::*; +pub(crate) use unverify_creator::*; +pub(crate) use verify_collection::*; +pub(crate) use verify_creator::*; + +fn process_creator_verification<'info>( + ctx: Context<'_, '_, '_, 'info, verify_creator::CreatorVerification<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + mut message: MetadataArgs, + verify: bool, +) -> Result<()> { + let owner = ctx.accounts.leaf_owner.to_account_info(); + let delegate = ctx.accounts.leaf_delegate.to_account_info(); + let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); + + let creator = ctx.accounts.creator.key(); + + // Creator Vec must contain creators. + if message.creators.is_empty() { + return Err(BubblegumError::NoCreatorsPresent.into()); + } + + // Creator must be in user-provided creator Vec. + if !message.creators.iter().any(|c| c.address == creator) { + return Err(BubblegumError::CreatorNotFound.into()); + } + + // User-provided creator Vec must result in same user-provided creator hash. + let incoming_creator_hash = hash_creators(&message.creators)?; + if creator_hash != incoming_creator_hash { + return Err(BubblegumError::CreatorHashMismatch.into()); + } + + // User-provided metadata must result in same user-provided data hash. + let incoming_data_hash = hash_metadata(&message)?; + if data_hash != incoming_data_hash { + return Err(BubblegumError::DataHashMismatch.into()); + } + + // Calculate new creator Vec with `verified` set to true for signing creator. + let updated_creator_vec = message + .creators + .iter() + .map(|c| { + let verified = if c.address == creator.key() { + verify + } else { + c.verified + }; + Creator { + address: c.address, + verified, + share: c.share, + } + }) + .collect::>(); + + // Calculate new creator hash. + let updated_creator_hash = hash_creators(&updated_creator_vec)?; + + // Update creator Vec in metadata args. + message.creators = updated_creator_vec; + + // Calculate new data hash. + let updated_data_hash = hash_metadata(&message)?; + + // Build previous leaf struct, new leaf struct, and replace the leaf in the tree. + let asset_id = get_asset_id(&merkle_tree.key(), nonce); + let previous_leaf = LeafSchema::new_v0( + asset_id, + owner.key(), + delegate.key(), + nonce, + data_hash, + creator_hash, + ); + let new_leaf = LeafSchema::new_v0( + asset_id, + owner.key(), + delegate.key(), + nonce, + updated_data_hash, + updated_creator_hash, + ); + + wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; + + replace_leaf( + &merkle_tree.key(), + *ctx.bumps.get("tree_authority").unwrap(), + &ctx.accounts.compression_program.to_account_info(), + &ctx.accounts.tree_authority.to_account_info(), + &ctx.accounts.merkle_tree.to_account_info(), + &ctx.accounts.log_wrapper.to_account_info(), + ctx.remaining_accounts, + root, + previous_leaf.to_node(), + new_leaf.to_node(), + index, + ) +} + +#[allow(deprecated)] +fn process_collection_verification_mpl_only<'info>( + collection_metadata: &Account<'info, TokenMetadata>, + collection_mint: &AccountInfo<'info>, + collection_authority: &AccountInfo<'info>, + collection_authority_record_pda: &AccountInfo<'info>, + edition_account: &AccountInfo<'info>, + bubblegum_signer: &AccountInfo<'info>, + bubblegum_bump: u8, + token_metadata_program: &AccountInfo<'info>, + message: &mut MetadataArgs, + verify: bool, + new_collection: Option, +) -> Result<()> { + // See if a collection authority record PDA was provided. + let collection_authority_record = if collection_authority_record_pda.key() == crate::id() { + None + } else { + Some(collection_authority_record_pda) + }; + + // Verify correct account ownerships. + require!( + *collection_metadata.to_account_info().owner == token_metadata_program.key(), + BubblegumError::IncorrectOwner + ); + require!( + *collection_mint.owner == spl_token::id(), + BubblegumError::IncorrectOwner + ); + require!( + *edition_account.owner == token_metadata_program.key(), + BubblegumError::IncorrectOwner + ); + + // If new collection was provided, set it in the NFT metadata. + if new_collection.is_some() { + message.collection = new_collection.map(|key| metaplex_adapter::Collection { + verified: false, // Set to true below. + key, + }); + } + + // If the NFT has collection data, we set it to the correct value after doing some validation. + if let Some(collection) = &mut message.collection { + // Don't verify already verified items, or unverify unverified items, otherwise for sized + // collections we end up with invalid size data. + if verify && collection.verified { + return Err(BubblegumError::AlreadyVerified.into()); + } else if !verify && !collection.verified { + return Err(BubblegumError::AlreadyUnverified.into()); + } + + // Collection verify assert from token-metadata program. + assert_collection_verify_is_valid( + &Some(collection.adapt()), + collection_metadata, + collection_mint, + edition_account, + ) + .map_err(metadata_error_into_bubblegum)?; + + assert_has_collection_authority( + collection_metadata, + collection_mint.key, + collection_authority.key, + collection_authority_record, + )?; + + // Update collection in metadata args. Note since this is a mutable reference, + // it is still updating `message.collection` after being destructured. + collection.verified = verify; + } else { + return Err(BubblegumError::CollectionNotFound.into()); + } + + // If this is a sized collection, then increment or decrement collection size. + if let Some(details) = &collection_metadata.collection_details { + // Increment or decrement existing size. + let new_size = match details { + CollectionDetails::V1 { size } => { + if verify { + size.checked_add(1) + .ok_or(BubblegumError::NumericalOverflowError)? + } else { + size.checked_sub(1) + .ok_or(BubblegumError::NumericalOverflowError)? + } + } + }; + + // CPI into to token-metadata program to change the collection size. + let mut bubblegum_set_collection_size_infos = vec![ + collection_metadata.to_account_info(), + collection_authority.clone(), + collection_mint.clone(), + bubblegum_signer.clone(), + ]; + + if let Some(record) = collection_authority_record { + bubblegum_set_collection_size_infos.push(record.clone()); + } + + invoke_signed( + &mpl_token_metadata::instruction::bubblegum_set_collection_size( + token_metadata_program.key(), + collection_metadata.to_account_info().key(), + collection_authority.key(), + collection_mint.key(), + bubblegum_signer.key(), + collection_authority_record.map(|r| r.key()), + new_size, + ), + bubblegum_set_collection_size_infos.as_slice(), + &[&[COLLECTION_CPI_PREFIX.as_bytes(), &[bubblegum_bump]]], + )?; + } else { + return Err(BubblegumError::CollectionMustBeSized.into()); + } + + Ok(()) +} + +fn process_collection_verification<'info>( + ctx: Context<'_, '_, '_, 'info, verify_collection::CollectionVerification<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + mut message: MetadataArgs, + verify: bool, + new_collection: Option, +) -> Result<()> { + let owner = ctx.accounts.leaf_owner.to_account_info(); + let delegate = ctx.accounts.leaf_delegate.to_account_info(); + let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); + let collection_metadata = &ctx.accounts.collection_metadata; + let collection_mint = ctx.accounts.collection_mint.to_account_info(); + let edition_account = ctx.accounts.edition_account.to_account_info(); + let collection_authority = ctx.accounts.collection_authority.to_account_info(); + let collection_authority_record_pda = ctx + .accounts + .collection_authority_record_pda + .to_account_info(); + let bubblegum_signer = ctx.accounts.bubblegum_signer.to_account_info(); + let token_metadata_program = ctx.accounts.token_metadata_program.to_account_info(); + + // User-provided metadata must result in same user-provided data hash. + let incoming_data_hash = hash_metadata(&message)?; + if data_hash != incoming_data_hash { + return Err(BubblegumError::DataHashMismatch.into()); + } + + // Note this call mutates message. + process_collection_verification_mpl_only( + collection_metadata, + &collection_mint, + &collection_authority, + &collection_authority_record_pda, + &edition_account, + &bubblegum_signer, + ctx.bumps["bubblegum_signer"], + &token_metadata_program, + &mut message, + verify, + new_collection, + )?; + + // Calculate new data hash. + let updated_data_hash = hash_metadata(&message)?; + + // Build previous leaf struct, new leaf struct, and replace the leaf in the tree. + let asset_id = get_asset_id(&merkle_tree.key(), nonce); + let previous_leaf = LeafSchema::new_v0( + asset_id, + owner.key(), + delegate.key(), + nonce, + data_hash, + creator_hash, + ); + let new_leaf = LeafSchema::new_v0( + asset_id, + owner.key(), + delegate.key(), + nonce, + updated_data_hash, + creator_hash, + ); + + wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; + + replace_leaf( + &merkle_tree.key(), + *ctx.bumps.get("tree_authority").unwrap(), + &ctx.accounts.compression_program.to_account_info(), + &ctx.accounts.tree_authority.to_account_info(), + &ctx.accounts.merkle_tree.to_account_info(), + &ctx.accounts.log_wrapper.to_account_info(), + ctx.remaining_accounts, + root, + previous_leaf.to_node(), + new_leaf.to_node(), + index, + ) +} diff --git a/programs/bubblegum/program/src/processor/redeem.rs b/programs/bubblegum/program/src/processor/redeem.rs new file mode 100644 index 00000000..8505cb7f --- /dev/null +++ b/programs/bubblegum/program/src/processor/redeem.rs @@ -0,0 +1,91 @@ +use anchor_lang::prelude::*; +use spl_account_compression::{program::SplAccountCompression, Node, Noop}; + +use crate::{ + error::BubblegumError, + state::{ + leaf_schema::LeafSchema, DecompressibleState, TreeConfig, Voucher, VOUCHER_PREFIX, + VOUCHER_SIZE, + }, + utils::{get_asset_id, replace_leaf}, +}; + +#[derive(Accounts)] +#[instruction( + _root: [u8; 32], + _data_hash: [u8; 32], + _creator_hash: [u8; 32], + nonce: u64, + _index: u32, +)] +pub struct Redeem<'info> { + #[account( + seeds = [merkle_tree.key().as_ref()], + bump, + )] + /// CHECK: This account is neither written to nor read from. + pub tree_authority: Account<'info, TreeConfig>, + #[account(mut)] + pub leaf_owner: Signer<'info>, + /// CHECK: This account is chekced in the instruction + pub leaf_delegate: UncheckedAccount<'info>, + #[account(mut)] + /// CHECK: checked in cpi + pub merkle_tree: UncheckedAccount<'info>, + #[account( + init, + seeds = [ + VOUCHER_PREFIX.as_ref(), + merkle_tree.key().as_ref(), + & nonce.to_le_bytes() + ], + payer = leaf_owner, + space = VOUCHER_SIZE, + bump + )] + pub voucher: Account<'info, Voucher>, + pub log_wrapper: Program<'info, Noop>, + pub compression_program: Program<'info, SplAccountCompression>, + pub system_program: Program<'info, System>, +} + +pub(crate) fn redeem<'info>( + ctx: Context<'_, '_, '_, 'info, Redeem<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, +) -> Result<()> { + if ctx.accounts.tree_authority.is_decompressible == DecompressibleState::Disabled { + return Err(BubblegumError::DecompressionDisabled.into()); + } + + let owner = ctx.accounts.leaf_owner.key(); + let delegate = ctx.accounts.leaf_delegate.key(); + let merkle_tree = ctx.accounts.merkle_tree.to_account_info(); + let asset_id = get_asset_id(&merkle_tree.key(), nonce); + let previous_leaf = + LeafSchema::new_v0(asset_id, owner, delegate, nonce, data_hash, creator_hash); + + let new_leaf = Node::default(); + + replace_leaf( + &merkle_tree.key(), + *ctx.bumps.get("tree_authority").unwrap(), + &ctx.accounts.compression_program.to_account_info(), + &ctx.accounts.tree_authority.to_account_info(), + &ctx.accounts.merkle_tree.to_account_info(), + &ctx.accounts.log_wrapper.to_account_info(), + ctx.remaining_accounts, + root, + previous_leaf.to_node(), + new_leaf, + index, + )?; + ctx.accounts + .voucher + .set_inner(Voucher::new(previous_leaf, index, merkle_tree.key())); + + Ok(()) +} diff --git a/programs/bubblegum/program/src/processor/set_and_verify_collection.rs b/programs/bubblegum/program/src/processor/set_and_verify_collection.rs new file mode 100644 index 00000000..b6bac586 --- /dev/null +++ b/programs/bubblegum/program/src/processor/set_and_verify_collection.rs @@ -0,0 +1,55 @@ +use anchor_lang::prelude::*; + +use crate::{ + error::BubblegumError, processor::process_collection_verification, + processor::verify_collection::CollectionVerification, state::metaplex_adapter::MetadataArgs, +}; + +pub(crate) fn set_and_verify_collection<'info>( + ctx: Context<'_, '_, '_, 'info, CollectionVerification<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + message: MetadataArgs, + collection: Pubkey, +) -> Result<()> { + 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; + let collection_metadata = &ctx.accounts.collection_metadata; + + // Require that either the tree authority signed this transaction, or the tree authority is + // the collection update authority which means the leaf update is approved via proxy, when + // we later call `assert_has_collection_authority()`. + // + // This is similar to logic in token-metadata for `set_and_verify_collection()` except + // this logic also allows the tree authority (which we are treating as the leaf metadata + // authority) to be different than the collection authority (actual or delegated). The + // token-metadata program required them to be the same. + let tree_authority_signed = incoming_tree_delegate.is_signer + && (incoming_tree_delegate.key() == tree_creator + || incoming_tree_delegate.key() == tree_delegate); + + let tree_authority_is_collection_update_authority = collection_metadata.update_authority + == tree_creator + || collection_metadata.update_authority == tree_delegate; + + require!( + tree_authority_signed || tree_authority_is_collection_update_authority, + BubblegumError::UpdateAuthorityIncorrect + ); + + process_collection_verification( + ctx, + root, + data_hash, + creator_hash, + nonce, + index, + message, + true, + Some(collection), + ) +} diff --git a/programs/bubblegum/program/src/processor/set_decompressible_state.rs b/programs/bubblegum/program/src/processor/set_decompressible_state.rs new file mode 100644 index 00000000..975c6c4b --- /dev/null +++ b/programs/bubblegum/program/src/processor/set_decompressible_state.rs @@ -0,0 +1,19 @@ +use anchor_lang::prelude::*; + +use crate::state::{DecompressibleState, TreeConfig}; + +#[derive(Accounts)] +pub struct SetDecompressibleState<'info> { + #[account(mut, has_one = tree_creator)] + pub tree_authority: Account<'info, TreeConfig>, + pub tree_creator: Signer<'info>, +} + +pub(crate) fn set_decompressible_state( + ctx: Context, + decompressable_state: DecompressibleState, +) -> Result<()> { + ctx.accounts.tree_authority.is_decompressible = decompressable_state; + + Ok(()) +} diff --git a/programs/bubblegum/program/src/processor/set_tree_delegate.rs b/programs/bubblegum/program/src/processor/set_tree_delegate.rs new file mode 100644 index 00000000..55fb5faa --- /dev/null +++ b/programs/bubblegum/program/src/processor/set_tree_delegate.rs @@ -0,0 +1,25 @@ +use anchor_lang::prelude::*; + +use crate::state::TreeConfig; + +#[derive(Accounts)] +pub struct SetTreeDelegate<'info> { + #[account( + mut, + seeds = [merkle_tree.key().as_ref()], + bump, + has_one = tree_creator + )] + pub tree_authority: Account<'info, TreeConfig>, + pub tree_creator: Signer<'info>, + /// CHECK: this account is neither read from or written to + pub new_tree_delegate: UncheckedAccount<'info>, + /// CHECK: this account is neither read from or written to + pub merkle_tree: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, +} + +pub(crate) fn set_tree_delegate(ctx: Context) -> Result<()> { + ctx.accounts.tree_authority.tree_delegate = ctx.accounts.new_tree_delegate.key(); + Ok(()) +} diff --git a/programs/bubblegum/program/src/processor/transfer.rs b/programs/bubblegum/program/src/processor/transfer.rs new file mode 100644 index 00000000..679a6193 --- /dev/null +++ b/programs/bubblegum/program/src/processor/transfer.rs @@ -0,0 +1,85 @@ +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}, +}; + +#[derive(Accounts)] +pub struct Transfer<'info> { + #[account( + seeds = [merkle_tree.key().as_ref()], + bump, + )] + /// CHECK: This account is neither written to nor read from. + pub tree_authority: Account<'info, TreeConfig>, + /// CHECK: This account is checked in the instruction + pub leaf_owner: UncheckedAccount<'info>, + /// CHECK: This account is chekced in the instruction + pub leaf_delegate: UncheckedAccount<'info>, + /// CHECK: This account is neither written to nor read from. + pub new_leaf_owner: UncheckedAccount<'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>, + pub system_program: Program<'info, System>, +} + +pub(crate) fn transfer<'info>( + ctx: Context<'_, '_, '_, 'info, Transfer<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, +) -> Result<()> { + // 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(); + let delegate = ctx.accounts.leaf_delegate.to_account_info(); + + // Transfers must be initiated by either the leaf owner or leaf delegate. + require!( + owner.is_signer || delegate.is_signer, + BubblegumError::LeafAuthorityMustSign + ); + let new_owner = ctx.accounts.new_leaf_owner.key(); + let asset_id = get_asset_id(&merkle_tree.key(), nonce); + let previous_leaf = LeafSchema::new_v0( + asset_id, + owner.key(), + delegate.key(), + nonce, + data_hash, + creator_hash, + ); + // New leafs are instantiated with no delegate + let new_leaf = LeafSchema::new_v0( + asset_id, + new_owner, + new_owner, + nonce, + data_hash, + creator_hash, + ); + + wrap_application_data_v1(new_leaf.to_event().try_to_vec()?, &ctx.accounts.log_wrapper)?; + + replace_leaf( + &merkle_tree.key(), + *ctx.bumps.get("tree_authority").unwrap(), + &ctx.accounts.compression_program.to_account_info(), + &ctx.accounts.tree_authority.to_account_info(), + &ctx.accounts.merkle_tree.to_account_info(), + &ctx.accounts.log_wrapper.to_account_info(), + ctx.remaining_accounts, + root, + previous_leaf.to_node(), + new_leaf.to_node(), + index, + ) +} diff --git a/programs/bubblegum/program/src/processor/unverify_collection.rs b/programs/bubblegum/program/src/processor/unverify_collection.rs new file mode 100644 index 00000000..3f769afe --- /dev/null +++ b/programs/bubblegum/program/src/processor/unverify_collection.rs @@ -0,0 +1,28 @@ +use anchor_lang::prelude::*; + +use crate::{ + processor::{process_collection_verification, verify_collection::CollectionVerification}, + state::metaplex_adapter::MetadataArgs, +}; + +pub(crate) fn unverify_collection<'info>( + ctx: Context<'_, '_, '_, 'info, CollectionVerification<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + message: MetadataArgs, +) -> Result<()> { + process_collection_verification( + ctx, + root, + data_hash, + creator_hash, + nonce, + index, + message, + false, + None, + ) +} diff --git a/programs/bubblegum/program/src/processor/unverify_creator.rs b/programs/bubblegum/program/src/processor/unverify_creator.rs new file mode 100644 index 00000000..0c7c4252 --- /dev/null +++ b/programs/bubblegum/program/src/processor/unverify_creator.rs @@ -0,0 +1,27 @@ +use anchor_lang::prelude::*; + +use crate::{ + processor::{process_creator_verification, verify_creator::CreatorVerification}, + state::metaplex_adapter::MetadataArgs, +}; + +pub(crate) fn unverify_creator<'info>( + ctx: Context<'_, '_, '_, 'info, CreatorVerification<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + message: MetadataArgs, +) -> Result<()> { + process_creator_verification( + ctx, + root, + data_hash, + creator_hash, + nonce, + index, + message, + false, + ) +} diff --git a/programs/bubblegum/program/src/processor/verify_collection.rs b/programs/bubblegum/program/src/processor/verify_collection.rs new file mode 100644 index 00000000..67ea7938 --- /dev/null +++ b/programs/bubblegum/program/src/processor/verify_collection.rs @@ -0,0 +1,75 @@ +use anchor_lang::prelude::*; +use spl_account_compression::{program::SplAccountCompression, Noop}; + +use crate::state::{ + metaplex_adapter::MetadataArgs, + metaplex_anchor::{MplTokenMetadata, TokenMetadata}, + TreeConfig, COLLECTION_CPI_PREFIX, +}; + +use super::process_collection_verification; + +#[derive(Accounts)] +pub struct CollectionVerification<'info> { + #[account( + seeds = [merkle_tree.key().as_ref()], + bump, + )] + pub tree_authority: Account<'info, TreeConfig>, + /// CHECK: This account is checked in the instruction + pub leaf_owner: UncheckedAccount<'info>, + /// CHECK: This account is checked in the instruction + pub leaf_delegate: UncheckedAccount<'info>, + #[account(mut)] + /// CHECK: This account is modified in the downstream program + pub merkle_tree: UncheckedAccount<'info>, + pub payer: Signer<'info>, + /// CHECK: This account is checked in the instruction + /// This account is checked to be a signer in + /// the case of `set_and_verify_collection` where + /// we are actually changing the NFT metadata. + pub tree_delegate: UncheckedAccount<'info>, + pub collection_authority: Signer<'info>, + /// CHECK: Optional collection authority record PDA. + /// If there is no collecton authority record PDA then + /// this must be the Bubblegum program address. + pub collection_authority_record_pda: UncheckedAccount<'info>, + /// CHECK: This account is checked in the instruction + pub collection_mint: UncheckedAccount<'info>, + #[account(mut)] + pub collection_metadata: Box>, + /// CHECK: This account is checked in the instruction + pub edition_account: UncheckedAccount<'info>, + /// CHECK: This is just used as a signing PDA. + #[account( + seeds = [COLLECTION_CPI_PREFIX.as_ref()], + bump, + )] + pub bubblegum_signer: UncheckedAccount<'info>, + pub log_wrapper: Program<'info, Noop>, + pub compression_program: Program<'info, SplAccountCompression>, + pub token_metadata_program: Program<'info, MplTokenMetadata>, + pub system_program: Program<'info, System>, +} + +pub(crate) fn verify_collection<'info>( + ctx: Context<'_, '_, '_, 'info, CollectionVerification<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + message: MetadataArgs, +) -> Result<()> { + process_collection_verification( + ctx, + root, + data_hash, + creator_hash, + nonce, + index, + message, + true, + None, + ) +} diff --git a/programs/bubblegum/program/src/processor/verify_creator.rs b/programs/bubblegum/program/src/processor/verify_creator.rs new file mode 100644 index 00000000..f34c7be8 --- /dev/null +++ b/programs/bubblegum/program/src/processor/verify_creator.rs @@ -0,0 +1,49 @@ +use anchor_lang::prelude::*; +use spl_account_compression::{program::SplAccountCompression, Noop}; + +use crate::{ + processor::process_creator_verification, + state::{metaplex_adapter::MetadataArgs, TreeConfig}, +}; + +#[derive(Accounts)] +pub struct CreatorVerification<'info> { + #[account( + seeds = [merkle_tree.key().as_ref()], + bump, + )] + pub tree_authority: Account<'info, TreeConfig>, + /// CHECK: This account is checked in the instruction + pub leaf_owner: UncheckedAccount<'info>, + /// CHECK: This account is checked in the instruction + pub leaf_delegate: UncheckedAccount<'info>, + #[account(mut)] + /// CHECK: This account is modified in the downstream program + 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>, + pub system_program: Program<'info, System>, +} + +pub(crate) fn verify_creator<'info>( + ctx: Context<'_, '_, '_, 'info, CreatorVerification<'info>>, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + message: MetadataArgs, +) -> Result<()> { + process_creator_verification( + ctx, + root, + data_hash, + creator_hash, + nonce, + index, + message, + true, + ) +} diff --git a/programs/bubblegum/program/src/state/mod.rs b/programs/bubblegum/program/src/state/mod.rs index 68bce3a3..3ca74245 100644 --- a/programs/bubblegum/program/src/state/mod.rs +++ b/programs/bubblegum/program/src/state/mod.rs @@ -20,7 +20,7 @@ pub struct TreeConfig { pub total_mint_capacity: u64, pub num_minted: u64, pub is_public: bool, - pub is_decompressable: DecompressableState, + pub is_decompressible: DecompressibleState, } impl TreeConfig { @@ -83,7 +83,7 @@ pub enum BubblegumEventType { #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Copy)] #[repr(u8)] -pub enum DecompressableState { +pub enum DecompressibleState { Enabled = 0, Disabled = 1, } diff --git a/programs/bubblegum/program/src/utils.rs b/programs/bubblegum/program/src/utils.rs index 0c51f1e9..3dfc915a 100644 --- a/programs/bubblegum/program/src/utils.rs +++ b/programs/bubblegum/program/src/utils.rs @@ -1,54 +1,39 @@ -use crate::{error::BubblegumError, state::metaplex_adapter::MetadataArgs, ASSET_PREFIX}; +use crate::state::{ + metaplex_adapter::{Creator, MetadataArgs}, + ASSET_PREFIX, +}; use anchor_lang::{ prelude::*, solana_program::{program_memory::sol_memcmp, pubkey::PUBKEY_BYTES}, }; -use mpl_token_metadata::{ - instruction::MetadataDelegateRole, - pda::{find_collection_authority_account, find_metadata_delegate_record_account}, - state::{CollectionAuthorityRecord, Metadata, MetadataDelegateRecord, TokenMetadataAccount}, -}; +use solana_program::keccak; use spl_account_compression::Node; -/// Assert that the provided MetadataArgs are compatible with MPL `Data` -pub fn assert_metadata_is_mpl_compatible(metadata: &MetadataArgs) -> Result<()> { - if metadata.name.len() > mpl_token_metadata::state::MAX_NAME_LENGTH { - return Err(BubblegumError::MetadataNameTooLong.into()); - } - - if metadata.symbol.len() > mpl_token_metadata::state::MAX_SYMBOL_LENGTH { - return Err(BubblegumError::MetadataSymbolTooLong.into()); - } - - if metadata.uri.len() > mpl_token_metadata::state::MAX_URI_LENGTH { - return Err(BubblegumError::MetadataUriTooLong.into()); - } - - if metadata.seller_fee_basis_points > 10000 { - return Err(BubblegumError::MetadataBasisPointsTooHigh.into()); - } - if !metadata.creators.is_empty() { - if metadata.creators.len() > mpl_token_metadata::state::MAX_CREATOR_LIMIT { - return Err(BubblegumError::CreatorsTooLong.into()); - } +pub fn hash_creators(creators: &[Creator]) -> Result<[u8; 32]> { + // Convert creator Vec to bytes Vec. + let creator_data = creators + .iter() + .map(|c| [c.address.as_ref(), &[c.verified as u8], &[c.share]].concat()) + .collect::>(); + // Calculate new creator hash. + Ok(keccak::hashv( + creator_data + .iter() + .map(|c| c.as_slice()) + .collect::>() + .as_ref(), + ) + .to_bytes()) +} - let mut total: u8 = 0; - for i in 0..metadata.creators.len() { - let creator = &metadata.creators[i]; - for iter in metadata.creators.iter().skip(i + 1) { - if iter.address == creator.address { - return Err(BubblegumError::DuplicateCreatorAddress.into()); - } - } - total = total - .checked_add(creator.share) - .ok_or(BubblegumError::CreatorShareTotalMustBe100)?; - } - if total != 100 { - return Err(BubblegumError::CreatorShareTotalMustBe100.into()); - } - } - Ok(()) +pub fn hash_metadata(metadata: &MetadataArgs) -> Result<[u8; 32]> { + let metadata_args_hash = keccak::hashv(&[metadata.try_to_vec()?.as_slice()]); + // Calculate new data hash. + Ok(keccak::hashv(&[ + &metadata_args_hash.to_bytes(), + &metadata.seller_fee_basis_points.to_le_bytes(), + ]) + .to_bytes()) } pub fn replace_leaf<'info>( @@ -110,51 +95,6 @@ pub fn cmp_bytes(a: &[u8], b: &[u8], size: usize) -> bool { sol_memcmp(a, b, size) == 0 } -pub fn assert_pubkey_equal( - a: &Pubkey, - b: &Pubkey, - error: Option, -) -> Result<()> { - if !cmp_pubkeys(a, b) { - if let Some(err) = error { - Err(err) - } else { - Err(BubblegumError::PublicKeyMismatch.into()) - } - } else { - Ok(()) - } -} - -pub fn assert_derivation( - program_id: &Pubkey, - account: &AccountInfo, - path: &[&[u8]], - error: Option, -) -> Result { - let (key, bump) = Pubkey::find_program_address(path, program_id); - if !cmp_pubkeys(&key, account.key) { - if let Some(err) = error { - msg!("Derivation {:?}", err); - Err(err) - } else { - msg!("DerivedKeyInvalid"); - Err(ProgramError::InvalidInstructionData.into()) - } - } else { - Ok(bump) - } -} - -pub fn assert_owned_by(account: &AccountInfo, owner: &Pubkey) -> Result<()> { - if !cmp_pubkeys(account.owner, owner) { - //todo add better errors - Err(ProgramError::IllegalOwner.into()) - } else { - Ok(()) - } -} - pub fn get_asset_id(tree_id: &Pubkey, nonce: u64) -> Pubkey { Pubkey::find_program_address( &[ @@ -166,62 +106,3 @@ pub fn get_asset_id(tree_id: &Pubkey, nonce: u64) -> Pubkey { ) .0 } - -// Checks both delegate types: old collection_authority_record and newer -// metadata_delegate -pub fn assert_has_collection_authority( - collection_data: &Metadata, - mint: &Pubkey, - collection_authority: &Pubkey, - delegate_record: Option<&AccountInfo>, -) -> Result<()> { - // Mint is the correct one for the metadata account. - if collection_data.mint != *mint { - return Err(BubblegumError::MetadataMintMismatch.into()); - } - - if let Some(record_info) = delegate_record { - let (ca_pda, ca_bump) = find_collection_authority_account(mint, collection_authority); - let (md_pda, md_bump) = find_metadata_delegate_record_account( - mint, - MetadataDelegateRole::Collection, - &collection_data.update_authority, - collection_authority, - ); - - let data = record_info.try_borrow_data()?; - if data.len() == 0 { - return Err(BubblegumError::InvalidCollectionAuthority.into()); - } - - if record_info.key == &ca_pda { - let record = CollectionAuthorityRecord::safe_deserialize(&data)?; - if record.bump != ca_bump { - return Err(BubblegumError::InvalidCollectionAuthority.into()); - } - - match record.update_authority { - Some(update_authority) => { - if update_authority != collection_data.update_authority { - return Err(BubblegumError::InvalidCollectionAuthority.into()); - } - } - None => return Err(BubblegumError::InvalidCollectionAuthority.into()), - } - } else if record_info.key == &md_pda { - let record = MetadataDelegateRecord::safe_deserialize(&data)?; - if record.bump != md_bump { - return Err(BubblegumError::InvalidCollectionAuthority.into()); - } - - if record.update_authority != collection_data.update_authority { - return Err(BubblegumError::InvalidCollectionAuthority.into()); - } - } else { - return Err(BubblegumError::InvalidDelegateRecord.into()); - } - } else if collection_data.update_authority != *collection_authority { - return Err(BubblegumError::InvalidCollectionAuthority.into()); - } - Ok(()) -} diff --git a/programs/bubblegum/program/tests/utils/context.rs b/programs/bubblegum/program/tests/utils/context.rs index 31eb61d7..2b25836d 100644 --- a/programs/bubblegum/program/tests/utils/context.rs +++ b/programs/bubblegum/program/tests/utils/context.rs @@ -25,6 +25,7 @@ pub struct BubblegumTestContext { pub const DEFAULT_LAMPORTS_FUND_AMOUNT: u64 = 1_000_000_000; +#[allow(deprecated)] impl BubblegumTestContext { pub fn test_context(&self) -> &ProgramTestContext { &self.program_context diff --git a/programs/bubblegum/program/tests/utils/mod.rs b/programs/bubblegum/program/tests/utils/mod.rs index 5aa3a6a1..2b1f5fcd 100644 --- a/programs/bubblegum/program/tests/utils/mod.rs +++ b/programs/bubblegum/program/tests/utils/mod.rs @@ -5,7 +5,10 @@ pub mod tx_builder; use anchor_lang::{self, InstructionData, ToAccountMetas}; use async_trait::async_trait; -use bubblegum::{hash_creators, hash_metadata, state::metaplex_adapter::MetadataArgs}; +use bubblegum::{ + state::metaplex_adapter::MetadataArgs, + utils::{hash_creators, hash_metadata}, +}; use bytemuck::PodCastError; use solana_program::{instruction::Instruction, pubkey::Pubkey, system_instruction}; use solana_program_test::{BanksClientError, ProgramTest, ProgramTestContext}; diff --git a/programs/bubblegum/program/tests/utils/tree.rs b/programs/bubblegum/program/tests/utils/tree.rs index 6427ed44..9d6451c1 100644 --- a/programs/bubblegum/program/tests/utils/tree.rs +++ b/programs/bubblegum/program/tests/utils/tree.rs @@ -12,7 +12,7 @@ use super::{ use crate::utils::tx_builder::DecompressV1Builder; use anchor_lang::{self, AccountDeserialize}; use bubblegum::{ - state::{leaf_schema::LeafSchema, DecompressableState, TreeConfig, Voucher, VOUCHER_PREFIX}, + state::{leaf_schema::LeafSchema, DecompressibleState, TreeConfig, Voucher, VOUCHER_PREFIX}, utils::get_asset_id, }; use bytemuck::try_from_bytes; @@ -1017,9 +1017,9 @@ impl Tree SetDecompressableStateBuilder { - let accounts = bubblegum::accounts::SetDecompressableState { + let accounts = bubblegum::accounts::SetDecompressibleState { tree_authority: self.authority(), tree_creator: self.creator_pubkey(), }; @@ -1041,14 +1041,14 @@ impl Tree Result<()> { - self.set_decompression_tx(DecompressableState::Enabled) + self.set_decompression_tx(DecompressibleState::Enabled) .execute() .await } // Disable Decompression pub async fn disable_decompression(&mut self) -> Result<()> { - self.set_decompression_tx(DecompressableState::Disabled) + self.set_decompression_tx(DecompressibleState::Disabled) .execute() .await } diff --git a/programs/bubblegum/program/tests/utils/tx_builder.rs b/programs/bubblegum/program/tests/utils/tx_builder.rs index bbe47893..92469666 100644 --- a/programs/bubblegum/program/tests/utils/tx_builder.rs +++ b/programs/bubblegum/program/tests/utils/tx_builder.rs @@ -389,7 +389,7 @@ impl<'a, const MAX_DEPTH: usize, const MAX_BUFFER_SIZE: usize> OnSuccessfulTxExe pub type SetDecompressableStateBuilder<'a, const MAX_DEPTH: usize, const MAX_BUFFER_SIZE: usize> = TxBuilder< 'a, - bubblegum::accounts::SetDecompressableState, + bubblegum::accounts::SetDecompressibleState, bubblegum::instruction::SetDecompressableState, (), MAX_DEPTH,