NOVO: Helius adquire a Light Protocol
como usar NFTs compactados na Solana
Blog/Desenvolvimento

NFTs na Solana: como modificar NFTs compactados (2023)

Líder de Relações com DesenvolvedoresHunter Davis no LinkedIn
6 min de leitura

NFTs na Solana: por que as modificações são necessárias

Para quem desenvolve na Solana, interagir com NFTs compactados pode parecer uma tarefa difícil. Mas isso não precisa mais ser assim, graças à possibilidade de modificar NFTs compactados por meio do programa Bubblegum.

Quando um NFT compactado é cunhado, provas de Merkle precisam ser fornecidas com a instrução da transação para fazer qualquer alteração nesse NFT, como transferi-lo ou queimá-lo. Para quem não conhece, uma prova de Merkle é um conjunto de hashes capaz de provar que uma folha pertence à árvore.

Neste guia, abordaremos:

  • A importância das modificações em NFTs compactados.
  • Como configurar modificações de queima e transferência de NFTs compactados.
  • A importância da API Digital Asset Standard (DAS).

NFTs na Solana: pré-requisitos

Antes de começar, verifique se você tem:

  • Conhecimentos básicos de JavaScript/TypeScript.
  • Alguma familiaridade com NFTs compactados.
  • Git instalado.
  • npm ou yarn.
  • Um clone do repositório de exemplo, disponível aqui.

NFTs na Solana: configuração do ambiente

  1. Clone nosso repositório de exemplo:
Código
git clone
  1. Acesse a pasta do projeto:
Código
cd compression-examples
  1. Instale o npm:
Código
npm install
  1. Crie um arquivo .env na raiz:
Código
API_KEY=YOUR_API_KEY
SECRET_KEY=YOUR_WALLET_SECRET_KEY

Nesse arquivo, insira sua chave da API Helius e a chave secreta da carteira pagadora. Verifique se essa carteira tem SOL para financiar as transações.

NFTs na Solana: modificações

As modificações exigem a obtenção das provas atuais para a instrução, além dos hashes atuais de propriedade, autoridade e compressão do ativo. Você precisa buscar esses dados na API Digital Asset Standard (DAS), que simplifica as interações com ativos compactados.

A lista atual de modificações disponíveis por meio do Bubblegum é:

  • Mint
  • Transfer
  • Burn
  • Delegate, CancelDelegate
  • Redeem, CancelRedeem
  • Decompress
  • VerifyCreator, SetAndVerifyCreator
  • VerifyCollection, SetAndVerifyCollection

Para interagir com ativos compactados, você precisa retornar:

  • getAssetProof - Busca a prova para processamento.
  • getAsset - Obtém o hash do criador, o hash dos dados, o nonce da folha e o proprietário/delegado atual da folha.
  • getBubblegumAuthorityPDA - Um endereço derivado do programa a partir do ID da árvore.

Nestes exemplos, veremos como queimar e transferir os NFTs compactados que cunharemos.

NFT: transferência

A modificação de transferência permite mover um NFT compactado da carteira de um proprietário para outra. Tanto as vendas quanto as listagens de NFTs compactados usam essa instrução para exibir vendas em marketplaces.

Para configurar isso em nosso repositório existente, acessaremos o arquivo utils.ts e analisaremos nossa função transferAsset.

Precisaremos fornecer os seguintes dados:

  • Conexão - Uma conexão RPC da Solana.
  • Proprietário atual - A chave pública do proprietário atual do NFT.
  • Novo proprietário do NFT - A chave pública do novo proprietário.
  • ID do ativo - Permite obter os detalhes atuais do ativo específico fornecido.

Na função transferAsset do exemplo abaixo, inicialmente solicitamos a prova do ativo e configuramos algo chamado proofPath. Essa solicitação é feita à API DAS, que retorna a prova do ativo que você deseja transferir. Então, você a processa para fornecê-la à instrução da transação.

Código
export const transferAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  newOwner: Keypair,
  assetId: string
) => {
  console.log(
    `Transfering asset ${assetId} from ${owner.publicKey.toBase58()} to ${newOwner.publicKey.toBase58()}.
    This will depend on indexer api calls to fetch the necessary data.`
  );
  let assetProof = await connectionWrapper.getAssetProof(assetId);
  if (!assetProof?.proof || assetProof.proof.length === 0) {
    throw new Error("Proof is empty");
  }
  let proofPath = assetProof.proof.map((node: string) => ({
    pubkey: new PublicKey(node),
    isSigner: false,
    isWritable: false,
  }));
  console.log("Successfully got proof path from RPC.");

// Additional code...

};

É necessário retornar a prova do ativo porque você precisa fornecer o estado atual do ativo compactado. Isso é obrigatório para cada transferência de um NFT compactado. Você processa essa prova no proofPath para fornecê-la à instrução da transação que veremos em breve.

Agora que temos o caminho da prova, podemos usar getAsset na API DAS para retornar o nonce da folha, o delegado da folha, o hash dos dados e o hash do criador.

Podemos configurar isso abaixo:

Código
export const transferAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  newOwner: Keypair,
  assetId: string
) => {

	// Previous code //

	// Calling getAsset from DAS
  const rpcAsset = await connectionWrapper.getAsset(assetId);
  console.log(
    "Successfully got asset from RPC. Current owner: " +
      rpcAsset.ownership.owner
  );
  if (rpcAsset.ownership.owner !== owner.publicKey.toBase58()) {
    throw new Error(
      `NFT is not owned by the expected owner. Expected ${owner.publicKey.toBase58()} but got ${
        rpcAsset.ownership.owner
      }.`
    );
  }
// Leaf nonce from getAsset call.
  const leafNonce = rpcAsset.compression.leaf_id;
// Locating Tree Authority.
  const treeAuthority = await getBubblegumAuthorityPDA(
    new PublicKey(assetProof.tree_id)
  );
// Leaf Owner/Delegate from getAsset call.
  const leafDelegate = rpcAsset.ownership.delegate
    ? new PublicKey(rpcAsset.ownership.delegate)
    : new PublicKey(rpcAsset.ownership.owner);
  );
};

Você pode ver que a solicitação getAsset está sendo feita para retornar o proprietário/delegado e o nonce da folha. Também definimos a autoridade da árvore usando apenas o ID da árvore, retornado pela solicitação assetProof.

Agora que essas informações estão definidas, podemos começar a criar a instrução da transação.

Código
export const transferAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  newOwner: Keypair,
  assetId: string
) => {

	// Previous code //

	// Transfer Instruction.
  let transferIx = createTransferInstruction(
    {
      treeAuthority, // Tree authority
      leafOwner: new PublicKey(rpcAsset.ownership.owner), // Current NFT owner
      leafDelegate: leafDelegate, // Leaf delegate/owner returned
      newLeafOwner: newOwner.publicKey, // New wallet to transfer our NFT.
      merkleTree: new PublicKey(assetProof.tree_id), // Merkle tree public key.
      logWrapper: SPL_NOOP_PROGRAM_ID, // NOOP program ID.
      compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, // Compression Program ID.
      anchorRemainingAccounts: proofPath, // Proofs computed to pass in
    },
    {
      root: bufferToArray(bs58.decode(assetProof.root)), // Root from getAssetProof returned
      dataHash: bufferToArray(
        bs58.decode(rpcAsset.compression.data_hash.trim()) // Data Hash from getAsset
      ),
      creatorHash: bufferToArray(
        bs58.decode(rpcAsset.compression.creator_hash.trim()) // Creator Hash from getAsset
      ),
      nonce: leafNonce, // Leaf nonce from getAsset
      index: leafNonce,
    }
  );
};

Para fazer isso com um ativo transferido, você precisará fornecer todas as variáveis retornadas por getAsset, getAssetProof e pelo SDK da Metaplex: delegado/proprietário da folha, hash dos dados, raiz e nonce da folha.

Você também verá que fornecemos alguns IDs de programa. Eles são as chaves públicas dos programas de compressão e noop importados no início do arquivo. Como alternativa, você pode fornecê-los como uma chave pública.

Agora que tudo está pronto, podemos configurar o envio da instrução da transação:

Código
export const transferAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  newOwner: Keypair,
  assetId: string
) => {

 // Previous code //

  const tx = new Transaction().add(transferIx);
  tx.feePayer = owner.publicKey;
  try {
    const sig = await sendAndConfirmTransaction(
      connectionWrapper,
      tx,
      [owner],
      {
        commitment: "confirmed",
        skipPreflight: true,
      }
    );
    return sig;
  } catch (e) {
    console.error("Failed to transfer compressed asset", e);
    throw e;
  }
};

No código acima, definimos nossa transação usando tx e adicionamos um tipo Transaction ao nosso transferIx de solana/web3.js .

Em seguida, enviamos a transação fornecendo:

  • Conexão com a Solana
  • Instruções da transação
  • Pagador
  • Compromisso

Agora podemos enviar essa transação executando:

Código
npm run e2e

Essa função cunhará um NFT compactado e sua coleção e, em seguida, fará a transferência para uma carteira especificada.

Veja a função transferAsset completa:

Código
export const transferAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  newOwner: Keypair,
  assetId: string
) => {
  console.log(
    `Transfering asset ${assetId} from ${owner.publicKey.toBase58()} to ${newOwner.publicKey.toBase58()}.
    This will depend on indexer api calls to fetch the necessary data.`
  );
  let assetProof = await connectionWrapper.getAssetProof(assetId);
  if (!assetProof?.proof || assetProof.proof.length === 0) {
    throw new Error("Proof is empty");
  }
  let proofPath = assetProof.proof.map((node: string) => ({
    pubkey: new PublicKey(node),
    isSigner: false,
    isWritable: false,
  }));
  console.log("Successfully got proof path from RPC.");

  const rpcAsset = await connectionWrapper.getAsset(assetId);
  console.log(
    "Successfully got asset from RPC. Current owner: " +
      rpcAsset.ownership.owner
  );
  if (rpcAsset.ownership.owner !== owner.publicKey.toBase58()) {
    throw new Error(
      `NFT is not owned by the expected owner. Expected ${owner.publicKey.toBase58()} but got ${
        rpcAsset.ownership.owner
      }.`
    );
  }
  const leafNonce = rpcAsset.compression.leaf_id;
  const treeAuthority = await getBubblegumAuthorityPDA(
    new PublicKey(assetProof.tree_id)
  );
  const leafDelegate = rpcAsset.ownership.delegate
    ? new PublicKey(rpcAsset.ownership.delegate)
    : new PublicKey(rpcAsset.ownership.owner);
  let transferIx = createTransferInstruction(
    {
      treeAuthority,
      leafOwner: new PublicKey(rpcAsset.ownership.owner),
      leafDelegate: leafDelegate,
      newLeafOwner: newOwner.publicKey,
      merkleTree: new PublicKey(assetProof.tree_id),
      logWrapper: SPL_NOOP_PROGRAM_ID,
      compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
      anchorRemainingAccounts: proofPath,
    },
    {
      root: bufferToArray(bs58.decode(assetProof.root)),
      dataHash: bufferToArray(
        bs58.decode(rpcAsset.compression.data_hash.trim())
      ),
      creatorHash: bufferToArray(
        bs58.decode(rpcAsset.compression.creator_hash.trim())
      ),
      nonce: leafNonce,
      index: leafNonce,
    }
  );
  const tx = new Transaction().add(transferIx);
  tx.feePayer = owner.publicKey;
  try {
    const sig = await sendAndConfirmTransaction(
      connectionWrapper,
      tx,
      [owner],
      {
        commitment: "confirmed",
        skipPreflight: true,
      }
    );
    return sig;
  } catch (e) {
    console.error("Failed to transfer compressed asset", e);
    throw e;
  }
};

NFT: queima

A queima de cNFTs remove o NFT da árvore de Merkle. Isso permite diversos casos de uso, como queimar ativos em troca de recompensas, remover ativos de spam ou atender a requisitos personalizados. Esse recurso foi usado na cunhagem da Tensor, em que 10 cNFTs foram queimados para permitir a cunhagem de 1 novo cNFT.

Para queimar um NFT compactado, precisaremos fornecer os seguintes dados:

  • Conexão - Uma conexão RPC da Solana.
  • Proprietário atual - A chave pública do proprietário atual do NFT.
  • ID do ativo - Permite obter os detalhes atuais do ativo específico fornecido.

Podemos ver isso no arquivo utils.ts, em burnAsset:

Código
export const burnAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  assetId?: string
) => {
  let assetProof = await connectionWrapper.getAssetProof(assetId);
  const rpcAsset = await connectionWrapper.getAsset(assetId);
  const leafNonce = rpcAsset.compression.leaf_id;
  let proofPath = assetProof.proof.map((node: string) => ({
    pubkey: new PublicKey(node),
    isSigner: false,
    isWritable: false,
}));
const treeAuthority = await getBubblegumAuthorityPDA(
    new PublicKey(assetProof.tree_id)
  );
  const leafDelegate = rpcAsset.ownership.delegate
    ? new PublicKey(rpcAsset.ownership.delegate)
    : new PublicKey(rpcAsset.ownership.owner);

// Remaining code //

}

O processo é igual ao de transferAsset, pois usa getAssetProof para retornar a raiz e o ID da árvore. Você também chama getAsset para retornar o hash do criador, o hash dos dados, o nonce da folha e o proprietário atual.

Assim, fica fácil saber quais valores precisam ser retornados aqui para modificar seu ativo compactado.

Agora, podemos configurar a instrução da transação abaixo desse código:

Código
export const burnAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  assetId?: string
) => {

	// Previous code //

// Burn transaction instruction //
  const burnIx = createBurnInstruction(
    {
      treeAuthority,
      leafOwner: new PublicKey(rpcAsset.ownership.owner),
      leafDelegate,
      merkleTree: new PublicKey(assetProof.tree_id),
      logWrapper: SPL_NOOP_PROGRAM_ID,
      compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
      anchorRemainingAccounts: proofPath,

    },
    {
      root: bufferToArray(bs58.decode(assetProof.root)),
      dataHash: bufferToArray(
        bs58.decode(rpcAsset.compression.data_hash.trim())
      ),
      creatorHash: bufferToArray(
        bs58.decode(rpcAsset.compression.creator_hash.trim())
      ),
      nonce: leafNonce,
      index: leafNonce,
    }
  );
 // Later code... //
};

Agora que configuramos a queima, podemos enviar nossa transação de maneira muito semelhante à instrução de transferência:

Código
export const burnAsset = async (
  connectionWrapper: WrappedConnection,
  owner: Keypair,
  assetId?: string
) => {

// Previous code //

const tx = new Transaction().add(burnIx);
  tx.feePayer = owner.publicKey;
  try {
    const sig = await sendAndConfirmTransaction(
      connectionWrapper,
      tx,
      [owner],
      {
        commitment: "confirmed",
        skipPreflight: true,
      }
    );
    return sig;
  } catch (e) {
    console.error("Failed to burn compressed asset", e);
    throw e;
  }
}

Em seguida, enviamos a transação fornecendo:

  • Conexão com a Solana
  • Instruções da transação
  • Pagador
  • Compromisso

Quando essa função for executada, ela queimará o ativo correspondente ao ID fornecido. Isso removerá os dados do ativo da árvore e aplicará a flag burnt=true ao ativo compactado. Por padrão, esses ativos queimados serão retornados quando você usar a API DAS.

Agora, no terminal, você pode executar:

Código
npm run burn -- --assetId=

NFTs na Solana: conclusão

Os NFTs compactados oferecem uma forma dinâmica de interagir com o ecossistema de NFTs na Solana. Com provas de Merkle obtidas por meio da API DAS e instruções de transação precisas, os desenvolvedores podem modificar e interagir com esses ativos digitais exclusivos. Ao entender as nuances das modificações de transferência e queima, os desenvolvedores podem criar aplicações mais confiáveis.

Assine a Helius

Acompanhe as novidades mais recentes do desenvolvimento Solana e receba atualizações quando publicarmos