Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Check compression programs at runtime #115

Merged
merged 15 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions clients/js/src/createTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ export const createTree = async (
getMerkleTreeSize(input.maxDepth, input.maxBufferSize, input.canopyDepth);
const lamports = await context.rpc.getRent(space);

let programId;
if (input.compressionProgram) {
programId = Array.isArray(input.compressionProgram)
? input.compressionProgram[0]
: input.compressionProgram;
} else {
programId = context.programs.getPublicKey(
'splAccountCompression',
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID
);
}

return (
transactionBuilder()
// Create the empty Merkle tree account.
Expand All @@ -36,10 +48,7 @@ export const createTree = async (
newAccount: input.merkleTree,
lamports,
space,
programId: context.programs.getPublicKey(
'splAccountCompression',
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID
),
programId,
})
)
// Create the tree config.
Expand Down
26 changes: 26 additions & 0 deletions clients/js/src/generated/errors/mplBubblegum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,32 @@ export class InvalidCanopySizeError extends ProgramError {
codeToErrorMap.set(0x1799, InvalidCanopySizeError);
nameToErrorMap.set('InvalidCanopySize', InvalidCanopySizeError);

/** InvalidLogWrapper: Invalid log wrapper program */
export class InvalidLogWrapperError extends ProgramError {
readonly name: string = 'InvalidLogWrapper';

readonly code: number = 0x179a; // 6042

constructor(program: Program, cause?: Error) {
super('Invalid log wrapper program', program, cause);
}
}
codeToErrorMap.set(0x179a, InvalidLogWrapperError);
nameToErrorMap.set('InvalidLogWrapper', InvalidLogWrapperError);

/** InvalidCompressionProgram: Invalid compression program */
export class InvalidCompressionProgramError extends ProgramError {
readonly name: string = 'InvalidCompressionProgram';

readonly code: number = 0x179b; // 6043

constructor(program: Program, cause?: Error) {
super('Invalid compression program', program, cause);
}
}
codeToErrorMap.set(0x179b, InvalidCompressionProgramError);
nameToErrorMap.set('InvalidCompressionProgram', InvalidCompressionProgramError);

/**
* Attempts to resolve a custom program error from the provided error code.
* @category Errors
Expand Down
253 changes: 251 additions & 2 deletions clients/js/test/createTree.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,75 @@
import { generateSigner, publicKey } from '@metaplex-foundation/umi';
import { createAccount } from '@metaplex-foundation/mpl-toolbox';
import {
generateSigner,
publicKey,
Context,
Signer,
TransactionBuilder,
transactionBuilder,
PublicKey,
} from '@metaplex-foundation/umi';
import test from 'ava';
import { TreeConfig, createTree, fetchTreeConfigFromSeeds } from '../src';
import {
TreeConfig,
createTree,
createTreeConfig,
fetchTreeConfigFromSeeds,
safeFetchTreeConfigFromSeeds,
getMerkleTreeSize,
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
} from '../src';
import { createUmi } from './_setup';

const createTreeWithSpecificMerkleOwner = async (
context: Parameters<typeof createAccount>[0] &
Parameters<typeof createTreeConfig>[0] &
Pick<Context, 'rpc'>,
input: Omit<Parameters<typeof createTreeConfig>[1], 'merkleTree'> & {
merkleTree: Signer;
merkleTreeSize?: number;
canopyDepth?: number;
merkleTreeOwner?: PublicKey;
}
): Promise<TransactionBuilder> => {
const space =
input.merkleTreeSize ??
getMerkleTreeSize(input.maxDepth, input.maxBufferSize, input.canopyDepth);
const lamports = await context.rpc.getRent(space);

let programId;
if (input.compressionProgram) {
programId = Array.isArray(input.compressionProgram)
? input.compressionProgram[0]
: input.compressionProgram;
} else {
programId = context.programs.getPublicKey(
'splAccountCompression',
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID
);
}

return (
transactionBuilder()
// Create the empty Merkle tree account.
.add(
createAccount(context, {
payer: input.payer ?? context.payer,
newAccount: input.merkleTree,
lamports,
space,
programId: input.merkleTreeOwner ? input.merkleTreeOwner : programId,
})
)
// Create the tree config.
.add(
createTreeConfig(context, {
...input,
merkleTree: input.merkleTree.publicKey,
})
)
);
};

test('it can create a Bubblegum tree', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
Expand Down Expand Up @@ -60,3 +127,185 @@ test('it can create a Bubblegum tree using a newer size', async (t) => {
isPublic: false,
});
});

test('it can create a Bubblegum tree using mpl-account-compression and mpl-noop', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
const merkleTree = generateSigner(umi);

// When we create a tree at this address.
const builder = await createTree(umi, {
merkleTree,
maxDepth: 14,
maxBufferSize: 64,
logWrapper: publicKey('mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3'),
compressionProgram: publicKey(
'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW'
),
});
await builder.sendAndConfirm(umi);

// Then an account exists at the merkle tree address.
t.true(await umi.rpc.accountExists(merkleTree.publicKey));

// And a tree config was created with the correct data.
const treeConfig = await fetchTreeConfigFromSeeds(umi, {
merkleTree: merkleTree.publicKey,
});
t.like(treeConfig, <TreeConfig>{
treeCreator: publicKey(umi.identity),
treeDelegate: publicKey(umi.identity),
totalMintCapacity: 2n ** 14n,
numMinted: 0n,
isPublic: false,
});
});

test('it cannot create a Bubblegum tree using invalid logWrapper with spl-account-compression', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
const merkleTree = generateSigner(umi);

// When we create a tree at this address.
const builder = await createTree(umi, {
merkleTree,
maxDepth: 14,
maxBufferSize: 64,
logWrapper: generateSigner(umi).publicKey,
});

const promise = builder.sendAndConfirm(umi);

// Then we expect a program error.
await t.throwsAsync(promise, { name: 'InvalidLogWrapper' });

// And an account does not exist at the merkle tree address.
t.false(await umi.rpc.accountExists(merkleTree.publicKey));

// And a tree config was not created with the correct data.
const treeConfig = await safeFetchTreeConfigFromSeeds(umi, {
merkleTree: merkleTree.publicKey,
});
t.is(treeConfig, null);
});

test('it cannot create a Bubblegum tree using invalid logWrapper with mpl-account-compression', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
const merkleTree = generateSigner(umi);

// When we create a tree at this address.
const builder = await createTree(umi, {
merkleTree,
maxDepth: 14,
maxBufferSize: 64,
logWrapper: generateSigner(umi).publicKey,
compressionProgram: publicKey(
'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW'
),
});

const promise = builder.sendAndConfirm(umi);

// Then we expect a program error.
await t.throwsAsync(promise, { name: 'InvalidLogWrapper' });

// And an account does not exist at the merkle tree address.
t.false(await umi.rpc.accountExists(merkleTree.publicKey));

// And a tree config was not created with the correct data.
const treeConfig = await safeFetchTreeConfigFromSeeds(umi, {
merkleTree: merkleTree.publicKey,
});
t.is(treeConfig, null);
});

test('it cannot create a Bubblegum tree when compression program does not match tree owned by spl-account-compression', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
const merkleTree = generateSigner(umi);

// When we create a tree at this address.
const builder = await createTreeWithSpecificMerkleOwner(umi, {
merkleTree,
maxDepth: 14,
maxBufferSize: 64,
compressionProgram: generateSigner(umi).publicKey,
merkleTreeOwner: umi.programs.getPublicKey(
'splAccountCompression',
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID
),
});

const promise = builder.sendAndConfirm(umi);

// Then we expect a program error.
await t.throwsAsync(promise, { name: 'InvalidCompressionProgram' });

// And an account does not exist at the merkle tree address.
t.false(await umi.rpc.accountExists(merkleTree.publicKey));

// And a tree config was not created with the correct data.
const treeConfig = await safeFetchTreeConfigFromSeeds(umi, {
merkleTree: merkleTree.publicKey,
});
t.is(treeConfig, null);
});

test('it cannot create a Bubblegum tree when compression program does not match tree owned by mpl-account-compression', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
const merkleTree = generateSigner(umi);

// When we create a tree at this address.
const builder = await createTreeWithSpecificMerkleOwner(umi, {
merkleTree,
maxDepth: 14,
maxBufferSize: 64,
logWrapper: publicKey('mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3'),
compressionProgram: generateSigner(umi).publicKey,
merkleTreeOwner: publicKey('mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW'),
});

const promise = builder.sendAndConfirm(umi);

// Then we expect a program error.
await t.throwsAsync(promise, { name: 'InvalidCompressionProgram' });

// And an account does not exist at the merkle tree address.
t.false(await umi.rpc.accountExists(merkleTree.publicKey));

// And a tree config was not created with the correct data.
const treeConfig = await safeFetchTreeConfigFromSeeds(umi, {
merkleTree: merkleTree.publicKey,
});
t.is(treeConfig, null);
});

test('it cannot create a Bubblegum tree with incorrect Merkle tree owner', async (t) => {
// Given a brand new merkle tree signer.
const umi = await createUmi();
const merkleTree = generateSigner(umi);

// When we create a tree at this address.
const builder = await createTreeWithSpecificMerkleOwner(umi, {
merkleTree,
maxDepth: 14,
maxBufferSize: 64,
merkleTreeOwner: generateSigner(umi).publicKey,
});

const promise = builder.sendAndConfirm(umi);

// Then we expect a program error.
await t.throwsAsync(promise, { name: 'IncorrectOwner' });

// And an account does not exist at the merkle tree address.
t.false(await umi.rpc.accountExists(merkleTree.publicKey));

// And a tree config was not created with the correct data.
const treeConfig = await safeFetchTreeConfigFromSeeds(umi, {
merkleTree: merkleTree.publicKey,
});
t.is(treeConfig, null);
});
Loading
Loading