NOUVEAU : Helius acquiert Light Protocol
comment utiliser des NFT compressés sur Solana
Blog/Développement

NFT Solana : modifier des NFT compressés (2023)

Responsable des relations développeursHunter Davis sur LinkedIn
6 min de lecture

NFT Solana : pourquoi des modifications sont nécessaires

En tant que développeur sur Solana, interagir avec des NFT compressés peut sembler particulièrement complexe. Ce n’est désormais plus le cas grâce à la possibilité de modifier les NFT compressés via le programme Bubblegum.

Lorsqu’un NFT compressé est créé, des preuves de Merkle doivent être fournies avec l’instruction de transaction pour apporter la moindre modification à ce NFT (par exemple, le transférer ou le brûler). Pour rappel, une preuve de Merkle est un ensemble de hachages permettant de prouver qu’une feuille appartient à l’arbre.

Dans ce guide, nous aborderons :

  • L’importance des modifications pour les NFT compressés.
  • La configuration des modifications de brûlage et de transfert des NFT compressés.
  • L’importance de l’API Digital Asset Standard (DAS).

NFT Solana : prérequis

Avant de commencer, assurez-vous de disposer des éléments suivants :

  • Une compréhension élémentaire de JavaScript/TypeScript.
  • Une certaine connaissance des NFT compressés.
  • Git installé.
  • npm ou yarn.
  • Un clone du dépôt d’exemple, disponible ici.

NFT Solana : configuration de l’environnement

  1. Clonez notre dépôt d’exemple :
Code
git clone
  1. Accédez au dossier du projet :
Code
cd compression-examples
  1. Installez npm :
Code
npm install
  1. Créez un fichier .env à la racine :
Code
API_KEY=YOUR_API_KEY
SECRET_KEY=YOUR_WALLET_SECRET_KEY

Dans ce fichier, saisissez votre clé API Helius et la clé secrète du wallet du payeur. Assurez-vous que ce wallet contient des SOL pour financer les transactions.

NFT Solana : modifications

Pour effectuer des modifications, vous devez obtenir les preuves actuelles nécessaires à l’instruction, ainsi que le propriétaire, l’autorité et les hachages de compression actuels de l’actif. Vous devez récupérer ces données depuis l’API Digital Asset Standard (DAS), qui simplifie les interactions avec les actifs compressés.

Voici la liste actuelle des modifications disponibles via Bubblegum :

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

Pour interagir avec des actifs compressés, vous devez vous assurer de renvoyer les éléments suivants :

  • getAssetProof - Récupère la preuve nécessaire au calcul.
  • getAsset - Récupère le hachage du créateur, le hachage des données, le nonce de la feuille et le propriétaire ou délégué actuel de la feuille.
  • getBubblegumAuthorityPDA - Une adresse dérivée du programme à partir de l’ID de l’arbre.

Dans ces exemples, nous verrons comment brûler et transférer les NFT compressés que nous créerons.

NFT : transfert

La modification de transfert vous permet de déplacer un NFT compressé du wallet d’un propriétaire vers celui d’un autre. Les ventes et les mises en vente de NFT compressés utilisent toutes deux cette instruction pour afficher les ventes sur les marketplaces.

Pour configurer cela dans notre dépôt existant, accédez à notre fichier utils.ts et examinez notre fonction transferAsset.

Nous devrons transmettre les éléments suivants :

  • Connexion - Une connexion RPC Solana.
  • Propriétaire actuel - La clé publique du propriétaire actuel du NFT.
  • Nouveau propriétaire du NFT - La clé publique du nouveau propriétaire.
  • ID de l’actif - Nous permet d’obtenir les informations actuelles sur l’actif spécifique transmis.

Dans notre exemple de fonction transferAsset ci-dessous, nous demandons d’abord la preuve de l’actif et configurons un élément appelé proofPath. Cette requête envoyée à l’API DAS renvoie la preuve de l’actif que vous souhaitez transférer. Vous la calculez afin de la transmettre à l’instruction de transaction.

Code
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...

};

Vous devez renvoyer la preuve de l’actif afin d’obtenir l’état actuel de l’actif compressé. Cette opération est obligatoire pour chaque transfert d’un NFT compressé. Vous la calculez dans proofPath afin de la transmettre à notre instruction de transaction, que nous verrons sous peu.

Maintenant que nous disposons du chemin de preuve, nous pouvons utiliser getAsset sur l’API DAS pour renvoyer le nonce de la feuille, son délégué, le hachage des données et le hachage du créateur.

Nous pouvons le configurer ci-dessous :

Code
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);
  );
};

Vous pouvez constater que la requête getAsset est effectuée pour renvoyer le propriétaire ou délégué et le nonce de la feuille. Nous définissons également l’autorité de notre arbre en utilisant simplement l’ID de l’arbre, renvoyé par notre requête assetProof.

Maintenant que ces éléments sont définis, nous pouvons commencer à créer notre instruction de transaction.

Code
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,
    }
  );
};

Pour effectuer cette opération sur un actif transféré, vous devez transmettre toutes les variables renvoyées par getAsset, getAssetProof et le SDK Metaplex : le délégué ou propriétaire de la feuille, le hachage des données, la racine et le nonce de la feuille.

Vous remarquerez également que nous transmettons plusieurs ID de programme. Il s’agit des clés publiques des programmes de compression et noop importés en haut du fichier. Vous pouvez également les transmettre sous forme de clé publique.

Maintenant que tout est en place, nous pouvons configurer l’envoi de notre instruction de transaction :

Code
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;
  }
};

Dans le code ci-dessus, nous définissons notre transaction à l’aide de tx et ajoutons un type Transaction pour notre transferIx provenant de solana/web3.js .

Nous l’envoyons ensuite en transmettant les éléments suivants :

  • Connexion Solana
  • Instructions de transaction
  • Payeur
  • Engagement

Nous pouvons maintenant envoyer cette transaction en exécutant :

Code
npm run e2e

Cette fonction crée un NFT compressé et sa collection, puis le transfère vers un wallet spécifié.

Voici l’intégralité de la fonction transferAsset :

Code
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 : brûlage

Le brûlage de cNFT supprime définitivement le NFT de l’arbre de Merkle. Il permet de nombreux cas d’usage, comme le brûlage en échange de récompenses, la suppression d’actifs indésirables ou la prise en charge d’exigences personnalisées. Cette méthode a été utilisée lors du mint Tensor, où 10 cNFT ont été brûlés afin de créer un nouveau cNFT.

Pour brûler un NFT compressé, nous devrons transmettre les éléments suivants :

  • Connexion - Une connexion RPC Solana.
  • Propriétaire actuel - La clé publique du propriétaire actuel du NFT.
  • ID de l’actif - Nous permet d’obtenir les informations actuelles sur l’actif spécifique transmis.

Vous pouvez voir cela dans notre fichier utils.ts, sous burnAsset :

Code
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 //

}

Le fonctionnement est identique à transferAsset, puisque getAssetProof est utilisé pour renvoyer la racine et l’ID de l’arbre. Vous appelez également getAsset pour renvoyer le hachage du créateur, le hachage des données, le nonce de la feuille et le propriétaire actuel.

Vous pouvez ainsi facilement identifier les valeurs à renvoyer ici pour modifier votre actif compressé.

Nous pouvons maintenant configurer notre instruction de transaction sous ce code :

Code
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... //
};

Maintenant que la configuration du brûlage est terminée, nous pouvons envoyer notre transaction de manière très similaire à l’instruction de transfert :

Code
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;
  }
}

Nous l’envoyons ensuite en transmettant les éléments suivants :

  • Connexion Solana
  • Instructions de transaction
  • Payeur
  • Engagement

Une fois cette fonction exécutée, elle brûle l’actif correspondant à l’ID que vous avez fourni. Les données de l’actif sont alors supprimées de l’arbre et un indicateur burnt=true est appliqué à l’actif compressé. Par défaut, ces actifs brûlés sont renvoyés lorsque vous utilisez l’API DAS.

Dans votre terminal, vous pouvez maintenant exécuter :

Code
npm run burn -- --assetId=

NFT Solana : conclusion

Les NFT compressés offrent un moyen dynamique d’interagir avec l’écosystème NFT sur Solana. Grâce aux preuves de Merkle obtenues via l’API DAS et à des instructions de transaction précises, les développeurs peuvent modifier ces actifs numériques uniques et interagir avec eux. En comprenant les subtilités des modifications de transfert et de brûlage, ils peuvent créer des applications plus fiables.

Abonnez-vous à Helius

Suivez les dernières actualités du développement sur Solana et recevez une notification à chaque publication