Skip to content

Commit

Permalink
Fixes to set_and_verify_collection (#43)
Browse files Browse the repository at this point in the history
* Removing deploy capabilities

* Adding back program deploy.

* Update collection verification checks

* Add Umi tests around collection verification

* Add deprecated log message (#2)

* Set decompressible on tree creation (#3)

* Fix binary name

* Set decompressible on tree creation

* Fix binary name

* Revert "Removing deploy capabilities"

This reverts commit c203069.

---------

Co-authored-by: blockiosaurus <[email protected]>
Co-authored-by: Fernando Otero <[email protected]>
  • Loading branch information
3 people authored Sep 28, 2023
1 parent ecd1146 commit fe5dae5
Show file tree
Hide file tree
Showing 10 changed files with 380 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/deploy-program.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ jobs:
if [ "${{ inputs.program }}" == "bubblegum" ]; then
echo ${{ secrets.BUBBLEGUM_DEPLOY_KEY }} > ./deploy-key.json
echo ${{ secrets.BUBBLEGUM_ID }} > ./program-id.json
echo PROGRAM_NAME="mpl_bubblegum" >> $GITHUB_ENV
echo PROGRAM_NAME="bubblegum" >> $GITHUB_ENV
fi
- name: Bump program version
Expand Down
2 changes: 1 addition & 1 deletion clients/js-solita/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"postpublish": "git push origin && git push origin --tags",
"build:docs": "typedoc",
"build": "rimraf dist && tsc -p tsconfig.json",
"start-validator": "solana-test-validator -ud --quiet --reset -c cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK -c 4VTQredsAmr1yzRJugLV6Mt6eu6XMeCwdkZ73wwVMWHv -c noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV -c 3RHkdjCwWyK2firrwFQGvXCxbUpBky1GTmb9EDK9hUnX -c metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s -c PwDiXFxQsGra4sFFTT8r1QWRMd4vfumiWC1jfWNfdYT --bpf-program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY ../../programs/.bin/mpl_bubblegum.so",
"start-validator": "solana-test-validator -ud --quiet --reset -c cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK -c 4VTQredsAmr1yzRJugLV6Mt6eu6XMeCwdkZ73wwVMWHv -c noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV -c 3RHkdjCwWyK2firrwFQGvXCxbUpBky1GTmb9EDK9hUnX -c metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s -c PwDiXFxQsGra4sFFTT8r1QWRMd4vfumiWC1jfWNfdYT --bpf-program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY ../../programs/.bin/bubblegum.so",
"run-tests": "jest tests --detectOpenHandles",
"test": "start-server-and-test start-validator http://localhost:8899/health run-tests",
"api:gen": "DEBUG='(solita|rustbin):(info|error)' solita",
Expand Down
127 changes: 85 additions & 42 deletions clients/js-solita/tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
ConcurrentMerkleTreeAccount,
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
ValidDepthSizePair
ValidDepthSizePair,
} from '@solana/spl-account-compression';

import {
Expand All @@ -30,19 +30,21 @@ import {
TokenProgramVersion,
TokenStandard,
Creator,
createSetDecompressibleStateInstruction,
DecompressibleState,
} from '../src/generated';
import { getLeafAssetId, computeDataHash, computeCreatorHash, computeCompressedNFTHash } from '../src/mpl-bubblegum';
import { BN } from 'bn.js';
import { PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID
} from "@solana/spl-token";
getLeafAssetId,
computeDataHash,
computeCreatorHash,
computeCompressedNFTHash,
} from '../src/mpl-bubblegum';
import { BN } from 'bn.js';
import { PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID } from '@metaplex-foundation/mpl-token-metadata';
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token';

function keypairFromSeed(seed: string) {
const expandedSeed = Uint8Array.from(
Buffer.from(`${seed}`),
);
const expandedSeed = Uint8Array.from(Buffer.from(`${seed}`));
return Keypair.fromSeed(expandedSeed.slice(0, 32));
}

Expand All @@ -69,16 +71,19 @@ async function setupTreeWithCompressedNFT(
compressedNFT: MetadataArgs,
depthSizePair: ValidDepthSizePair = {
maxDepth: 14,
maxBufferSize: 64
}
maxBufferSize: 64,
},
): Promise<{
merkleTree: PublicKey;
}> {
const payer = payerKeypair.publicKey;

const merkleTreeKeypair = Keypair.generate();
const merkleTree = merkleTreeKeypair.publicKey;
const space = getConcurrentMerkleTreeAccountSize(depthSizePair.maxDepth, depthSizePair.maxBufferSize);
const space = getConcurrentMerkleTreeAccountSize(
depthSizePair.maxDepth,
depthSizePair.maxBufferSize,
);
const allocTreeIx = SystemProgram.createAccount({
fromPubkey: payer,
newAccountPubkey: merkleTree,
Expand Down Expand Up @@ -107,6 +112,17 @@ async function setupTreeWithCompressedNFT(
BUBBLEGUM_PROGRAM_ID,
);

const setDecompressibleStateIx = createSetDecompressibleStateInstruction(
{
treeAuthority,
treeCreator: payer,
},
{
decompressableState: DecompressibleState.Enabled,
},
BUBBLEGUM_PROGRAM_ID,
);

const mintIx = createMintV1Instruction(
{
merkleTree,
Expand All @@ -123,7 +139,11 @@ async function setupTreeWithCompressedNFT(
},
);

const tx = new Transaction().add(allocTreeIx).add(createTreeIx).add(mintIx);
const tx = new Transaction()
.add(allocTreeIx)
.add(createTreeIx)
.add(setDecompressibleStateIx)
.add(mintIx);
tx.feePayer = payer;
await sendAndConfirmTransaction(connection, tx, [merkleTreeKeypair, payerKeypair], {
commitment: 'confirmed',
Expand Down Expand Up @@ -158,7 +178,10 @@ describe('Bubblegum tests', () => {
sellerFeeBasisPoints: 0,
isMutable: false,
};
await setupTreeWithCompressedNFT(connection, payerKeypair, compressedNFT, { maxDepth: 14, maxBufferSize: 64 });
await setupTreeWithCompressedNFT(connection, payerKeypair, compressedNFT, {
maxDepth: 14,
maxBufferSize: 64,
});
});

describe('Unit test compressed NFT instructions', () => {
Expand All @@ -174,7 +197,7 @@ describe('Bubblegum tests', () => {
share: 45,
verified: false,
},
]
];
const originalCompressedNFT = makeCompressedNFT('test', 'TST', creators);
beforeEach(async () => {
await connection.requestAirdrop(payer, LAMPORTS_PER_SOL);
Expand All @@ -185,7 +208,7 @@ describe('Bubblegum tests', () => {
{
maxDepth: 14,
maxBufferSize: 64,
}
},
);
merkleTree = result.merkleTree;
});
Expand All @@ -197,15 +220,12 @@ describe('Bubblegum tests', () => {
// Verify leaf exists.
const leafIndex = new BN.BN(0);
const assetId = await getLeafAssetId(merkleTree, leafIndex);
const verifyLeafIx = createVerifyLeafIx(
merkleTree,
{
root: account.getCurrentRoot(),
leaf: computeCompressedNFTHash(assetId, payer, payer, leafIndex, originalCompressedNFT),
leafIndex: 0,
proof: [],
}
);
const verifyLeafIx = createVerifyLeafIx(merkleTree, {
root: account.getCurrentRoot(),
leaf: computeCompressedNFTHash(assetId, payer, payer, leafIndex, originalCompressedNFT),
leafIndex: 0,
proof: [],
});
const tx = new Transaction().add(verifyLeafIx);
const txId = await sendAndConfirmTransaction(connection, tx, [payerKeypair], {
commitment: 'confirmed',
Expand Down Expand Up @@ -240,7 +260,7 @@ describe('Bubblegum tests', () => {
dataHash: Array.from(computeDataHash(originalCompressedNFT)),
creatorHash: Array.from(computeCreatorHash(originalCompressedNFT.creators)),
nonce: 0,
index: 0
index: 0,
},
);

Expand Down Expand Up @@ -268,16 +288,21 @@ describe('Bubblegum tests', () => {
dataHash: Array.from(computeDataHash(originalCompressedNFT)),
creatorHash: Array.from(computeCreatorHash(originalCompressedNFT.creators)),
nonce: 0,
index: 0
index: 0,
},
);

const burnTx = new Transaction().add(burnIx);
burnTx.feePayer = payer;
const burnTxId = await sendAndConfirmTransaction(connection, burnTx, [payerKeypair, newLeafOwnerKeypair], {
commitment: 'confirmed',
skipPreflight: true,
});
const burnTxId = await sendAndConfirmTransaction(
connection,
burnTx,
[payerKeypair, newLeafOwnerKeypair],
{
commitment: 'confirmed',
skipPreflight: true,
},
);

console.log('NFT burn tx:', burnTxId);
});
Expand All @@ -292,7 +317,11 @@ describe('Bubblegum tests', () => {
);
const nonce = new BN.BN(0);
const [voucher] = PublicKey.findProgramAddressSync(
[Buffer.from('voucher', 'utf8'), merkleTree.toBuffer(), Uint8Array.from(nonce.toArray('le', 8))],
[
Buffer.from('voucher', 'utf8'),
merkleTree.toBuffer(),
Uint8Array.from(nonce.toArray('le', 8)),
],
BUBBLEGUM_PROGRAM_ID,
);

Expand All @@ -311,7 +340,7 @@ describe('Bubblegum tests', () => {
dataHash: Array.from(computeDataHash(originalCompressedNFT)),
creatorHash: Array.from(computeCreatorHash(originalCompressedNFT.creators)),
nonce,
index: 0
index: 0,
},
);

Expand All @@ -326,7 +355,11 @@ describe('Bubblegum tests', () => {

// Decompress.
const [mint] = PublicKey.findProgramAddressSync(
[Buffer.from('asset', 'utf8'), merkleTree.toBuffer(), Uint8Array.from(nonce.toArray('le', 8))],
[
Buffer.from('asset', 'utf8'),
merkleTree.toBuffer(),
Uint8Array.from(nonce.toArray('le', 8)),
],
BUBBLEGUM_PROGRAM_ID,
);
const [tokenAccount] = PublicKey.findProgramAddressSync(
Expand All @@ -335,14 +368,19 @@ describe('Bubblegum tests', () => {
);
const [mintAuthority] = PublicKey.findProgramAddressSync(
[mint.toBuffer()],
BUBBLEGUM_PROGRAM_ID
BUBBLEGUM_PROGRAM_ID,
);
const [metadata] = PublicKey.findProgramAddressSync(
[Buffer.from('metadata', 'utf8'), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer()],
TOKEN_METADATA_PROGRAM_ID,
);
const [masterEdition] = PublicKey.findProgramAddressSync(
[Buffer.from('metadata', 'utf8'), TOKEN_METADATA_PROGRAM_ID.toBuffer(), mint.toBuffer(), Buffer.from('edition', 'utf8')],
[
Buffer.from('metadata', 'utf8'),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
Buffer.from('edition', 'utf8'),
],
TOKEN_METADATA_PROGRAM_ID,
);

Expand All @@ -361,16 +399,21 @@ describe('Bubblegum tests', () => {
logWrapper: SPL_NOOP_PROGRAM_ID,
},
{
metadata: originalCompressedNFT
metadata: originalCompressedNFT,
},
);

const decompressTx = new Transaction().add(decompressIx);
decompressTx.feePayer = payer;
const decompressTxId = await sendAndConfirmTransaction(connection, decompressTx, [payerKeypair], {
commitment: 'confirmed',
skipPreflight: true,
});
const decompressTxId = await sendAndConfirmTransaction(
connection,
decompressTx,
[payerKeypair],
{
commitment: 'confirmed',
skipPreflight: true,
},
);

console.log('NFT decompress tx:', decompressTxId);
});
Expand Down
64 changes: 62 additions & 2 deletions clients/js/test/mintToCollectionV1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from '../src';
import { createTree, createUmi } from './_setup';

test('it can mint an NFT from a collection', async (t) => {
test('it can mint an NFT from a collection (collection unverified in passed-in metadata)', async (t) => {
// Given an empty Bubblegum tree.
const umi = await createUmi();
const merkleTree = await createTree(umi);
Expand All @@ -45,7 +45,7 @@ test('it can mint an NFT from a collection', async (t) => {
isCollection: true,
}).sendAndConfirm(umi);

// When we mint a new NFT from the tree using the following metadata.
// When we mint a new NFT from the tree using the following metadata, with collection unverified.
const metadata: MetadataArgsArgs = {
name: 'My NFT',
uri: 'https://example.com/my-nft.json',
Expand Down Expand Up @@ -83,6 +83,66 @@ test('it can mint an NFT from a collection', async (t) => {
t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(leaf));
});

test('it can mint an NFT from a collection (collection verified in passed-in metadata)', async (t) => {
// Given an empty Bubblegum tree.
const umi = await createUmi();
const merkleTree = await createTree(umi);
const leafOwner = generateSigner(umi).publicKey;
let merkleTreeAccount = await fetchMerkleTree(umi, merkleTree);
t.is(merkleTreeAccount.tree.sequenceNumber, 0n);
t.is(merkleTreeAccount.tree.activeIndex, 0n);
t.is(merkleTreeAccount.tree.bufferSize, 1n);
t.is(merkleTreeAccount.tree.rightMostPath.index, 0);
t.is(merkleTreeAccount.tree.rightMostPath.leaf, defaultPublicKey());

// And a Collection NFT.
const collectionMint = generateSigner(umi);
await createNft(umi, {
mint: collectionMint,
name: 'My Collection',
uri: 'https://example.com/my-collection.json',
sellerFeeBasisPoints: percentAmount(5.5), // 5.5%
isCollection: true,
}).sendAndConfirm(umi);

// When we mint a new NFT from the tree using the following metadata, with collection verified.
const metadata: MetadataArgsArgs = {
name: 'My NFT',
uri: 'https://example.com/my-nft.json',
sellerFeeBasisPoints: 550, // 5.5%
collection: {
key: collectionMint.publicKey,
verified: true,
},
creators: [],
};
await mintToCollectionV1(umi, {
leafOwner,
merkleTree,
metadata,
collectionMint: collectionMint.publicKey,
}).sendAndConfirm(umi);

// Then a new leaf was added to the merkle tree.
merkleTreeAccount = await fetchMerkleTree(umi, merkleTree);
t.is(merkleTreeAccount.tree.sequenceNumber, 1n);
t.is(merkleTreeAccount.tree.activeIndex, 1n);
t.is(merkleTreeAccount.tree.bufferSize, 2n);
t.is(merkleTreeAccount.tree.rightMostPath.index, 1);

// And the hash of the metadata matches the new leaf.
const leaf = hashLeaf(umi, {
merkleTree,
owner: leafOwner,
leafIndex: 0,
metadata: {
...metadata,
collection: some({ key: collectionMint.publicKey, verified: true }),
},
});
t.is(merkleTreeAccount.tree.rightMostPath.leaf, publicKey(leaf));
});

test('it can mint an NFT from a collection using a collection delegate', async (t) => {
// Given an empty Bubblegum tree.
const umi = await createUmi();
Expand Down
Loading

0 comments on commit fe5dae5

Please sign in to comment.