4

Is there a way to get a compressed NFT's asset ID right after minting it, without using an RPC request?

3 Answers 3

4

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!

1

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.

https://github.com/metaplex-foundation/js/blob/main/packages/js/src/plugins/nftModule/operations/createCompressedNft.ts#L239-L297

      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)
      );
0

@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;
        }
      }
    }
  }
};
3
  • 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
    – nickfrosty
    Commented Jan 2 at 14:15
  • @nickfrosty I really faced to a transaction without noop instruction.
    – Jahongir
    Commented Jan 6 at 20:37
  • can you provide a link to the transaction?
    – nickfrosty
    Commented Jan 12 at 16:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.