
Solana NFT: 압축 NFT 수정하기(2023)
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: 환경 설정
- 예제 저장소를 클론합니다.
git clone- 프로젝트 폴더로 이동합니다.
cd compression-examples- npm을 설치합니다.
npm install- 루트에 .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와 해당 컬렉션을 민팅한 다음 지정된 지갑으로 전송합니다.
소각 수정 작업에 사용할 자산 ID입니다.
전체 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 개발 소식을 확인하고 새 게시물 알림을 받아보세요


