НОВОЕ: Helius приобретает Light Protocol
как использовать сжатые NFT в Solana
Блог/Разработка

Solana NFT: изменение сжатых NFT (2023)

Руководитель по работе с разработчикамиHunter Davis в LinkedIn
Чтение: 6 мин

Solana NFT: зачем нужны изменения

Разработчикам в Solana взаимодействие со сжатыми NFT может казаться сложной задачей. Теперь это не так: сжатые NFT можно изменять с помощью программы Bubblegum.

При выпуске сжатого NFT в инструкцию транзакции необходимо включать доказательства Меркла, чтобы вносить любые изменения в этот NFT, например передавать или сжигать его. Доказательство Меркла — это набор хешей, подтверждающий принадлежность листа дереву.

В этом руководстве мы рассмотрим:

  • Важность изменения сжатых NFT.
  • Настройку сжигания и передачи сжатых NFT.
  • Значение API Digital Asset Standard (DAS).

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 и секретный ключ кошелька плательщика. Убедитесь, что на кошельке достаточно SOL для оплаты транзакций.

Solana NFT: изменения

Для изменений нужны актуальные доказательства для инструкции, а также текущий владелец актива, полномочия и хеши сжатия. Эти данные необходимо получить через API Digital Asset Standard (DAS), который упрощает взаимодействие со сжатыми активами.

Через Bubblegum сейчас доступны следующие изменения:

  • Выпуск
  • Передача
  • Сжигание
  • Делегирование, отмена делегирования
  • Погашение, отмена погашения
  • Распаковка
  • Проверка создателя, назначение и проверка создателя
  • Проверка коллекции, назначение и проверка коллекции

Для взаимодействия со сжатыми активами необходимо получить:

  • getAssetProof — получает доказательство для вычисления.
  • getAsset — получает хеш создателя, хеш данных, одноразовое значение листа и текущего владельца или делегата листа.
  • getBubblegumAuthorityPDA — производный адрес программы, созданный на основе ID дерева.

В этих примерах мы рассмотрим сжигание и передачу выпущенных нами сжатых NFT.

NFT: передача

Операция передачи позволяет переместить сжатый NFT из кошелька одного владельца в кошелек другого. Продажи и объявления о продаже сжатых NFT используют эту инструкцию для отображения сделок на маркетплейсах.

Чтобы настроить это в существующем репозитории, откроем файл utils.ts и рассмотрим функцию transferAsset.

Нам потребуется передать следующие данные:

  • Подключение — подключение к Solana RPC.
  • Текущий владелец — публичный ключ текущего владельца NFT.
  • Новый владелец NFT — публичный ключ нового владельца.
  • ID актива — позволяет получить актуальные сведения об указанном активе.

В приведенном ниже примере функции transferAsset мы сначала запрашиваем доказательство актива и настраиваем proofPath. Этот запрос к DAS API возвращает доказательство для передаваемого актива. Затем оно вычисляется для передачи в инструкцию транзакции.

Код
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...

};

Доказательство актива необходимо, поскольку оно отражает текущее состояние сжатого актива. Оно требуется при каждой передаче сжатого NFT. Вы вычисляете его в proofPath, чтобы затем передать в инструкцию транзакции, которую мы вскоре рассмотрим.

Получив путь доказательства, мы можем использовать getAsset в DAS API, чтобы получить одноразовое значение листа, делегата листа, хеш данных и хеш создателя.

Настроим это следующим образом:

Код
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 возвращает владельца или делегата и одноразовое значение листа. Мы также определяем полномочия дерева, используя его 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: делегата или владельца листа, хеш данных, корень и одноразовое значение листа.

Также мы передаем несколько 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 и добавляем тип Transaction для transferIx из solana/web3.js .

Затем мы отправляем ее, передавая:

  • Подключение к 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 из дерева Меркла. Это открывает различные сценарии использования: сжигание ради наград, удаление спам-активов или выполнение особых требований. Такой подход использовался при выпуске Tensor: 10 cNFT сжигались для выпуска одного нового 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 //

}

Здесь используется тот же подход, что и в transferAsset: getAssetProof возвращает корень и ID дерева. Вы также вызываете getAsset, чтобы получить хеш создателя, хеш данных, одноразовое значение листа и текущего владельца.

Так легко определить, какие значения нужно получить для изменения сжатого актива.

Теперь под этим кодом можно настроить инструкцию транзакции:

Код
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 предоставляют гибкий способ взаимодействия с экосистемой NFT в Solana. Благодаря доказательствам Меркла, полученным через DAS API, и точным инструкциям транзакций разработчики могут изменять эти уникальные цифровые активы и взаимодействовать с ними. Понимание особенностей передачи и сжигания помогает разработчикам создавать более надежные приложения.

Подпишитесь на Helius

Следите за новостями разработки на Solana и получайте уведомления о наших публикациях