Is there a way to get a compressed NFT's asset ID right after minting it, without using an RPC request?
3 Answers
In order to compute the assetId of a compressed nft, you need a few pieces of information:
- the merkle tree's address, and
- the tree leaf the represents the compressed nft
The way to do this is from extracting some data from the transaction that minted the compressed nft and processing the "changelog event" that was emitted via a CPI to the noop program.
During the compressed nft mint process, the Bubblegum program performs a CPI call to the SPL compression program to emit a "changelog" event on chain (see here for the Bubblegum code). This changelog event will contain the index of the leaf that represents the compressed nft.
The easiest way to read this cpi call and parse the emitted changelog event is from the TransactionResponse inner instructions, looking for the specific calls to the noop program. After you deserialize the CPI instruction's data as a Changelog event, you will have the leaf index.
Here is an example of how to find the changelog events from a transaction using TypeScript:
/**
* Helper function to extract the all ChangeLogEventV1 emitted in a transaction
* @param txResponse - Transaction response from `@solana/web3.js`
* @param noopProgramId - program id of the noop program used (default: `SPL_NOOP_PROGRAM_ID`)
* @returns
*/
export function getAllChangeLogEventV1FromTransaction(
txResponse: TransactionResponse | VersionedTransactionResponse,
noopProgramId: PublicKey = SPL_NOOP_PROGRAM_ID
): ChangeLogEventV1[] {
// ensure a transaction response was provided
if (!txResponse) throw Error("No txResponse provided");
// flatten the array of all account keys (e.g. static, readonly, writable)
const accountKeys = txResponse.transaction.message
.getAccountKeys()
.keySegments()
.flat();
let changeLogEvents: ChangeLogEventV1[] = [];
// locate and parse noop instruction calls via cpi (aka inner instructions)
txResponse!.meta?.innerInstructions?.forEach((compiledIx) => {
compiledIx.instructions.forEach((innerIx) => {
// only attempt to parse noop instructions
if (
noopProgramId.toBase58() !==
accountKeys[innerIx.programIdIndex].toBase58()
)
return;
try {
// try to deserialize the cpi data as a changelog event
changeLogEvents.push(
deserializeChangeLogEventV1(Buffer.from(bs58.decode(innerIx.data)))
);
} catch (__) {
// this noop cpi is not a changelog event. do nothing with it.
}
});
});
return changeLogEvents;
}
Note: This code is taken from this PR I wrote to add this function directly into the SPL compression sdk. So soon, it should be integrated into the sdk itself, if not already.
You can use the above function to get all the changelog events emitted inside a transaction:
const devnetSig = "3V1pMta1aKzJdnYwttHR5qtrPVQfazwdUykvgM1YY5Xwb2x6hwttLj9R1hJi4ZjMA9KaXWGx9LxiNBAAoAHKsKdn";
const tx = await connection.getTransaction(devnetSig, {
maxSupportedTransactionVersion: 0,
});
if (!tx) throw Error("Tx not found");
const events = getAllChangeLogEventV1FromTransaction(tx);
console.log(events);
For most transactions that simply mint a compressed nft, the above events
array should only have 1 event. But for more complex actions, there could be multiple events. So you will have to filter this events
array or maybe write some custom code/checks.
Assuming there is only 1 events, you can get the leaf index and tree of the minted compressed nft, and then compute the assetId:
// the index of the leaf that represents the compressed nft
const leafIndex = events[0].index;
// the tree address (if you did not already have it)
const leafIndex = events[0].treeId;
// compute the assetId of the compressed nft just minted
const assetId = await getLeafAssetId(events[0].treeId, new BN(events[0].index));
Note: this getLeafAssetId
function is from the @metaplex-foundation/mpl-bubblegum
package. It derives a PDA using some static seeds, the tree address, and the leaf index. (see the source code here)
And there you go: you now have the assetId of the compressed nft minted in a transaction!
It looks like the way Metaplex does it is by fetching the transaction info from the transaction signature and then parsing out the asset index and deriving the assetId.
const txInfo = await metaplex.connection.getTransaction(
output.response.signature,
{
maxSupportedTransactionVersion: 0,
}
);
scope.throwIfCanceled();
// find the index of the bubblegum instruction
const relevantIndex =
txInfo!.transaction.message.compiledInstructions.findIndex(
(instruction) => {
return (
txInfo?.transaction.message.staticAccountKeys[
instruction.programIdIndex
].toBase58() === 'BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY'
);
}
);
// locate the no-op inner instructions called via cpi from bubblegum
const relevantInnerIxs = txInfo!.meta?.innerInstructions?.[
relevantIndex
].instructions.filter((instruction) => {
return (
txInfo?.transaction.message.staticAccountKeys[
instruction.programIdIndex
].toBase58() === 'noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV'
);
});
// when no valid noop instructions are found, throw an error
if (!relevantInnerIxs || relevantInnerIxs.length == 0)
throw Error('Unable to locate valid noop instructions');
// locate the asset index by attempting to locate and parse the correct `relevantInnerIx`
let assetIndex: number | undefined = undefined;
// note: the `assetIndex` is expected to be at position `1`, and normally expect only 2 `relevantInnerIx`
for (let i = relevantInnerIxs.length - 1; i > 0; i--) {
try {
const changeLogEvent = deserializeChangeLogEventV1(
Buffer.from(base58.decode(relevantInnerIxs[i]?.data!))
);
// extract a successful changelog index
assetIndex = changeLogEvent?.index;
} catch (__) {
// do nothing, invalid data is handled just after the for loop
}
}
// when no `assetIndex` was found, throw an error
if (typeof assetIndex == 'undefined')
throw Error('Unable to locate the newly minted assetId ');
const assetId = await getLeafAssetId(
operation.input.tree,
new BN(assetIndex)
);
@nickfrosty is right, In order to compute the assetId of a compressed nft, you need to know:
- the merkle tree's address (
treeId
) - the tree leaf the represents the compressed nft (
leafIndex
)
But, not always noop instruction exists on transaction for getting leafIndex
, that's why as an alternative, you could use leaf replace instruction:
import {
replaceLeafStruct,
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
} from '@solana/spl-account-compression';
import { ParsedInnerInstruction, PublicKey } from '@solana/web3.js';
import { decode } from 'bs58';
import { getLeafAssetId } from '@metaplex-foundation/mpl-bubblegum';
import BN from 'bn.js';
const getAssetIdFromReplaceLeafInstruction = async (
innerInstructions: ParsedInnerInstruction[],
): Promise<PublicKey | undefined> => {
for (let i = innerInstructions.length - 1; i >= 0; i--) {
for (let j = innerInstructions[i].instructions.length - 1; j >= 0; j--) {
const instruction = innerInstructions[i].instructions[j];
const programId = instruction.programId.toBase58();
if (programId === SPL_ACCOUNT_COMPRESSION_PROGRAM_ID.toBase58()) {
if ('accounts' in instruction) {
const treeId = new PublicKey(instruction.accounts[0]);
const replaceLeafEvent = replaceLeafStruct.deserialize(
Buffer.from(decode(instruction.data)),
);
const assetIndex = replaceLeafEvent[0].index;
const assetId = await getLeafAssetId(treeId, new BN(assetIndex));
return assetId;
}
}
}
}
};
-
a noop instruction should exist on every transaction that alters a merkle tree via bubble gum and/or the compression program. bubblegum cpi's to the compression program to alter the tree, like minting a new cNFT. the compression program will then emit and event and log via a cpi to the noop program Commented Jan 2 at 14:15
-
@nickfrosty I really faced to a transaction without noop instruction.– JahongirCommented Jan 6 at 20:37
-