-
Notifications
You must be signed in to change notification settings - Fork 38
/
Copy pathverboseCreateAndMint.ts
302 lines (251 loc) · 10 KB
/
verboseCreateAndMint.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
294
295
296
297
298
299
300
301
302
/**
* Compressed NFTs on Solana, using State Compression
---
Overall flow of this script
- load or create two keypairs (named `payer` and `testWallet`)
- create a new tree with enough space to mint all the nft's you want for the "collection"
- create a new NFT Collection on chain (using the usual Metaplex methods)
- mint a single compressed nft into the tree to the `payer`
- mint a single compressed nft into the tree to the `testWallet`
- display the overall cost to perform all these actions
---
NOTE: this script is identical to the `scripts/createAndMint.ts` file, except THIS file has
additional explanation, comments, and console logging for demonstration purposes.
*/
/**
* General process of minting a compressed NFT:
* - create a tree
* - create a collection
* - mint compressed NFTs to the tree
*/
import { Keypair, LAMPORTS_PER_SOL, clusterApiUrl } from "@solana/web3.js";
import {
ValidDepthSizePair,
getConcurrentMerkleTreeAccountSize,
} from "@solana/spl-account-compression";
import {
MetadataArgs,
TokenProgramVersion,
TokenStandard,
} from "@metaplex-foundation/mpl-bubblegum";
import { CreateMetadataAccountArgsV3 } from "@metaplex-foundation/mpl-token-metadata";
// import custom helpers for demos
import {
loadKeypairFromFile,
loadOrGenerateKeypair,
numberFormatter,
printConsoleSeparator,
savePublicKeyToFile,
} from "@/utils/helpers";
// import custom helpers to mint compressed NFTs
import { createCollection, createTree, mintCompressedNFT } from "@/utils/compression";
// local import of the connection wrapper, to help with using the ReadApi
import { WrapperConnection } from "@/ReadApi/WrapperConnection";
import dotenv from "dotenv";
dotenv.config();
// define some reusable balance values for tracking
let initBalance: number, balance: number;
(async () => {
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// generate a new Keypair for testing, named `wallet`
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());
// locally save the addresses for the demo
savePublicKeyToFile("userAddress", payer.publicKey);
savePublicKeyToFile("testWallet", testWallet.publicKey);
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// 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, "confirmed");
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// get the payer's starting balance
initBalance = await connection.getBalance(payer.publicKey);
console.log(
"Starting account balance:",
numberFormatter(initBalance / LAMPORTS_PER_SOL),
"SOL\n",
);
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
/*
Define our tree size parameters
*/
const maxDepthSizePair: ValidDepthSizePair = {
// max=8 nodes
// maxDepth: 3,
// maxBufferSize: 8,
// max=16,384 nodes
maxDepth: 14,
maxBufferSize: 64,
// max=131,072 nodes
// maxDepth: 17,
// maxBufferSize: 64,
// max=1,048,576 nodes
// maxDepth: 20,
// maxBufferSize: 256,
// max=1,073,741,824 nodes
// maxDepth: 30,
// maxBufferSize: 2048,
};
const canopyDepth = maxDepthSizePair.maxDepth - 5;
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
/*
For demonstration purposes, we can compute how much space our tree will
need to allocate to store all the records. As well as the cost to allocate
this space (aka minimum balance to be rent exempt)
---
NOTE: These are performed automatically when using the `createAllocTreeIx`
function to ensure enough space is allocated, and rent paid.
*/
// calculate the space available in the tree
const requiredSpace = getConcurrentMerkleTreeAccountSize(
maxDepthSizePair.maxDepth,
maxDepthSizePair.maxBufferSize,
canopyDepth,
);
const storageCost = await connection.getMinimumBalanceForRentExemption(requiredSpace);
// demonstrate data points for compressed NFTs
console.log("Space to allocate:", numberFormatter(requiredSpace), "bytes");
console.log("Estimated cost to allocate space:", numberFormatter(storageCost / LAMPORTS_PER_SOL));
console.log(
"Max compressed NFTs for tree:",
numberFormatter(Math.pow(2, maxDepthSizePair.maxDepth)),
"\n",
);
// ensure the payer has enough balance to create the allocate the Merkle tree
if (initBalance < storageCost) return console.error("Not enough SOL to allocate the merkle tree");
printConsoleSeparator();
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
/*
Actually allocate the tree on chain
*/
// define the address the tree will live at
const treeKeypair = Keypair.generate();
// create and send the transaction to create the tree on chain
const tree = await createTree(connection, payer, treeKeypair, maxDepthSizePair, canopyDepth);
// locally save the addresses for the demo
savePublicKeyToFile("treeAddress", tree.treeAddress);
savePublicKeyToFile("treeAuthority", tree.treeAuthority);
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
/*
Create the actual NFT collection (using the normal Metaplex method)
(nothing special about compression here)
*/
// define the metadata to be used for creating the NFT collection
const collectionMetadataV3: CreateMetadataAccountArgsV3 = {
data: {
name: "Super Sweet NFT Collection",
symbol: "SSNC",
// specific json metadata for the collection
uri: "https://supersweetcollection.notarealurl/collection.json",
sellerFeeBasisPoints: 100,
creators: [
{
address: payer.publicKey,
verified: false,
share: 100,
},
], // or set to `null`
collection: null,
uses: null,
},
isMutable: false,
collectionDetails: null,
};
// create a full token mint and initialize the collection (with the `payer` as the authority)
const collection = await createCollection(connection, payer, collectionMetadataV3);
// locally save the addresses for the demo
savePublicKeyToFile("collectionMint", collection.mint);
savePublicKeyToFile("collectionMetadataAccount", collection.metadataAccount);
savePublicKeyToFile("collectionMasterEditionAccount", collection.masterEditionAccount);
/**
* INFO: NFT collection != tree
* ---
* NFTs collections can use multiple trees for their same collection.
* When minting any compressed NFT, simply pass the collection's addresses
* in the transaction using any valid tree the `payer` has authority over.
*
* These minted compressed NFTs should all still be apart of the same collection
* on marketplaces and wallets.
*/
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
/*
Mint a single compressed NFT
*/
const compressedNFTMetadata: MetadataArgs = {
name: "NFT Name",
symbol: collectionMetadataV3.data.symbol,
// specific json metadata for each NFT
uri: "https://supersweetcollection.notarealurl/token.json",
creators: [
{
address: payer.publicKey,
verified: false,
share: 100,
},
{
address: testWallet.publicKey,
verified: false,
share: 0,
},
], // or set to null
editionNonce: 0,
uses: null,
collection: null,
primarySaleHappened: false,
sellerFeeBasisPoints: 0,
isMutable: false,
// these values are taken from the Bubblegum package
tokenProgramVersion: TokenProgramVersion.Original,
tokenStandard: TokenStandard.NonFungible,
};
// fully mint a single compressed NFT to the payer
console.log(`Minting a single compressed NFT to ${payer.publicKey.toBase58()}...`);
await mintCompressedNFT(
connection,
payer,
treeKeypair.publicKey,
collection.mint,
collection.metadataAccount,
collection.masterEditionAccount,
compressedNFTMetadata,
// mint to this specific wallet (in this case, the tree owner aka `payer`)
payer.publicKey,
);
// fully mint a single compressed NFT
console.log(`Minting a single compressed NFT to ${testWallet.publicKey.toBase58()}...`);
await mintCompressedNFT(
connection,
payer,
treeKeypair.publicKey,
collection.mint,
collection.metadataAccount,
collection.masterEditionAccount,
compressedNFTMetadata,
// mint to this specific wallet (in this case, airdrop to `testWallet`)
testWallet.publicKey,
);
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
// fetch the payer's final balance
balance = await connection.getBalance(payer.publicKey);
console.log(`===============================`);
console.log(
"Total cost:",
numberFormatter((initBalance - balance) / LAMPORTS_PER_SOL, true),
"SOL\n",
);
})();