forked from solana-developers/compressed-nfts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
transferNFT.ts
293 lines (231 loc) · 9.57 KB
/
transferNFT.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
/**
* Demonstrate how to transfer a compressed NFT from one account to another
*/
import {
AccountMeta,
PublicKey,
Transaction,
clusterApiUrl,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
PROGRAM_ID as BUBBLEGUM_PROGRAM_ID,
createTransferInstruction,
} from "@metaplex-foundation/mpl-bubblegum";
import {
SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
SPL_NOOP_PROGRAM_ID,
ConcurrentMerkleTreeAccount,
MerkleTree,
MerkleTreeProof,
} from "@solana/spl-account-compression";
// import custom helpers for demos
import {
loadPublicKeysFromFile,
loadKeypairFromFile,
loadOrGenerateKeypair,
printConsoleSeparator,
extractSignatureFromFailedTransaction,
explorerURL,
} from "@/utils/helpers";
// local import of the connection wrapper, to help with using the ReadApi
import { WrapperConnection } from "@/ReadApi/WrapperConnection";
import dotenv from "dotenv";
dotenv.config();
(async () => {
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// load the env variables and store the cluster RPC url
const CLUSTER_URL = process.env.RPC_URL ?? clusterApiUrl("devnet");
// create a new rpc connection, using the ReadApi wrapper
const connection = new WrapperConnection(CLUSTER_URL);
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// load or generate a new Keypair for testing, named `testWallet`
const testWallet = loadOrGenerateKeypair("testWallet");
// generate a new keypair for use in this demo (or load it locally from the filesystem when available)
const payer = process.env?.LOCAL_PAYER_JSON_ABSPATH
? loadKeypairFromFile(process.env?.LOCAL_PAYER_JSON_ABSPATH)
: loadOrGenerateKeypair("payer");
console.log("Payer address:", payer.publicKey.toBase58());
console.log("Test wallet address:", testWallet.publicKey.toBase58());
// load the stored PublicKeys for ease of use
let keys = loadPublicKeysFromFile();
// ensure the primary script was already run
if (!keys?.assetIdTestAddress)
return console.warn("No locally saved `assetId` was found, Please run a `fetchNFT` script");
const assetIdTestAddress: PublicKey = keys.assetIdTestAddress;
const assetIdUserAddress: PublicKey = keys.assetIdUserAddress;
console.log("==== Local PublicKeys loaded ====");
console.log("Test Asset ID:", assetIdTestAddress.toBase58());
console.log("User Asset ID:", assetIdUserAddress.toBase58());
// set the asset to test with
// const assetId = assetIdTestAddress;
const assetId = assetIdUserAddress;
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
/**
* Get the asset details from the RPC
*/
printConsoleSeparator("Get the asset details from the RPC");
const asset = await connection.getAsset(assetId);
console.log(asset);
console.log("Is this a compressed NFT?", asset.compression.compressed);
console.log("Current owner:", asset.ownership.owner);
console.log("Current delegate:", asset.ownership.delegate);
// ensure the current asset is actually a compressed NFT
if (!asset.compression.compressed)
return console.error(`The asset ${asset.id} is NOT a compressed NFT!`);
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
/**
* Get the asset's proof from the RPC
*/
printConsoleSeparator("Get the asset proof from the RPC");
const assetProof = await connection.getAssetProof(assetId);
console.log(assetProof);
/**
* Get the tree's current on-chain account data
*/
// parse the tree's address from the `asset`
const treeAddress = new PublicKey(asset.compression.tree);
console.log("Tree address:", treeAddress.toBase58());
// get the tree's account info from the cluster
const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress(connection, treeAddress);
/**
* Perform client side verification of the proof that was provided by the RPC
* ---
* NOTE: This is not required to be performed, but may aid in catching errors
* due to your RPC providing stale or incorrect data (often due to caching issues)
* The actual proof validation is performed on-chain.
*/
printConsoleSeparator("Validate the RPC provided asset proof on the client side:");
const merkleTreeProof: MerkleTreeProof = {
leafIndex: asset.compression.leaf_id,
leaf: new PublicKey(assetProof.leaf).toBuffer(),
root: new PublicKey(assetProof.root).toBuffer(),
proof: assetProof.proof.map((node: string) => new PublicKey(node).toBuffer()),
};
const currentRoot = treeAccount.getCurrentRoot();
const rpcRoot = new PublicKey(assetProof.root).toBuffer();
console.log(
"Is RPC provided proof/root valid:",
MerkleTree.verify(rpcRoot, merkleTreeProof, false),
);
console.log(
"Does the current on-chain root match RPC provided root:",
new PublicKey(currentRoot).toBase58() === new PublicKey(rpcRoot).toBase58(),
);
/**
* INFO:
* The current on-chain root value does NOT have to match this RPC provided
* root in order to perform the transfer. This is due to the on-chain
* "changelog" (set via the tree's `maxBufferSize` at creation) keeping track
* of valid roots and proofs. Thus allowing for the "concurrent" nature of
* these special "concurrent merkle trees".
*/
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
/**
* Build the transfer instruction to transfer ownership of the compressed NFT
* ---
* By "transferring" ownership of a compressed NFT, the `leafOwner`
* value is updated to the new owner.
* ---
* NOTE: This will also remove the `leafDelegate`. If a new delegate is
* desired, then another instruction needs to be built (using the
* `createDelegateInstruction`) and added into the transaction.
*/
// set the new owner of the compressed NFT
const newLeafOwner = testWallet.publicKey;
// set the current leafOwner (aka the current owner of the NFT)
const leafOwner = new PublicKey(asset.ownership.owner);
// set the current leafDelegate
const leafDelegate = !!asset.ownership?.delegate
? new PublicKey(asset.ownership.delegate)
: leafOwner;
/**
* NOTE: When there is NOT a current `leafDelegate`,
* the current leafOwner` address should be used
*/
const treeAuthority = treeAccount.getAuthority();
const canopyDepth = treeAccount.getCanopyDepth();
// parse the list of proof addresses into a valid AccountMeta[]
const proofPath: AccountMeta[] = assetProof.proof
.map((node: string) => ({
pubkey: new PublicKey(node),
isSigner: false,
isWritable: false,
}))
.slice(0, assetProof.proof.length - (!!canopyDepth ? canopyDepth : 0));
//
// console.log(proofPath);
// create the NFT transfer instruction (via the Bubblegum package)
const transferIx = createTransferInstruction(
{
merkleTree: treeAddress,
treeAuthority,
leafOwner,
leafDelegate,
newLeafOwner,
logWrapper: SPL_NOOP_PROGRAM_ID,
compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
anchorRemainingAccounts: proofPath,
},
{
root: [...new PublicKey(assetProof.root.trim()).toBytes()],
dataHash: [...new PublicKey(asset.compression.data_hash.trim()).toBytes()],
creatorHash: [...new PublicKey(asset.compression.creator_hash.trim()).toBytes()],
nonce: asset.compression.leaf_id,
index: asset.compression.leaf_id,
},
BUBBLEGUM_PROGRAM_ID,
);
// return;
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
printConsoleSeparator("Sending the transfer transaction...");
try {
// create and send the transaction to transfer ownership of the NFT
const tx = new Transaction().add(transferIx);
tx.feePayer = payer.publicKey;
// send the transaction
const txSignature = await sendAndConfirmTransaction(
connection,
tx,
// ensuring the feePayer signs the transaction
[payer],
{
commitment: "confirmed",
skipPreflight: true,
},
);
console.log("\nTransfer successful!\n", explorerURL({ txSignature }));
/**
* Re-fetch the asset from the RPC to see the new ownership
*/
const newAsset = await connection.getAsset(assetId);
// console.log(newAsset);
printConsoleSeparator();
/**
* NOTE: Since part of the asset's data changed (i.e. the owner),
* the proof will have also changed
*/
// const newAssetProof = await connection.getAssetProof(assetId);
// console.log(newAssetProof);
// display the new and old ownership values
console.log(" Old owner:", asset.ownership.owner);
console.log(" Old delegate:", asset.ownership.delegate);
console.log(" New owner:", newAsset.ownership.owner);
console.log(" New delegate:", newAsset.ownership.delegate);
// the end :)
} catch (err: any) {
console.error("\nFailed to create transfer nft:", err);
console.log("\n=======================");
console.log(" Transfer failed!");
console.log("=======================");
// log a block explorer link for the failed transaction
await extractSignatureFromFailedTransaction(connection, err);
throw err;
}
})();