NEU: Helius übernimmt Light Protocol
So verwendest du komprimierte NFTs auf Solana
Blog/Entwicklung

Solana NFT: Komprimierte NFTs ändern (2023)

Leiter Developer RelationsHunter Davis auf LinkedIn
6 Min. Lesezeit

Solana NFT: Warum Änderungen nötig sind

Als Entwickler auf Solana kann die Interaktion mit komprimierten NFTs zunächst schwierig wirken. Das muss nicht mehr so sein: Über das Bubblegum-Programm kannst du komprimierte NFTs ändern.

Wenn ein komprimiertes NFT geprägt wird, musst du der Transaktionsanweisung Merkle-Proofs beifügen, um das NFT zu ändern, etwa um es zu übertragen oder zu verbrennen. Falls du damit noch nicht vertraut bist: Ein Merkle-Proof ist eine Reihe von Hashes, mit denen sich nachweisen lässt, dass ein Blatt zum Baum gehört.

In diesem Leitfaden behandeln wir:

  • Warum Änderungen an komprimierten NFTs wichtig sind.
  • Wie du das Verbrennen und Übertragen komprimierter NFTs einrichtest.
  • Warum die Digital Asset Standard (DAS) API wichtig ist.

Solana NFT: Voraussetzungen

Bevor du loslegst, benötigst du:

  • Grundkenntnisse in JavaScript/TypeScript.
  • Etwas Erfahrung mit komprimierten NFTs.
  • Eine installierte Git-Version.
  • npm oder yarn.
  • Einen Klon des Beispiel-Repositorys, das du hier findest.

Solana NFT: Umgebung einrichten

  1. Klone unser Beispiel-Repository:
Code
git clone
  1. Wechsle in den Projektordner:
Code
cd compression-examples
  1. Installiere npm:
Code
npm install
  1. Erstelle im Stammverzeichnis eine .env-Datei:
Code
API_KEY=YOUR_API_KEY
SECRET_KEY=YOUR_WALLET_SECRET_KEY

Trage dort deinen Helius API-Schlüssel und den geheimen Wallet-Schlüssel des Zahlers ein. Stelle sicher, dass dieses Wallet genug SOL enthält, um die Transaktionen zu finanzieren.

Solana NFT: Änderungen

Für Änderungen benötigst du die aktuellen Proofs für die Anweisung sowie die aktuellen Eigentümer-, Autoritäts- und Komprimierungs-Hashes des Assets. Rufe diese Daten über die Digital Asset Standard (DAS) API ab. Sie vereinfacht die Interaktion mit komprimierten Assets.

Bubblegum unterstützt derzeit folgende Änderungen:

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

Für die Interaktion mit komprimierten Assets musst du Folgendes abrufen:

  • getAssetProof – Ruft den Proof für die Berechnung ab.
  • getAsset – Ruft den Creator-Hash, den Daten-Hash, die Blatt-Nonce und den aktuellen Eigentümer bzw. Blatt-Delegierten ab.
  • getBubblegumAuthorityPDA – Eine aus der Baum-ID abgeleitete Programmadresse.

In diesen Beispielen zeigen wir, wie du die komprimierten NFTs, die wir prägen, verbrennst und überträgst.

NFT: Übertragen

Mit der Transfer-Änderung kannst du ein komprimiertes NFT vom Wallet eines Eigentümers in ein anderes verschieben. Sowohl Verkäufe als auch Listings komprimierter NFTs verwenden diese Anweisung, um Verkäufe auf Marktplätzen anzuzeigen.

Um dies in unserem bestehenden Repository einzurichten, öffnen wir die Datei utils.ts und sehen uns unsere Funktion transferAsset an.

Wir müssen Folgendes übergeben:

  • Verbindung – Eine Solana RPC-Verbindung.
  • Aktueller Eigentümer – Der öffentliche Schlüssel des aktuellen NFT-Eigentümers.
  • Neuer Eigentümer des NFTs – Der öffentliche Schlüssel des neuen Eigentümers.
  • Asset-ID – Damit können wir die aktuellen Details des angegebenen Assets abrufen.

In unserer Beispielfunktion transferAsset unten siehst du, dass wir zuerst den Asset-Proof anfordern und ein sogenanntes proofPath einrichten. Diese Anfrage an die DAS API gibt den Proof für das Asset zurück, das du übertragen möchtest. Du bereitest ihn so auf, dass du ihn an die Transaktionsanweisung übergeben kannst.

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

};

Du musst den Asset-Proof abrufen, weil du den aktuellen Zustand des komprimierten Assets zurückgeben musst. Das ist bei jeder Übertragung eines komprimierten NFTs erforderlich. Du bereitest ihn im proofPath auf, damit du ihn an unsere Transaktionsanweisung übergeben kannst, die wir gleich betrachten.

Da wir nun unseren Proof-Pfad haben, können wir getAsset in der DAS API verwenden, um unsere Blatt-Nonce, den Blatt-Delegierten, den Daten-Hash und den Creator-Hash abzurufen.

So richten wir das ein:

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

Du siehst, dass die getAsset-Anfrage den Eigentümer bzw. Delegierten und die Blatt-Nonce abruft. Außerdem definieren wir unsere Baumautorität allein anhand der Baum-ID, die unsere Anfrage assetProof zurückgegeben hat.

Da diese Werte nun definiert sind, können wir unsere Transaktionsanweisung erstellen.

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

Für ein übertragenes Asset musst du alle Variablen übergeben, die wir von getAsset, getAssetProof und dem Metaplex SDK erhalten haben: Blatt-Delegierter bzw. -Eigentümer, Daten-Hash, Root und Blatt-Nonce.

Du siehst außerdem, dass wir mehrere Programm-IDs übergeben. Das sind die öffentlichen Schlüssel der Komprimierungs- und noop-Programme, die oben in der Datei importiert werden. Alternativ kannst du sie als öffentlichen Schlüssel übergeben.

Nun können wir den Versand unserer Transaktionsanweisung einrichten:

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

Im obigen Code definieren wir unsere Transaktion mit tx und fügen für unser transferIx aus solana/web3.js einen Transaction-Typ hinzu.

Anschließend senden wir sie ab und übergeben dabei:

  • Solana-Verbindung
  • Transaktionsanweisungen
  • Zahler
  • Commitment

Wir können diese Transaktion nun mit folgendem Befehl absenden:

Code
npm run e2e

Diese Funktion prägt ein komprimiertes NFT und dessen Collection und überträgt es anschließend an ein angegebenes Wallet.

Hier ist die vollständige Funktion 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: Verbrennen

Das Verbrennen von cNFTs entfernt das NFT aus dem Merkle-Baum. Das ermöglicht verschiedene Anwendungsfälle: Du kannst NFTs für Belohnungen verbrennen, Spam-Assets entfernen oder individuelle Anforderungen umsetzen. Beim Tensor-Mint wurde dies genutzt, um zehn cNFTs zu verbrennen und dafür ein neues cNFT zu prägen.

Um ein komprimiertes NFT zu verbrennen, müssen wir Folgendes übergeben:

  • Verbindung – Eine Solana RPC-Verbindung.
  • Aktueller Eigentümer – Der öffentliche Schlüssel des aktuellen NFT-Eigentümers.
  • Asset-ID – Damit können wir die aktuellen Details des angegebenen Assets abrufen.

Du findest dies in unserer utils.ts unter 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 //

}

Das funktioniert genauso wie transferAsset, da getAssetProof die Root und Baum-ID zurückgibt. Außerdem rufst du getAsset auf, um den Creator-Hash, den Daten-Hash, die Blatt-Nonce und den aktuellen Eigentümer abzurufen.

So erkennst du leicht, welche Werte du hier abrufen musst, um dein komprimiertes Asset zu ändern.

Unter diesem Code können wir nun unsere Transaktionsanweisung einrichten:

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

Da die Burn-Änderung nun eingerichtet ist, können wir unsere Transaktion ähnlich wie bei der Transfer-Anweisung absenden:

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

Anschließend senden wir sie ab und übergeben dabei:

  • Solana-Verbindung
  • Transaktionsanweisungen
  • Zahler
  • Commitment

Sobald du diese Funktion ausführst, verbrennt sie das Asset mit der angegebenen ID. Dadurch werden die Asset-Daten aus dem Baum entfernt und beim komprimierten Asset das Flag burnt=true gesetzt. Standardmäßig werden diese verbrannten Assets weiterhin zurückgegeben, wenn du die DAS API verwendest.

In deinem Terminal kannst du nun Folgendes ausführen:

Code
npm run burn -- --assetId=

Solana NFTs: Fazit

Komprimierte NFTs bieten eine flexible Möglichkeit, mit dem NFT-Ökosystem auf Solana zu interagieren. Mithilfe von Merkle-Proofs aus der DAS API und präzisen Transaktionsanweisungen können Entwickler diese einzigartigen digitalen Assets ändern und mit ihnen interagieren. Wer die Feinheiten der Transfer- und Burn-Änderungen versteht, kann zuverlässigere Anwendungen entwickeln.

Helius abonnieren

Bleib bei der Solana-Entwicklung auf dem Laufenden und erhalte Updates, wenn wir neue Beiträge veröffentlichen