신규: Helius가 Light Protocol을 인수했습니다
Solana에서 압축 NFT를 사용하는 방법
블로그/개발

Solana NFT: 압축 NFT 수정하기(2023)

개발자 관계 책임자LinkedIn의 Hunter Davis
읽는 데 6분

Solana NFT: 수정이 필요한 이유

Solana 개발자에게 압축 NFT 연동은 어려운 과제로 느껴질 수 있습니다. 하지만 이제 Bubblegum 프로그램을 통해 압축 NFT를 수정하거나 변경할 수 있어 더 이상 어렵지 않습니다.

압축 NFT를 민팅한 후 해당 NFT를 변경하려면(예: 전송, 소각) 트랜잭션 명령어와 함께 Merkle 증명을 제공해야 합니다. Merkle 증명은 리프가 트리에 속한다는 것을 증명하는 해시 집합입니다.

이 가이드에서는 다음 내용을 다룹니다.

  • 압축 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: 수정

수정하려면 자산의 현재 소유권, 권한, 압축 해시와 함께 명령어에 필요한 최신 증명을 확보해야 합니다. 이 데이터는 압축 자산과의 상호작용을 간소화하는 Digital Asset Standard (DAS) API에서 가져와야 합니다.

현재 Bubblegum에서 사용할 수 있는 수정 작업은 다음과 같습니다.

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

압축 자산과 상호작용하려면 다음을 반환해야 합니다.

  • 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에서 이를 계산합니다. 해당 명령어는 곧 살펴보겠습니다.

이제 증명 경로를 확보했으므로 DAS API에서 getAsset을 사용해 리프 논스, 리프 위임자, 데이터 해시, 생성자 해시를 반환할 수 있습니다.

아래와 같이 설정할 수 있습니다.

코드
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만 사용해 트리 권한을 정의합니다. 이 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를 사용해 트랜잭션을 정의하고, 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를 소각하면 Merkle 트리에서 NFT가 제거됩니다. 보상을 위한 소각, 스팸 자산 제거, 맞춤 요구 사항 처리 등 다양한 사용 사례에 활용할 수 있습니다. Tensor 민팅에서는 새 cNFT 1개를 민팅하기 위해 cNFT 10개를 소각하는 데 사용되었습니다.

압축 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 //

}

루트와 트리 ID를 반환하기 위해 getAssetProof을 사용하므로 transferAsset과 동일합니다. 또한 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는 Solana NFT 생태계와 상호작용하는 역동적인 수단을 제공합니다. 개발자는 DAS API에서 얻은 Merkle 증명과 정확한 트랜잭션 명령어를 통해 고유한 디지털 자산을 수정하고 상호작용할 수 있습니다. 전송과 소각 수정 작업의 세부 사항을 이해하면 더 안정적인 애플리케이션을 구축할 수 있습니다.

Helius 구독하기

최신 Solana 개발 소식을 확인하고 새 게시물 알림을 받아보세요