@zoralabs/protocol-sdk 0.5.8-DEV.2 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,619 @@
1
+ import { Address, zeroAddress } from "viem";
2
+ import { foundry } from "viem/chains";
3
+ import { describe, expect } from "vitest";
4
+ import { parseEther } from "viem";
5
+ import {
6
+ zoraCreator1155PremintExecutorImplABI as preminterAbi,
7
+ zoraCreator1155ImplABI,
8
+ zoraCreator1155FactoryImplAddress,
9
+ zoraCreator1155FactoryImplConfig,
10
+ } from "@zoralabs/protocol-deployments";
11
+
12
+ import {
13
+ premintTypedDataDefinition,
14
+ isValidSignature,
15
+ recoverCreatorFromCreatorAttribution,
16
+ getPremintExecutorAddress,
17
+ getPremintMintCosts,
18
+ } from "./preminter";
19
+ import {
20
+ ContractCreationConfig,
21
+ PremintConfigV1,
22
+ TokenCreationConfigV1,
23
+ PremintConfigVersion,
24
+ TokenCreationConfigV2,
25
+ PremintConfigV2,
26
+ MintArguments,
27
+ } from "./contract-types";
28
+ import { AnvilViemClientsTest, forkUrls, makeAnvilTest } from "src/anvil";
29
+
30
+ // create token and contract creation config:
31
+ export const defaultContractConfig = ({
32
+ contractAdmin,
33
+ }: {
34
+ contractAdmin: Address;
35
+ }): ContractCreationConfig => ({
36
+ contractAdmin,
37
+ contractURI: "ipfs://asdfasdfasdf",
38
+ contractName: "My fun NFT",
39
+ });
40
+
41
+ const defaultTokenConfigV1 = (
42
+ fixedPriceMinterAddress: Address,
43
+ creatorAccount: Address,
44
+ ): TokenCreationConfigV1 => ({
45
+ tokenURI: "ipfs://tokenIpfsId0",
46
+ maxSupply: 100n,
47
+ maxTokensPerAddress: 10n,
48
+ pricePerToken: 0n,
49
+ mintStart: 0n,
50
+ mintDuration: 100n,
51
+ royaltyMintSchedule: 30,
52
+ royaltyBPS: 200,
53
+ royaltyRecipient: creatorAccount,
54
+ fixedPriceMinter: fixedPriceMinterAddress,
55
+ });
56
+
57
+ const defaultTokenConfigV2 = (
58
+ fixedPriceMinterAddress: Address,
59
+ creatorAccount: Address,
60
+ createReferral: Address,
61
+ pricePerToken = 0n,
62
+ ): TokenCreationConfigV2 => ({
63
+ tokenURI: "ipfs://tokenIpfsId0",
64
+ maxSupply: 100n,
65
+ maxTokensPerAddress: 1000n,
66
+ pricePerToken,
67
+ mintStart: 0n,
68
+ mintDuration: 100n,
69
+ royaltyBPS: 200,
70
+ payoutRecipient: creatorAccount,
71
+ fixedPriceMinter: fixedPriceMinterAddress,
72
+ createReferral,
73
+ });
74
+
75
+ const defaultPremintConfigV1 = ({
76
+ fixedPriceMinter,
77
+ creatorAccount,
78
+ }: {
79
+ fixedPriceMinter: Address;
80
+ creatorAccount: Address;
81
+ }): PremintConfigV1 => ({
82
+ tokenConfig: defaultTokenConfigV1(fixedPriceMinter, creatorAccount),
83
+ deleted: false,
84
+ uid: 105,
85
+ version: 0,
86
+ });
87
+
88
+ export const defaultPremintConfigV2 = ({
89
+ fixedPriceMinter,
90
+ creatorAccount,
91
+ createReferral = zeroAddress,
92
+ pricePerToken = 0n,
93
+ }: {
94
+ fixedPriceMinter: Address;
95
+ creatorAccount: Address;
96
+ createReferral?: Address;
97
+ pricePerToken?: bigint;
98
+ }): PremintConfigV2 => ({
99
+ tokenConfig: defaultTokenConfigV2(
100
+ fixedPriceMinter,
101
+ creatorAccount,
102
+ createReferral,
103
+ pricePerToken,
104
+ ),
105
+ deleted: false,
106
+ uid: 106,
107
+ version: 0,
108
+ });
109
+
110
+ const PREMINTER_ADDRESS = getPremintExecutorAddress();
111
+
112
+ const anvilTest = makeAnvilTest({
113
+ forkUrl: forkUrls.zoraSepolia,
114
+ forkBlockNumber: 1265490,
115
+ });
116
+
117
+ async function setupContracts({
118
+ viemClients: { walletClient, testClient, publicClient },
119
+ }: AnvilViemClientsTest) {
120
+ // JSON-RPC Account
121
+ const [deployerAccount, creatorAccount, collectorAccount] =
122
+ (await walletClient.getAddresses()) as [Address, Address, Address, Address];
123
+
124
+ // deploy signature minter contract
125
+ await testClient.setBalance({
126
+ address: deployerAccount,
127
+ value: parseEther("10"),
128
+ });
129
+
130
+ const fixedPriceMinterAddress = await publicClient.readContract({
131
+ abi: zoraCreator1155FactoryImplConfig.abi,
132
+ address: zoraCreator1155FactoryImplAddress[999],
133
+ functionName: "fixedPriceMinter",
134
+ });
135
+
136
+ return {
137
+ accounts: {
138
+ deployerAccount,
139
+ creatorAccount,
140
+ collectorAccount,
141
+ },
142
+ fixedPriceMinterAddress,
143
+ };
144
+ }
145
+
146
+ const zoraSepoliaAnvilTest = makeAnvilTest({
147
+ forkUrl: forkUrls.zoraSepolia,
148
+ forkBlockNumber: 3118200,
149
+ });
150
+
151
+ describe("ZoraCreator1155Preminter", () => {
152
+ // skip for now - we need to make this work on zora testnet chain too
153
+ anvilTest(
154
+ "can sign on the forked premint contract",
155
+ async ({ viemClients }) => {
156
+ const {
157
+ fixedPriceMinterAddress,
158
+ accounts: { creatorAccount },
159
+ } = await setupContracts({ viemClients });
160
+ const premintConfig = defaultPremintConfigV1({
161
+ fixedPriceMinter: fixedPriceMinterAddress,
162
+ creatorAccount,
163
+ });
164
+ const contractConfig = defaultContractConfig({
165
+ contractAdmin: creatorAccount,
166
+ });
167
+
168
+ const preminterAddress = getPremintExecutorAddress();
169
+
170
+ const contractAddress = await viemClients.publicClient.readContract({
171
+ abi: preminterAbi,
172
+ address: preminterAddress,
173
+ functionName: "getContractAddress",
174
+ args: [contractConfig],
175
+ });
176
+
177
+ const signedMessage = await viemClients.walletClient.signTypedData({
178
+ ...premintTypedDataDefinition({
179
+ verifyingContract: contractAddress,
180
+ chainId: 999,
181
+ premintConfig,
182
+ premintConfigVersion: PremintConfigVersion.V1,
183
+ }),
184
+ account: creatorAccount,
185
+ });
186
+
187
+ console.log({
188
+ creatorAccount,
189
+ signedMessage,
190
+ contractConfig,
191
+ premintConfig,
192
+ contractAddress,
193
+ });
194
+ },
195
+ 20 * 1000,
196
+ );
197
+ zoraSepoliaAnvilTest(
198
+ "can sign and recover a v1 premint config signature",
199
+ async ({ viemClients }) => {
200
+ const {
201
+ fixedPriceMinterAddress,
202
+ accounts: { creatorAccount },
203
+ } = await setupContracts({ viemClients });
204
+
205
+ const premintConfig = defaultPremintConfigV1({
206
+ fixedPriceMinter: fixedPriceMinterAddress,
207
+ creatorAccount,
208
+ });
209
+ const contractConfig = defaultContractConfig({
210
+ contractAdmin: creatorAccount,
211
+ });
212
+
213
+ const tokenContract = await viemClients.publicClient.readContract({
214
+ abi: preminterAbi,
215
+ address: PREMINTER_ADDRESS,
216
+ functionName: "getContractAddress",
217
+ args: [contractConfig],
218
+ });
219
+
220
+ // sign message containing contract and token creation config and uid
221
+ const signedMessage = await viemClients.walletClient.signTypedData({
222
+ ...premintTypedDataDefinition({
223
+ verifyingContract: tokenContract,
224
+ // we need to sign here for the anvil chain, cause thats where it is run on
225
+ chainId: foundry.id,
226
+ premintConfig,
227
+ premintConfigVersion: PremintConfigVersion.V1,
228
+ }),
229
+ account: creatorAccount,
230
+ });
231
+
232
+ // recover and verify address is correct
233
+ const { recoveredAddress, isAuthorized } = await isValidSignature({
234
+ collection: contractConfig,
235
+ chainId: viemClients.publicClient.chain!.id,
236
+ premintConfig,
237
+ premintConfigVersion: PremintConfigVersion.V1,
238
+ publicClient: viemClients.publicClient,
239
+ signature: signedMessage,
240
+ });
241
+
242
+ expect(recoveredAddress).to.equal(creatorAccount);
243
+ expect(isAuthorized).toBe(true);
244
+
245
+ expect(recoveredAddress).to.equal(creatorAccount);
246
+ },
247
+
248
+ 20 * 1000,
249
+ );
250
+ zoraSepoliaAnvilTest(
251
+ "can sign and recover a v2 premint config signature",
252
+ async ({ viemClients }) => {
253
+ const {
254
+ fixedPriceMinterAddress,
255
+ accounts: { creatorAccount },
256
+ } = await setupContracts({ viemClients });
257
+
258
+ const premintConfig = defaultPremintConfigV2({
259
+ creatorAccount,
260
+ fixedPriceMinter: fixedPriceMinterAddress,
261
+ createReferral: creatorAccount,
262
+ });
263
+ const contractConfig = defaultContractConfig({
264
+ contractAdmin: creatorAccount,
265
+ });
266
+
267
+ const tokenContract = await viemClients.publicClient.readContract({
268
+ abi: preminterAbi,
269
+ address: PREMINTER_ADDRESS,
270
+ functionName: "getContractAddress",
271
+ args: [contractConfig],
272
+ });
273
+
274
+ // sign message containing contract and token creation config and uid
275
+ const signedMessage = await viemClients.walletClient.signTypedData({
276
+ ...premintTypedDataDefinition({
277
+ verifyingContract: tokenContract,
278
+ // we need to sign here for the anvil chain, cause thats where it is run on
279
+ chainId: foundry.id,
280
+ premintConfig,
281
+ premintConfigVersion: PremintConfigVersion.V2,
282
+ }),
283
+ account: creatorAccount,
284
+ });
285
+
286
+ // recover and verify address is correct
287
+ const { recoveredAddress, isAuthorized } = await isValidSignature({
288
+ collection: contractConfig,
289
+ chainId: viemClients.publicClient.chain!.id,
290
+ premintConfig,
291
+ premintConfigVersion: PremintConfigVersion.V2,
292
+ publicClient: viemClients.publicClient,
293
+ signature: signedMessage,
294
+ });
295
+
296
+ expect(recoveredAddress).to.equal(creatorAccount);
297
+ expect(isAuthorized).toBe(true);
298
+
299
+ expect(recoveredAddress).to.equal(creatorAccount);
300
+ },
301
+
302
+ 20 * 1000,
303
+ );
304
+ zoraSepoliaAnvilTest(
305
+ "can sign and mint multiple tokens",
306
+ async ({ viemClients }) => {
307
+ const {
308
+ fixedPriceMinterAddress,
309
+ accounts: { creatorAccount, collectorAccount },
310
+ } = await setupContracts({ viemClients });
311
+ // setup contract and token creation parameters
312
+ const premintConfig1 = defaultPremintConfigV1({
313
+ fixedPriceMinter: fixedPriceMinterAddress,
314
+ creatorAccount,
315
+ });
316
+ const contractConfig = defaultContractConfig({
317
+ contractAdmin: creatorAccount,
318
+ });
319
+
320
+ // lets make it a random number to not break the existing tests that expect fresh data
321
+ premintConfig1.uid = Math.round(Math.random() * 1000000);
322
+
323
+ let contractAddress = await viemClients.publicClient.readContract({
324
+ abi: preminterAbi,
325
+ address: PREMINTER_ADDRESS,
326
+ functionName: "getContractAddress",
327
+ args: [contractConfig],
328
+ });
329
+
330
+ // have creator sign the message to create the contract
331
+ // and the token
332
+ const signedMessage = await viemClients.walletClient.signTypedData({
333
+ ...premintTypedDataDefinition({
334
+ verifyingContract: contractAddress,
335
+ // we need to sign here for the anvil chain, cause thats where it is run on
336
+ chainId: foundry.id,
337
+ premintConfig: premintConfig1,
338
+ premintConfigVersion: PremintConfigVersion.V1,
339
+ }),
340
+ account: creatorAccount,
341
+ });
342
+
343
+ const quantityToMint = 2n;
344
+
345
+ const valueToSend = (
346
+ await getPremintMintCosts({
347
+ publicClient: viemClients.publicClient,
348
+ quantityToMint,
349
+ tokenContract: contractAddress,
350
+ tokenPrice: premintConfig1.tokenConfig.pricePerToken,
351
+ })
352
+ ).totalCost;
353
+
354
+ await viemClients.testClient.setBalance({
355
+ address: collectorAccount,
356
+ value: parseEther("10"),
357
+ });
358
+
359
+ // get the premint status - it should not be minted
360
+ let [contractCreated, tokenId] =
361
+ await viemClients.publicClient.readContract({
362
+ abi: preminterAbi,
363
+ address: PREMINTER_ADDRESS,
364
+ functionName: "premintStatus",
365
+ args: [contractAddress, premintConfig1.uid],
366
+ });
367
+
368
+ expect(contractCreated).toBe(false);
369
+ expect(tokenId).toBe(0n);
370
+
371
+ const mintArguments: MintArguments = {
372
+ mintComment: "",
373
+ mintRecipient: collectorAccount,
374
+ mintRewardsRecipients: [],
375
+ };
376
+
377
+ // now have the collector execute the first signed message;
378
+ // it should create the contract, the token,
379
+ // and min the quantity to mint tokens to the collector
380
+ // the signature along with contract + token creation
381
+ // parameters are required to call this function
382
+ const mintHash = await viemClients.walletClient.writeContract({
383
+ abi: preminterAbi,
384
+ functionName: "premintV1",
385
+ account: collectorAccount,
386
+ chain: foundry,
387
+ address: PREMINTER_ADDRESS,
388
+ args: [
389
+ contractConfig,
390
+ premintConfig1,
391
+ signedMessage,
392
+ quantityToMint,
393
+ mintArguments,
394
+ ],
395
+ value: valueToSend,
396
+ });
397
+
398
+ // ensure it succeeded
399
+ const receipt = await viemClients.publicClient.waitForTransactionReceipt({
400
+ hash: mintHash,
401
+ });
402
+
403
+ expect(receipt.status).toBe("success");
404
+
405
+ // fetch the premint token id
406
+ [contractCreated, tokenId] = await viemClients.publicClient.readContract({
407
+ abi: preminterAbi,
408
+ address: PREMINTER_ADDRESS,
409
+ functionName: "premintStatus",
410
+ args: [contractAddress, premintConfig1.uid],
411
+ });
412
+
413
+ expect(contractCreated).toBe(true);
414
+ expect(tokenId).not.toBe(0n);
415
+
416
+ // now use what was created, to get the balance from the created contract
417
+ const tokenBalance = await viemClients.publicClient.readContract({
418
+ abi: zoraCreator1155ImplABI,
419
+ address: contractAddress,
420
+ functionName: "balanceOf",
421
+ args: [collectorAccount, tokenId],
422
+ });
423
+
424
+ // get token balance - should be amount that was created
425
+ expect(tokenBalance).toBe(quantityToMint);
426
+
427
+ const premintConfig2 = defaultPremintConfigV2({
428
+ creatorAccount,
429
+ fixedPriceMinter: fixedPriceMinterAddress,
430
+ createReferral: creatorAccount,
431
+ });
432
+
433
+ // sign the message to create the second token
434
+ const signedMessage2 = await viemClients.walletClient.signTypedData({
435
+ ...premintTypedDataDefinition({
436
+ verifyingContract: contractAddress,
437
+ chainId: foundry.id,
438
+ premintConfig: premintConfig2,
439
+ premintConfigVersion: PremintConfigVersion.V2,
440
+ }),
441
+ account: creatorAccount,
442
+ });
443
+
444
+ const quantityToMint2 = 4n;
445
+
446
+ const valueToSend2 = (
447
+ await getPremintMintCosts({
448
+ publicClient: viemClients.publicClient,
449
+ quantityToMint: quantityToMint2,
450
+ tokenContract: contractAddress,
451
+ tokenPrice: premintConfig2.tokenConfig.pricePerToken,
452
+ })
453
+ ).totalCost;
454
+
455
+ const simulationResult = await viemClients.publicClient.simulateContract({
456
+ abi: preminterAbi,
457
+ functionName: "premintV2",
458
+ account: collectorAccount,
459
+ chain: foundry,
460
+ address: PREMINTER_ADDRESS,
461
+ args: [
462
+ contractConfig,
463
+ premintConfig2,
464
+ signedMessage2,
465
+ quantityToMint2,
466
+ mintArguments,
467
+ ],
468
+ value: valueToSend2,
469
+ });
470
+
471
+ // now have the collector execute the second signed message.
472
+ // it should create a new token against the existing contract
473
+ const mintHash2 = await viemClients.walletClient.writeContract(
474
+ simulationResult.request,
475
+ );
476
+
477
+ const premintV2Receipt =
478
+ await viemClients.publicClient.waitForTransactionReceipt({
479
+ hash: mintHash2,
480
+ });
481
+
482
+ expect(premintV2Receipt.status).toBe("success");
483
+
484
+ // now premint status for the second mint, it should be minted
485
+ [, tokenId] = await viemClients.publicClient.readContract({
486
+ abi: preminterAbi,
487
+ address: PREMINTER_ADDRESS,
488
+ functionName: "premintStatus",
489
+ args: [contractAddress, premintConfig2.uid],
490
+ });
491
+
492
+ expect(tokenId).not.toBe(0n);
493
+
494
+ // get balance of second token
495
+ const tokenBalance2 = await viemClients.publicClient.readContract({
496
+ abi: zoraCreator1155ImplABI,
497
+ address: contractAddress,
498
+ functionName: "balanceOf",
499
+ args: [collectorAccount, tokenId],
500
+ });
501
+
502
+ expect(tokenBalance2).toBe(quantityToMint2);
503
+ },
504
+ // 10 second timeout
505
+ 40 * 1000,
506
+ );
507
+
508
+ zoraSepoliaAnvilTest(
509
+ "can decode the CreatorAttribution event",
510
+ async ({ viemClients }) => {
511
+ const {
512
+ fixedPriceMinterAddress,
513
+ accounts: { creatorAccount, collectorAccount },
514
+ } = await setupContracts({ viemClients });
515
+ const premintConfig = defaultPremintConfigV2({
516
+ fixedPriceMinter: fixedPriceMinterAddress,
517
+ creatorAccount,
518
+ });
519
+ const contractConfig = defaultContractConfig({
520
+ contractAdmin: creatorAccount,
521
+ });
522
+
523
+ // lets make it a random number to not break the existing tests that expect fresh data
524
+ premintConfig.uid = Math.round(Math.random() * 1000000);
525
+
526
+ let contractAddress = await viemClients.publicClient.readContract({
527
+ abi: preminterAbi,
528
+ address: PREMINTER_ADDRESS,
529
+ functionName: "getContractAddress",
530
+ args: [contractConfig],
531
+ });
532
+
533
+ const signingChainId = foundry.id;
534
+
535
+ // have creator sign the message to create the contract
536
+ // and the token
537
+ const signedMessage = await viemClients.walletClient.signTypedData({
538
+ ...premintTypedDataDefinition({
539
+ verifyingContract: contractAddress,
540
+ // we need to sign here for the anvil chain, cause thats where it is run on
541
+ chainId: signingChainId,
542
+ premintConfig,
543
+ premintConfigVersion: PremintConfigVersion.V2,
544
+ }),
545
+ account: creatorAccount,
546
+ });
547
+
548
+ const quantityToMint = 2n;
549
+
550
+ const valueToSend = (
551
+ await getPremintMintCosts({
552
+ publicClient: viemClients.publicClient,
553
+ quantityToMint,
554
+ tokenContract: contractAddress,
555
+ tokenPrice: premintConfig.tokenConfig.pricePerToken,
556
+ })
557
+ ).totalCost;
558
+
559
+ await viemClients.testClient.setBalance({
560
+ address: collectorAccount,
561
+ value: parseEther("10"),
562
+ });
563
+
564
+ // now have the collector execute the first signed message;
565
+ // it should create the contract, the token,
566
+ // and min the quantity to mint tokens to the collector
567
+ // the signature along with contract + token creation
568
+ // parameters are required to call this function
569
+ const mintHash = await viemClients.walletClient.writeContract({
570
+ abi: preminterAbi,
571
+ functionName: "premintV2",
572
+ account: collectorAccount,
573
+ chain: foundry,
574
+ address: PREMINTER_ADDRESS,
575
+ args: [
576
+ contractConfig,
577
+ premintConfig,
578
+ signedMessage,
579
+ quantityToMint,
580
+ {
581
+ mintComment: "",
582
+ mintRecipient: collectorAccount,
583
+ mintRewardsRecipients: [],
584
+ },
585
+ ],
586
+ value: valueToSend,
587
+ });
588
+
589
+ // ensure it succeeded
590
+ const receipt = await viemClients.publicClient.waitForTransactionReceipt({
591
+ hash: mintHash,
592
+ });
593
+
594
+ expect(receipt.status).toBe("success");
595
+
596
+ // get the CreatorAttribution event from the erc1155 contract:
597
+ const topics = await viemClients.publicClient.getContractEvents({
598
+ abi: zoraCreator1155ImplABI,
599
+ address: contractAddress,
600
+ eventName: "CreatorAttribution",
601
+ });
602
+
603
+ expect(topics.length).toBe(1);
604
+
605
+ const creatorAttributionEvent = topics[0]!;
606
+
607
+ const { creator: creatorFromEvent } = creatorAttributionEvent.args;
608
+
609
+ const recoveredSigner = await recoverCreatorFromCreatorAttribution({
610
+ creatorAttribution: creatorAttributionEvent.args,
611
+ chainId: signingChainId,
612
+ tokenContract: contractAddress,
613
+ });
614
+
615
+ expect(creatorFromEvent).toBe(creatorAccount);
616
+ expect(recoveredSigner).toBe(creatorFromEvent);
617
+ },
618
+ );
619
+ });