Skip to content

Commit

Permalink
Bubblegum add event log for burn, redeem, and decompress (#1115)
Browse files Browse the repository at this point in the history
* Bubblegum add event log for burn, redeem, and decompress

* Add JS tests for transfer, burn, redeem, and decompress

Also fix yarn API gen so Solita can build Anchor 0.26.0
the way cargo install does.
  • Loading branch information
danenbm authored Jun 14, 2023
1 parent e196d7b commit 7551a28
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 4 deletions.
1 change: 1 addition & 0 deletions bubblegum/js/.solitarc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ module.exports = {
sdkDir,
binaryInstallDir,
programDir,
rustbin: { locked: true }
};
2 changes: 1 addition & 1 deletion bubblegum/js/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 --bpf-program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY ../program/target/deploy/mpl_bubblegum.so",
"start-validator": "solana-test-validator -ud --quiet --reset -c cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK -c 4VTQredsAmr1yzRJugLV6Mt6eu6XMeCwdkZ73wwVMWHv -c noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV -c 3RHkdjCwWyK2firrwFQGvXCxbUpBky1GTmb9EDK9hUnX -c metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s -c PwDiXFxQsGra4sFFTT8r1QWRMd4vfumiWC1jfWNfdYT -c TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA -c ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL --bpf-program BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY ../program/target/deploy/mpl_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
2 changes: 1 addition & 1 deletion bubblegum/js/src/mpl-bubblegum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function computeCreatorHash(creators: Creator[]) {
Buffer.from([creator.verified ? 1 : 0]),
Buffer.from([creator.share]),
]);
})
}),
);
return Buffer.from(keccak_256.digest(bufferOfCreatorData));
}
Expand Down
175 changes: 173 additions & 2 deletions bubblegum/js/tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
sendAndConfirmTransaction,
SystemProgram,
Transaction,
SYSVAR_RENT_PUBKEY,
} from '@solana/web3.js';

import {
Expand All @@ -20,14 +21,23 @@ import {
import {
createCreateTreeInstruction,
createMintV1Instruction,
createTransferInstruction,
createBurnInstruction,
createRedeemInstruction,
createDecompressV1Instruction,
MetadataArgs,
PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
TokenProgramVersion,
TokenStandard,
Creator,
} from '../src/generated';
import { getLeafAssetId, computeCompressedNFTHash } from '../src/mpl-bubblegum';
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";

function keypairFromSeed(seed: string) {
const expandedSeed = Uint8Array.from(
Expand Down Expand Up @@ -184,7 +194,7 @@ describe('Bubblegum tests', () => {
const accountInfo = await connection.getAccountInfo(merkleTree, { commitment: 'confirmed' });
const account = ConcurrentMerkleTreeAccount.fromBuffer(accountInfo!.data!);

// Verify leaf exists
// Verify leaf exists.
const leafIndex = new BN.BN(0);
const assetId = await getLeafAssetId(merkleTree, leafIndex);
const verifyLeafIx = createVerifyLeafIx(
Expand All @@ -204,6 +214,167 @@ describe('Bubblegum tests', () => {
console.log('Verified NFT existence:', txId);
});

it('Can transfer and burn a compressed NFT', async () => {
// Transfer.
const accountInfo = await connection.getAccountInfo(merkleTree, { commitment: 'confirmed' });
const account = ConcurrentMerkleTreeAccount.fromBuffer(accountInfo!.data!);
const [treeAuthority] = PublicKey.findProgramAddressSync(
[merkleTree.toBuffer()],
BUBBLEGUM_PROGRAM_ID,
);
const newLeafOwnerKeypair = new Keypair();
const newLeafOwner = newLeafOwnerKeypair.publicKey;

const transferIx = createTransferInstruction(
{
treeAuthority,
leafOwner: payer,
leafDelegate: payer,
newLeafOwner,
merkleTree,
logWrapper: SPL_NOOP_PROGRAM_ID,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
},
{
root: Array.from(account.getCurrentRoot()),
dataHash: Array.from(computeDataHash(originalCompressedNFT)),
creatorHash: Array.from(computeCreatorHash(originalCompressedNFT.creators)),
nonce: 0,
index: 0
},
);

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

console.log('NFT transfer tx:', transferTxId);

// Burn.
const burnIx = createBurnInstruction(
{
treeAuthority,
leafOwner: newLeafOwner,
leafDelegate: newLeafOwner,
merkleTree,
logWrapper: SPL_NOOP_PROGRAM_ID,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
},
{
root: Array.from(account.getCurrentRoot()),
dataHash: Array.from(computeDataHash(originalCompressedNFT)),
creatorHash: Array.from(computeCreatorHash(originalCompressedNFT.creators)),
nonce: 0,
index: 0
},
);

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

console.log('NFT burn tx:', burnTxId);
});

it('Can redeem and decompress compressed NFT', async () => {
// Redeem.
const accountInfo = await connection.getAccountInfo(merkleTree, { commitment: 'confirmed' });
const account = ConcurrentMerkleTreeAccount.fromBuffer(accountInfo!.data!);
const [treeAuthority] = PublicKey.findProgramAddressSync(
[merkleTree.toBuffer()],
BUBBLEGUM_PROGRAM_ID,
);
const nonce = new BN.BN(0);
const [voucher] = PublicKey.findProgramAddressSync(
[Buffer.from('voucher', 'utf8'), merkleTree.toBuffer(), Uint8Array.from(nonce.toArray('le', 8))],
BUBBLEGUM_PROGRAM_ID,
);

const redeemIx = createRedeemInstruction(
{
treeAuthority,
leafOwner: payer,
leafDelegate: payer,
merkleTree,
voucher,
logWrapper: SPL_NOOP_PROGRAM_ID,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
},
{
root: Array.from(account.getCurrentRoot()),
dataHash: Array.from(computeDataHash(originalCompressedNFT)),
creatorHash: Array.from(computeCreatorHash(originalCompressedNFT.creators)),
nonce,
index: 0
},
);

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

console.log('NFT redeem tx:', redeemTxId);

// Decompress.
const [mint] = PublicKey.findProgramAddressSync(
[Buffer.from('asset', 'utf8'), merkleTree.toBuffer(), Uint8Array.from(nonce.toArray('le', 8))],
BUBBLEGUM_PROGRAM_ID,
);
const [tokenAccount] = PublicKey.findProgramAddressSync(
[payer.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()],
ASSOCIATED_TOKEN_PROGRAM_ID,
);
const [mintAuthority] = PublicKey.findProgramAddressSync(
[mint.toBuffer()],
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')],
TOKEN_METADATA_PROGRAM_ID,
);

const decompressIx = createDecompressV1Instruction(
{
voucher,
leafOwner: payer,
tokenAccount,
mint,
mintAuthority,
metadata,
masterEdition,
sysvarRent: SYSVAR_RENT_PUBKEY,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
logWrapper: SPL_NOOP_PROGRAM_ID,
},
{
metadata: originalCompressedNFT
},
);

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

console.log('NFT decompress tx:', decompressTxId);
});

// TODO(@metaplex): add collection tests here
});
});
16 changes: 16 additions & 0 deletions bubblegum/program/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1322,6 +1322,11 @@ pub mod bubblegum {
creator_hash,
);

wrap_application_data_v1(
previous_leaf.to_event().try_to_vec()?,
&ctx.accounts.log_wrapper,
)?;

let new_leaf = Node::default();

replace_leaf(
Expand Down Expand Up @@ -1354,6 +1359,11 @@ pub mod bubblegum {
let previous_leaf =
LeafSchema::new_v0(asset_id, owner, delegate, nonce, data_hash, creator_hash);

wrap_application_data_v1(
previous_leaf.to_event().try_to_vec()?,
&ctx.accounts.log_wrapper,
)?;

let new_leaf = Node::default();

replace_leaf(
Expand Down Expand Up @@ -1427,6 +1437,12 @@ pub mod bubblegum {
}

let voucher = &ctx.accounts.voucher;

wrap_application_data_v1(
voucher.leaf_schema.to_event().try_to_vec()?,
&ctx.accounts.log_wrapper,
)?;

match metadata.token_program_version {
TokenProgramVersion::Original => {
if ctx.accounts.mint.data_is_empty() {
Expand Down

0 comments on commit 7551a28

Please sign in to comment.