新着:HeliusがLight Protocolを買収
Solanaで圧縮NFTを使用する方法
ブログ/開発

Solana NFT:圧縮NFTを変更する方法(2023年)

Developer RelationsリードLinkedInのHunter Davis
読了時間:6分

Solana NFT:変更が必要な理由

Solana開発者にとって、圧縮NFTの操作は難しく感じられるかもしれません。しかし、Bubblegumプログラムを通じて圧縮NFTを変更できるようになったため、もはやそのような心配はありません。

圧縮NFTをミントした後、そのNFTに変更(転送やバーンなど)を加えるには、トランザクション命令とともにMerkle proofを提供する必要があります。Merkle proofとは、そのリーフがツリーに属していることを証明できるハッシュの集合です。

このガイドでは、以下について説明します。

  • 圧縮NFTにおける変更の重要性。
  • 圧縮NFTのバーンと転送を設定する方法。
  • Digital Asset Standard(DAS)APIの重要性。

Solana NFT:前提条件

始める前に、以下を用意してください。

  • JavaScript/TypeScriptの基本的な知識。
  • 圧縮NFTに関する基礎知識。
  • Gitがインストールされていること。
  • npmまたはyarn。
  • こちらから入手できるサンプルリポジトリのクローン。

Solana NFT:環境設定

  1. サンプルリポジトリをクローンします。
コード
git clone
  1. プロジェクトフォルダに移動します。
コード
cd compression-examples
  1. npmをインストールします。
コード
npm install
  1. ルートに**.env**ファイルを作成します。
コード
API_KEY=YOUR_API_KEY
SECRET_KEY=YOUR_WALLET_SECRET_KEY

このファイルにHelius APIキーと、支払者のウォレット秘密鍵を入力します。トランザクションの資金を支払えるよう、このウォレットにSolanaを入れておいてください。

Solana NFT:変更

変更には、命令に使用する最新のproofに加え、アセットの現在の所有者、権限、圧縮ハッシュを取得する必要があります。このデータは、圧縮アセットの操作を効率化するDigital Asset Standard(DAS)APIから取得します。

Bubblegumで現在利用できる変更操作は以下のとおりです。

  • ミント
  • 転送
  • バーン
  • 委任、委任の取り消し
  • 償還、償還の取り消し
  • 解凍
  • クリエイターの検証、クリエイターの設定と検証
  • コレクションの検証、コレクションの設定と検証

圧縮アセットを操作するには、以下の結果を取得する必要があります。

  • getAssetProof - 計算に使用するproofを取得します。
  • getAsset - クリエイターハッシュ、データハッシュ、リーフnonce、現在の所有者/リーフデリゲートを取得します。
  • getBubblegumAuthorityPDA - ツリーIDから導出されるプログラムアドレスです。

以下の例では、ミントする圧縮NFTのバーンと転送について説明します。

NFT:転送

転送操作を使うと、圧縮NFTを現在の所有者のウォレットから別のウォレットへ移動できます。圧縮NFTの販売と出品では、どちらもこの命令を使用してマーケットプレイスに販売情報を表示します。

既存のリポジトリで設定するために、utils.tsファイルを開き、transferAsset関数を確認します。

以下を渡す必要があります。

  • 接続 - Solana RPC接続。
  • 現在の所有者 - 現在のNFT所有者の公開鍵。
  • NFTの新しい所有者 - 新しい所有者の公開鍵。
  • アセットID - 渡した特定のアセットの最新情報を取得するために使用します。

以下のtransferAsset関数の例では、最初にアセットのproofをリクエストし、proofPathと呼ばれるものを設定しています。これはDAS APIへのリクエストで、転送するアセットのproofを返します。そのproofを計算し、トランザクション命令に渡します。

コード
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...

};

圧縮アセットの現在の状態を取得する必要があるため、アセットのproofを返す処理が必要です。これは、圧縮NFTを転送するたびに必須です。後ほど説明するトランザクション命令に渡せるよう、proofPathで計算します。

proof pathを取得したので、DAS APIのgetAssetを使用して、リーフnonce、リーフデリゲート、データハッシュ、クリエイターハッシュを取得できます。

以下のように設定できます。

コード
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);
  );
};

getAssetリクエストで、所有者/デリゲートとリーフnonceを取得していることが分かります。また、ツリーID(assetProofリクエストから取得したもの)を使用するだけで、ツリー権限も定義しています。

これらを定義できたので、トランザクション命令の構築を始めます。

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

転送するアセットに対してこれを実行するには、getAsset、getAssetProof、Metaplex SDKから取得したすべての変数(リーフデリゲート/所有者、データハッシュ、ルート、リーフnonce)を渡す必要があります。

複数のプログラムIDも渡していることが分かります。これらは、ファイルの先頭でインポートしている圧縮プログラムとnoopプログラムの公開鍵です。代わりに、これらを公開鍵として渡すこともできます。

準備が整ったので、トランザクション命令の送信を設定します。

コード
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;
  }
};

上記のコードでは、txを使用してトランザクションを定義し、solana/web3.jsのtransferIxにTransaction型を追加しています。

次に、以下を渡して送信します。

  • Solana接続
  • トランザクション命令
  • 支払者
  • コミットメント

以下を実行して、このトランザクションを送信できます。

コード
npm run e2e

この関数は圧縮NFTとそのコレクションをミントし、指定したウォレットへ転送します。

transferAsset関数の全体は以下のとおりです。

コード
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:バーン

cNFTをバーンすると、そのNFTがMerkle treeから削除されます。これにより、報酬を得るためのバーン、スパムアセットの削除、独自要件への対応など、さまざまなユースケースが可能になります。Tensorのミントでは、新しい1つのcNFTをミントするために10個のcNFTをバーンする用途で使用されました。

圧縮NFTをバーンするには、以下を渡す必要があります。

  • 接続 - Solana RPC接続。
  • 現在の所有者 - 現在のNFT所有者の公開鍵。
  • アセットID - 渡した特定のアセットの最新情報を取得するために使用します。

utils.tsのburnAssetで確認できます。

コード
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 //

}

getAssetProofを使用してルートとツリーIDを取得するため、これはtransferAssetと同じです。また、getAssetを呼び出して、クリエイターハッシュ、データハッシュ、リーフnonce、現在の所有者を取得しています。

これにより、圧縮アセットを変更するために、ここでどの値を取得する必要があるかを簡単に把握できます。

次に、このコードの下でトランザクション命令を設定します。

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

バーンの設定が完了したので、転送命令とよく似た方法でトランザクションを送信できます。

コード
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;
  }
}

次に、以下を渡して送信します。

  • Solana接続
  • トランザクション命令
  • 支払者
  • コミットメント

この関数を実行すると、指定したIDのアセットがバーンされます。アセットデータがツリーから削除され、圧縮アセットにburnt=trueフラグが設定されます。デフォルトでは、DAS APIを使用すると、これらのバーン済みアセットも結果に含まれます。

ターミナルで以下を実行できます。

コード
npm run burn -- --assetId=

Solana NFT:まとめ

圧縮NFTは、SolanaのNFTエコシステムと柔軟に連携する手段を提供します。DAS APIから取得したMerkle proofと正確なトランザクション命令を使用することで、開発者はこれらのユニークなデジタルアセットを変更し、操作できます。転送とバーン操作の詳細を理解すれば、より信頼性の高いアプリケーションを構築できます。

Heliusを購読

Solana開発の最新情報や新しい記事の公開通知を受け取れます