@bannynet/core-v6 0.0.1

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.
Files changed (48) hide show
  1. package/README.md +53 -0
  2. package/SKILLS.md +94 -0
  3. package/deployments/banny-core-v5/arbitrum/Banny721TokenUriResolver.json +1809 -0
  4. package/deployments/banny-core-v5/arbitrum_sepolia/Banny721TokenUriResolver.json +1795 -0
  5. package/deployments/banny-core-v5/base/Banny721TokenUriResolver.json +1810 -0
  6. package/deployments/banny-core-v5/base_sepolia/Banny721TokenUriResolver.json +1796 -0
  7. package/deployments/banny-core-v5/ethereum/Banny721TokenUriResolver.json +1795 -0
  8. package/deployments/banny-core-v5/optimism/Banny721TokenUriResolver.json +1810 -0
  9. package/deployments/banny-core-v5/optimism_sepolia/Banny721TokenUriResolver.json +1796 -0
  10. package/deployments/banny-core-v5/sepolia/Banny721TokenUriResolver.json +1795 -0
  11. package/foundry.toml +22 -0
  12. package/package.json +53 -0
  13. package/remappings.txt +1 -0
  14. package/script/1.Fix.s.sol +290 -0
  15. package/script/Add.Denver.s.sol +75 -0
  16. package/script/AirdropOutfits.s.sol +2302 -0
  17. package/script/Deploy.s.sol +440 -0
  18. package/script/Drop1.s.sol +979 -0
  19. package/script/MigrationContractArbitrum.sol +494 -0
  20. package/script/MigrationContractArbitrum1.sol +170 -0
  21. package/script/MigrationContractArbitrum2.sol +204 -0
  22. package/script/MigrationContractArbitrum3.sol +174 -0
  23. package/script/MigrationContractArbitrum4.sol +478 -0
  24. package/script/MigrationContractBase1.sol +444 -0
  25. package/script/MigrationContractBase2.sol +175 -0
  26. package/script/MigrationContractBase3.sol +309 -0
  27. package/script/MigrationContractBase4.sol +350 -0
  28. package/script/MigrationContractBase5.sol +259 -0
  29. package/script/MigrationContractEthereum1.sol +468 -0
  30. package/script/MigrationContractEthereum2.sol +306 -0
  31. package/script/MigrationContractEthereum3.sol +349 -0
  32. package/script/MigrationContractEthereum4.sol +352 -0
  33. package/script/MigrationContractEthereum5.sol +354 -0
  34. package/script/MigrationContractEthereum6.sol +270 -0
  35. package/script/MigrationContractEthereum7.sol +439 -0
  36. package/script/MigrationContractEthereum8.sol +385 -0
  37. package/script/MigrationContractOptimism.sol +196 -0
  38. package/script/helpers/BannyverseDeploymentLib.sol +73 -0
  39. package/script/helpers/MigrationHelper.sol +155 -0
  40. package/script/outfit_drop/generate-migration.js +3441 -0
  41. package/script/outfit_drop/raw.json +43276 -0
  42. package/slither-ci.config.json +10 -0
  43. package/sphinx.lock +521 -0
  44. package/src/Banny721TokenUriResolver.sol +1288 -0
  45. package/src/interfaces/IBanny721TokenUriResolver.sol +137 -0
  46. package/test/Banny721TokenUriResolver.t.sol +669 -0
  47. package/test/BannyAttacks.t.sol +322 -0
  48. package/test/DecorateFlow.t.sol +1056 -0
@@ -0,0 +1,3441 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const createKeccakHash = require('keccak');
6
+
7
+ function toChecksumAddress(address) {
8
+ if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) {
9
+ // Not a valid address format, return as is
10
+ return address;
11
+ }
12
+ address = address.toLowerCase().replace('0x', '');
13
+ const hash = createKeccakHash('keccak256').update(address).digest('hex');
14
+ let checksumAddress = '0x';
15
+
16
+ for (let i = 0; i < address.length; i++) {
17
+ // If the i-th hex character is greater than 7, use the uppercase char, otherwise lowercase
18
+ checksumAddress += parseInt(hash[i], 16) >= 8 ? address[i].toUpperCase() : address[i];
19
+ }
20
+
21
+ return checksumAddress;
22
+ }
23
+
24
+ // V4 to V5 migration script generator
25
+ // Generates contract-based migration scripts and chain-specific contracts from raw.json
26
+ // Note: Outfit IDs are generated automatically when minting in V5
27
+ // The system handles V4 to V5 category mapping internally
28
+
29
+ function generateMigrationScript() {
30
+ // Generate contract-based migration script and chain-specific contracts from raw.json
31
+ generateScriptForFile('raw.json', 'AirdropOutfits.s.sol');
32
+
33
+ // Generate batch scripts for debugging
34
+ generateBatchScripts('raw.json');
35
+
36
+ // Generate chain-specific migration contracts
37
+ generateChainSpecificContracts('raw.json');
38
+ }
39
+
40
+ function generateScriptForFile(inputFile, outputFile) {
41
+ console.log(`\n=== Generating ${outputFile} from ${inputFile} ===`);
42
+
43
+ // Load the raw data
44
+ const rawDataPath = path.join(__dirname, inputFile);
45
+ const rawData = JSON.parse(fs.readFileSync(rawDataPath, 'utf8'));
46
+
47
+ const items = rawData.data.nfts.items;
48
+
49
+ // Calculate tier IDs for each chain and chunks
50
+ const ethereumItems = items.filter(item => item.chainId === 1);
51
+ const optimismItems = items.filter(item => item.chainId === 10);
52
+ const baseItems = items.filter(item => item.chainId === 8453);
53
+ const arbitrumItems = items.filter(item => item.chainId === 42161);
54
+
55
+ // Calculate chunk-specific tier IDs for Ethereum (6 chunks), Base (4 chunks), and Arbitrum (3 chunks)
56
+ // Increased from 5/3 to 6/4 due to smaller BATCH_SIZE (100 vs 150)
57
+ const ethereumChunks = splitBanniesIntoChunks(ethereumItems, 6);
58
+ const baseChunks = splitBanniesIntoChunks(baseItems, 4);
59
+ const arbitrumChunks = splitBanniesIntoChunks(arbitrumItems, 3);
60
+
61
+ const ethereumTierIds = [];
62
+ const ethereumChunkTierIds = [];
63
+ ethereumChunks.forEach((chunk, index) => {
64
+ const tierIdQuantities = new Map();
65
+ chunk.allItems.forEach(item => {
66
+ const upc = item.metadata.upc;
67
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
68
+ });
69
+ const uniqueUpcs = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
70
+ const tierIds = [];
71
+ uniqueUpcs.forEach(upc => {
72
+ const quantity = tierIdQuantities.get(upc);
73
+ for (let i = 0; i < quantity; i++) {
74
+ tierIds.push(upc);
75
+ }
76
+ });
77
+ ethereumChunkTierIds.push(tierIds);
78
+ if (index === 0) ethereumTierIds.push(...tierIds);
79
+ });
80
+
81
+ // Collect all token IDs already processed in Ethereum chunks 1-6
82
+ // These should NOT be included in MigrationContractEthereum7
83
+ const ethereumProcessedTokenIds = new Set();
84
+ ethereumChunks.forEach(chunk => {
85
+ chunk.allItems.forEach(item => {
86
+ const tokenId = item.metadata.tokenId;
87
+ ethereumProcessedTokenIds.add(Number(tokenId));
88
+ });
89
+ });
90
+
91
+ // Calculate UPC counts from CHUNKS ONLY (not all items) to determine starting unit numbers
92
+ // This tells us how many tokens of each UPC were already minted in previous chunks
93
+ const ethereumUpcCountsFromChunks = new Map();
94
+ ethereumChunks.forEach(chunk => {
95
+ chunk.allItems.forEach(item => {
96
+ const upc = item.metadata.upc;
97
+ ethereumUpcCountsFromChunks.set(upc, (ethereumUpcCountsFromChunks.get(upc) || 0) + 1);
98
+ });
99
+ });
100
+ // Convert counts to starting unit numbers (1-indexed, so add 1)
101
+ const ethereumUpcStartingUnitNumbers = new Map();
102
+ ethereumUpcCountsFromChunks.forEach((count, upc) => {
103
+ ethereumUpcStartingUnitNumbers.set(upc, count + 1);
104
+ });
105
+ // Calculate unused assets tier IDs for Ethereum (split into 2 chunks)
106
+ const ethereumUnusedData = generateUnusedAssetsContract({ id: 1, name: 'Ethereum', numChunks: 6 }, ethereumItems, ethereumUpcStartingUnitNumbers, ethereumProcessedTokenIds);
107
+ let ethereumUnusedTierIds7 = [];
108
+ let ethereumUnusedTierIds8 = [];
109
+ if (ethereumUnusedData && ethereumUnusedData.unusedItems.length > 0) {
110
+ const allUnusedTierIds = [];
111
+ ethereumUnusedData.unusedItems.forEach(item => {
112
+ allUnusedTierIds.push(item.upc);
113
+ });
114
+ // Split into two chunks
115
+ const midPoint = Math.ceil(allUnusedTierIds.length / 2);
116
+ ethereumUnusedTierIds7 = allUnusedTierIds.slice(0, midPoint);
117
+ ethereumUnusedTierIds8 = allUnusedTierIds.slice(midPoint);
118
+ ethereumChunkTierIds.push(ethereumUnusedTierIds7);
119
+ if (ethereumUnusedTierIds8.length > 0) {
120
+ ethereumChunkTierIds.push(ethereumUnusedTierIds8);
121
+ }
122
+ }
123
+
124
+ const baseTierIds = [];
125
+ const baseChunkTierIds = [];
126
+ baseChunks.forEach((chunk, index) => {
127
+ const tierIdQuantities = new Map();
128
+ chunk.allItems.forEach(item => {
129
+ const upc = item.metadata.upc;
130
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
131
+ });
132
+ const uniqueUpcs = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
133
+ const tierIds = [];
134
+ uniqueUpcs.forEach(upc => {
135
+ const quantity = tierIdQuantities.get(upc);
136
+ for (let i = 0; i < quantity; i++) {
137
+ tierIds.push(upc);
138
+ }
139
+ });
140
+ baseChunkTierIds.push(tierIds);
141
+ if (index === 0) baseTierIds.push(...tierIds);
142
+ });
143
+
144
+ // Collect all token IDs already processed in Base chunks 1-4
145
+ // These should NOT be included in MigrationContractBase5
146
+ const baseProcessedTokenIds = new Set();
147
+ baseChunks.forEach(chunk => {
148
+ chunk.allItems.forEach(item => {
149
+ const tokenId = item.metadata.tokenId;
150
+ baseProcessedTokenIds.add(Number(tokenId));
151
+ });
152
+ });
153
+
154
+ // Calculate UPC counts from CHUNKS ONLY (not all items) to determine starting unit numbers
155
+ // This tells us how many tokens of each UPC were already minted in previous chunks
156
+ const baseUpcCountsFromChunks = new Map();
157
+ baseChunks.forEach(chunk => {
158
+ chunk.allItems.forEach(item => {
159
+ const upc = item.metadata.upc;
160
+ baseUpcCountsFromChunks.set(upc, (baseUpcCountsFromChunks.get(upc) || 0) + 1);
161
+ });
162
+ });
163
+ // Convert counts to starting unit numbers (1-indexed, so add 1)
164
+ const baseUpcStartingUnitNumbers = new Map();
165
+ baseUpcCountsFromChunks.forEach((count, upc) => {
166
+ baseUpcStartingUnitNumbers.set(upc, count + 1);
167
+ });
168
+ // Calculate unused assets tier IDs for Base
169
+ const baseUnusedData = generateUnusedAssetsContract({ id: 8453, name: 'Base', numChunks: 4 }, baseItems, baseUpcStartingUnitNumbers, baseProcessedTokenIds);
170
+ let baseUnusedTierIds = [];
171
+ if (baseUnusedData && baseUnusedData.unusedItems && baseUnusedData.unusedItems.length > 0) {
172
+ baseUnusedData.unusedItems.forEach(item => {
173
+ baseUnusedTierIds.push(item.upc);
174
+ });
175
+ baseChunkTierIds.push(baseUnusedTierIds);
176
+ console.log(`Added Base unused chunk with ${baseUnusedTierIds.length} tier IDs`);
177
+ } else {
178
+ console.log(`No Base unused items found (unusedData: ${!!baseUnusedData}, unusedItems: ${baseUnusedData?.unusedItems?.length || 0})`);
179
+ }
180
+
181
+ // Build tier ID arrays for single-contract chains
182
+ const optimismTierIds = [];
183
+
184
+ // Optimism - single contract
185
+ {
186
+ const tierIdQuantities = new Map();
187
+ optimismItems.forEach(item => {
188
+ const upc = item.metadata.upc;
189
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
190
+ });
191
+
192
+ const uniqueUpcs = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
193
+ uniqueUpcs.forEach(upc => {
194
+ const quantity = tierIdQuantities.get(upc);
195
+ for (let i = 0; i < quantity; i++) {
196
+ optimismTierIds.push(upc);
197
+ }
198
+ });
199
+ }
200
+
201
+ // Arbitrum - 3 chunks (similar to Base)
202
+ const arbitrumChunkTierIds = [];
203
+ arbitrumChunks.forEach((chunk) => {
204
+ const tierIdQuantities = new Map();
205
+ chunk.allItems.forEach(item => {
206
+ const upc = item.metadata.upc;
207
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
208
+ });
209
+ const uniqueUpcs = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
210
+ const tierIds = [];
211
+ uniqueUpcs.forEach(upc => {
212
+ const quantity = tierIdQuantities.get(upc);
213
+ for (let i = 0; i < quantity; i++) {
214
+ tierIds.push(upc);
215
+ }
216
+ });
217
+ arbitrumChunkTierIds.push(tierIds);
218
+ });
219
+
220
+ // Generate the contract-based migration script
221
+ const script = generateContractVersion(items, {
222
+ ethereumTierIds,
223
+ ethereumChunkTierIds,
224
+ optimismTierIds,
225
+ baseTierIds,
226
+ baseChunkTierIds,
227
+ arbitrumChunkTierIds
228
+ });
229
+
230
+ // Write the script to file
231
+ const outputPath = path.join(__dirname, '..', outputFile);
232
+ fs.writeFileSync(outputPath, script);
233
+
234
+ console.log(`Generated migration script with chain-specific filtering`);
235
+ console.log(`Script written to: ${outputPath}`);
236
+ }
237
+
238
+ function generateBatchScripts(inputFile) {
239
+ console.log(`\n=== Generating batch scripts from ${inputFile} ===`);
240
+
241
+ // Load the raw data
242
+ const rawDataPath = path.join(__dirname, inputFile);
243
+ const rawData = JSON.parse(fs.readFileSync(rawDataPath, 'utf8'));
244
+
245
+ const items = rawData.data.nfts.items;
246
+
247
+ // Calculate tier IDs for each chain and chunks
248
+ const ethereumItems = items.filter(item => item.chainId === 1);
249
+ const optimismItems = items.filter(item => item.chainId === 10);
250
+ const baseItems = items.filter(item => item.chainId === 8453);
251
+ const arbitrumItems = items.filter(item => item.chainId === 42161);
252
+
253
+ // Calculate chunk-specific tier IDs for Ethereum (6 chunks), Base (4 chunks), and Arbitrum (3 chunks)
254
+ // Increased from 5/3 to 6/4 due to smaller BATCH_SIZE (100 vs 150)
255
+ const ethereumChunks = splitBanniesIntoChunks(ethereumItems, 6);
256
+ const baseChunks = splitBanniesIntoChunks(baseItems, 4);
257
+ const arbitrumChunks = splitBanniesIntoChunks(arbitrumItems, 3);
258
+
259
+ // Calculate tier IDs for each chunk
260
+ const ethereumChunkTierIds = [];
261
+ ethereumChunks.forEach((chunk) => {
262
+ const tierIdQuantities = new Map();
263
+ chunk.allItems.forEach(item => {
264
+ const upc = item.metadata.upc;
265
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
266
+ });
267
+ const uniqueUpcs = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
268
+ const tierIds = [];
269
+ uniqueUpcs.forEach(upc => {
270
+ const quantity = tierIdQuantities.get(upc);
271
+ for (let i = 0; i < quantity; i++) {
272
+ tierIds.push(upc);
273
+ }
274
+ });
275
+ ethereumChunkTierIds.push(tierIds);
276
+ });
277
+
278
+ // Calculate unused assets for Ethereum (chunk 4)
279
+ const ethereumProcessedTokenIds = new Set();
280
+ ethereumChunks.forEach(chunk => {
281
+ chunk.allItems.forEach(item => {
282
+ const tokenId = item.metadata.tokenId;
283
+ ethereumProcessedTokenIds.add(Number(tokenId));
284
+ });
285
+ });
286
+
287
+ const ethereumUpcCountsFromChunks = new Map();
288
+ ethereumChunks.forEach(chunk => {
289
+ chunk.allItems.forEach(item => {
290
+ const upc = item.metadata.upc;
291
+ ethereumUpcCountsFromChunks.set(upc, (ethereumUpcCountsFromChunks.get(upc) || 0) + 1);
292
+ });
293
+ });
294
+ const ethereumUpcStartingUnitNumbers = new Map();
295
+ ethereumUpcCountsFromChunks.forEach((count, upc) => {
296
+ ethereumUpcStartingUnitNumbers.set(upc, count + 1);
297
+ });
298
+
299
+ const ethereumUnusedContractData = generateUnusedAssetsContract(
300
+ { id: 1, name: 'Ethereum', numChunks: 6 },
301
+ ethereumItems,
302
+ ethereumUpcStartingUnitNumbers,
303
+ ethereumProcessedTokenIds
304
+ );
305
+
306
+ let ethereumChunk7TierIds = [];
307
+ let ethereumChunk8TierIds = [];
308
+ if (ethereumUnusedContractData && ethereumUnusedContractData.unusedItems.length > 0) {
309
+ const tierIdQuantities = new Map();
310
+ ethereumUnusedContractData.unusedItems.forEach(item => {
311
+ const upc = item.upc;
312
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
313
+ });
314
+ const uniqueUpcs = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
315
+ const allTierIds = [];
316
+ uniqueUpcs.forEach(upc => {
317
+ const quantity = tierIdQuantities.get(upc);
318
+ for (let i = 0; i < quantity; i++) {
319
+ allTierIds.push(upc);
320
+ }
321
+ });
322
+
323
+ // Split into two chunks
324
+ const midPoint = Math.ceil(allTierIds.length / 2);
325
+ ethereumChunk7TierIds = allTierIds.slice(0, midPoint);
326
+ ethereumChunk8TierIds = allTierIds.slice(midPoint);
327
+ }
328
+
329
+ const baseChunkTierIds = [];
330
+ baseChunks.forEach((chunk) => {
331
+ const tierIdQuantities = new Map();
332
+ chunk.allItems.forEach(item => {
333
+ const upc = item.metadata.upc;
334
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
335
+ });
336
+ const uniqueUpcs = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
337
+ const tierIds = [];
338
+ uniqueUpcs.forEach(upc => {
339
+ const quantity = tierIdQuantities.get(upc);
340
+ for (let i = 0; i < quantity; i++) {
341
+ tierIds.push(upc);
342
+ }
343
+ });
344
+ baseChunkTierIds.push(tierIds);
345
+ });
346
+
347
+ // Calculate unused assets for Base (chunk 3)
348
+ const baseProcessedTokenIds = new Set();
349
+ baseChunks.forEach(chunk => {
350
+ chunk.allItems.forEach(item => {
351
+ const tokenId = item.metadata.tokenId;
352
+ baseProcessedTokenIds.add(Number(tokenId));
353
+ });
354
+ });
355
+
356
+ const baseUpcCountsFromChunks = new Map();
357
+ baseChunks.forEach(chunk => {
358
+ chunk.allItems.forEach(item => {
359
+ const upc = item.metadata.upc;
360
+ baseUpcCountsFromChunks.set(upc, (baseUpcCountsFromChunks.get(upc) || 0) + 1);
361
+ });
362
+ });
363
+ const baseUpcStartingUnitNumbers = new Map();
364
+ baseUpcCountsFromChunks.forEach((count, upc) => {
365
+ baseUpcStartingUnitNumbers.set(upc, count + 1);
366
+ });
367
+
368
+ const baseUnusedContractData = generateUnusedAssetsContract(
369
+ { id: 8453, name: 'Base', numChunks: 4 },
370
+ baseItems,
371
+ baseUpcStartingUnitNumbers,
372
+ baseProcessedTokenIds
373
+ );
374
+
375
+ let baseChunk3TierIds = null;
376
+ if (baseUnusedContractData && baseUnusedContractData.unusedItems.length > 0) {
377
+ const tierIdQuantities = new Map();
378
+ baseUnusedContractData.unusedItems.forEach(item => {
379
+ const upc = item.upc;
380
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
381
+ });
382
+ const uniqueUpcs = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
383
+ baseChunk3TierIds = [];
384
+ uniqueUpcs.forEach(upc => {
385
+ const quantity = tierIdQuantities.get(upc);
386
+ for (let i = 0; i < quantity; i++) {
387
+ baseChunk3TierIds.push(upc);
388
+ }
389
+ });
390
+ }
391
+
392
+ // Calculate Optimism tier IDs
393
+ const optimismTierIdQuantities = new Map();
394
+ optimismItems.forEach(item => {
395
+ const upc = item.metadata.upc;
396
+ optimismTierIdQuantities.set(upc, (optimismTierIdQuantities.get(upc) || 0) + 1);
397
+ });
398
+ const optimismUniqueUpcs = Array.from(optimismTierIdQuantities.keys()).sort((a, b) => a - b);
399
+ const optimismTierIds = [];
400
+ optimismUniqueUpcs.forEach(upc => {
401
+ const quantity = optimismTierIdQuantities.get(upc);
402
+ for (let i = 0; i < quantity; i++) {
403
+ optimismTierIds.push(upc);
404
+ }
405
+ });
406
+
407
+ // Calculate Arbitrum chunk tier IDs (3 chunks)
408
+ const arbitrumChunkTierIds = [];
409
+ arbitrumChunks.forEach((chunk) => {
410
+ const tierIdQuantities = new Map();
411
+ chunk.allItems.forEach(item => {
412
+ const upc = item.metadata.upc;
413
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
414
+ });
415
+ const uniqueUpcs = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
416
+ const tierIds = [];
417
+ uniqueUpcs.forEach(upc => {
418
+ const quantity = tierIdQuantities.get(upc);
419
+ for (let i = 0; i < quantity; i++) {
420
+ tierIds.push(upc);
421
+ }
422
+ });
423
+ arbitrumChunkTierIds.push(tierIds);
424
+ });
425
+
426
+ // Calculate unused assets for Arbitrum (chunk 4)
427
+ const arbitrumProcessedTokenIds = new Set();
428
+ arbitrumChunks.forEach(chunk => {
429
+ chunk.allItems.forEach(item => {
430
+ const tokenId = item.metadata.tokenId;
431
+ arbitrumProcessedTokenIds.add(Number(tokenId));
432
+ });
433
+ });
434
+
435
+ const arbitrumUpcCountsFromChunks = new Map();
436
+ arbitrumChunks.forEach(chunk => {
437
+ chunk.allItems.forEach(item => {
438
+ const upc = item.metadata.upc;
439
+ arbitrumUpcCountsFromChunks.set(upc, (arbitrumUpcCountsFromChunks.get(upc) || 0) + 1);
440
+ });
441
+ });
442
+ const arbitrumUpcStartingUnitNumbers = new Map();
443
+ arbitrumUpcCountsFromChunks.forEach((count, upc) => {
444
+ arbitrumUpcStartingUnitNumbers.set(upc, count + 1);
445
+ });
446
+
447
+ const arbitrumUnusedContractData = generateUnusedAssetsContract(
448
+ { id: 42161, name: 'Arbitrum', numChunks: 3 },
449
+ arbitrumItems,
450
+ arbitrumUpcStartingUnitNumbers,
451
+ arbitrumProcessedTokenIds
452
+ );
453
+
454
+ let arbitrumChunk4TierIds = null;
455
+ if (arbitrumUnusedContractData && arbitrumUnusedContractData.unusedItems.length > 0) {
456
+ const tierIdQuantities = new Map();
457
+ arbitrumUnusedContractData.unusedItems.forEach(item => {
458
+ const upc = item.upc;
459
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
460
+ });
461
+ const uniqueUpcs = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
462
+ arbitrumChunk4TierIds = [];
463
+ uniqueUpcs.forEach(upc => {
464
+ const quantity = tierIdQuantities.get(upc);
465
+ for (let i = 0; i < quantity; i++) {
466
+ arbitrumChunk4TierIds.push(upc);
467
+ }
468
+ });
469
+ arbitrumChunkTierIds.push(arbitrumChunk4TierIds);
470
+ console.log(`Added Arbitrum unused chunk with ${arbitrumChunk4TierIds.length} tier IDs`);
471
+ } else {
472
+ console.log(`No Arbitrum unused items found (unusedData: ${!!arbitrumUnusedContractData}, unusedItems: ${arbitrumUnusedContractData?.unusedItems?.length || 0})`);
473
+ }
474
+
475
+ // Build transfer data functions for all batches
476
+ const tierIds = {
477
+ ethereumChunkTierIds: [...ethereumChunkTierIds, ethereumChunk7TierIds, ethereumChunk8TierIds],
478
+ baseChunkTierIds,
479
+ optimismTierIds,
480
+ arbitrumChunkTierIds
481
+ };
482
+
483
+ // Generate transfer data functions (reuse from generateContractVersion logic)
484
+ const chains = [
485
+ { id: 1, name: 'Ethereum', numChunks: 6 },
486
+ { id: 10, name: 'Optimism', numChunks: 1 },
487
+ { id: 8453, name: 'Base', numChunks: 4 },
488
+ { id: 42161, name: 'Arbitrum', numChunks: 3 }
489
+ ];
490
+
491
+ let allTransferDataFunctions = '';
492
+
493
+ chains.forEach(chain => {
494
+ const chainItems = items.filter(item => item.chainId === chain.id);
495
+ if (chainItems.length === 0) return;
496
+
497
+ if (chain.numChunks > 1) {
498
+ const chunks = splitBanniesIntoChunks(chainItems, chain.numChunks);
499
+ chunks.forEach((chunk, chunkIndex) => {
500
+ const transferData = chunk.transferData;
501
+ allTransferDataFunctions += `
502
+ function _get${chain.name}TransferOwners${chunkIndex + 1}() internal pure returns (address[] memory) {
503
+ address[] memory transferOwners = new address[](${transferData.length});
504
+ `;
505
+
506
+ transferData.forEach((data, index) => {
507
+ allTransferDataFunctions += `
508
+ transferOwners[${index}] = ${toChecksumAddress(data.owner)};`;
509
+ });
510
+
511
+ allTransferDataFunctions += `
512
+ return transferOwners;
513
+ }
514
+ `;
515
+ });
516
+
517
+ // Generate transfer data function for unused assets (Ethereum, Base, and Arbitrum)
518
+ if (chain.id === 1 || chain.id === 8453 || chain.id === 42161) {
519
+ const processedTokenIds = new Set();
520
+ chunks.forEach(chunk => {
521
+ chunk.allItems.forEach(item => {
522
+ const tokenId = item.metadata.tokenId;
523
+ processedTokenIds.add(Number(tokenId));
524
+ });
525
+ });
526
+
527
+ const upcCountsFromChunks = new Map();
528
+ chunks.forEach(chunk => {
529
+ chunk.allItems.forEach(item => {
530
+ const upc = item.metadata.upc;
531
+ upcCountsFromChunks.set(upc, (upcCountsFromChunks.get(upc) || 0) + 1);
532
+ });
533
+ });
534
+ const upcStartingUnitNumbers = new Map();
535
+ upcCountsFromChunks.forEach((count, upc) => {
536
+ upcStartingUnitNumbers.set(upc, count + 1);
537
+ });
538
+
539
+ const unusedContractData = generateUnusedAssetsContract(chain, chainItems, upcStartingUnitNumbers, processedTokenIds);
540
+ if (unusedContractData && unusedContractData.unusedItems.length > 0) {
541
+ const chunkIndex = chain.numChunks;
542
+ allTransferDataFunctions += `
543
+ function _get${chain.name}TransferOwners${chunkIndex + 1}() internal pure returns (address[] memory) {
544
+ address[] memory transferOwners = new address[](${unusedContractData.unusedItems.length});
545
+ `;
546
+
547
+ unusedContractData.unusedItems.forEach((item, index) => {
548
+ allTransferDataFunctions += `
549
+ transferOwners[${index}] = ${toChecksumAddress(item.owner)};`;
550
+ });
551
+
552
+ allTransferDataFunctions += `
553
+ return transferOwners;
554
+ }
555
+ `;
556
+ }
557
+ }
558
+ } else {
559
+ const transferData = buildTransferDataForChain(chainItems);
560
+
561
+ allTransferDataFunctions += `
562
+ function _get${chain.name}TransferOwners() internal pure returns (address[] memory) {
563
+ address[] memory transferOwners = new address[](${transferData.length});
564
+ `;
565
+
566
+ transferData.forEach((data, index) => {
567
+ allTransferDataFunctions += `
568
+ transferOwners[${index}] = ${toChecksumAddress(data.owner)};`;
569
+ });
570
+
571
+ allTransferDataFunctions += `
572
+ return transferOwners;
573
+ }
574
+ `;
575
+ }
576
+ });
577
+
578
+ // Generate Batch 1 script (Ethereum 1, Optimism, Base 1, Arbitrum 1)
579
+ generateBatchScript(1, {
580
+ ethereum: ethereumChunkTierIds[0],
581
+ optimism: optimismTierIds,
582
+ base: baseChunkTierIds[0],
583
+ arbitrum: arbitrumChunkTierIds[0]
584
+ }, allTransferDataFunctions, items);
585
+
586
+ // Generate Batch 2 script (Ethereum 2, Base 2, Arbitrum 2)
587
+ generateBatchScript(2, {
588
+ ethereum: ethereumChunkTierIds[1],
589
+ base: baseChunkTierIds[1],
590
+ arbitrum: arbitrumChunkTierIds[1]
591
+ }, allTransferDataFunctions, items);
592
+
593
+ // Generate Batch 3 script (Ethereum 3, Base 3, Arbitrum 3)
594
+ generateBatchScript(3, {
595
+ ethereum: ethereumChunkTierIds[2],
596
+ base: baseChunkTierIds[2],
597
+ arbitrum: arbitrumChunkTierIds[2]
598
+ }, allTransferDataFunctions, items);
599
+
600
+ // Generate Batch 4 script (Ethereum 4, Base 4, Arbitrum 4 - unused assets)
601
+ const batch4TierIds = {
602
+ ethereum: ethereumChunkTierIds[3],
603
+ base: baseChunkTierIds[3]
604
+ };
605
+ // Add Arbitrum 4 (unused assets) if it exists
606
+ if (arbitrumChunkTierIds.length > 3 && arbitrumChunkTierIds[3] && arbitrumChunkTierIds[3].length > 0) {
607
+ batch4TierIds.arbitrum = arbitrumChunkTierIds[3];
608
+ }
609
+ generateBatchScript(4, batch4TierIds, allTransferDataFunctions, items);
610
+
611
+ // Generate Batch 5 script (Ethereum 5, Base 5 - unused assets)
612
+ const batch5TierIds = {
613
+ ethereum: ethereumChunkTierIds[4]
614
+ };
615
+ if (baseChunk3TierIds && baseChunk3TierIds.length > 0) {
616
+ batch5TierIds.base = baseChunk3TierIds;
617
+ }
618
+ generateBatchScript(5, batch5TierIds, allTransferDataFunctions, items);
619
+
620
+ // Generate Batch 6 script (Ethereum 6)
621
+ generateBatchScript(6, {
622
+ ethereum: ethereumChunkTierIds[5]
623
+ }, allTransferDataFunctions, items);
624
+
625
+ // Generate Batch 7 script (Ethereum 7 - unused assets part 1)
626
+ if (ethereumChunk7TierIds.length > 0) {
627
+ generateBatchScript(7, {
628
+ ethereum: ethereumChunk7TierIds
629
+ }, allTransferDataFunctions, items);
630
+ }
631
+
632
+ // Generate Batch 8 script (Ethereum 8 - unused assets part 2)
633
+ if (ethereumChunk8TierIds.length > 0) {
634
+ generateBatchScript(8, {
635
+ ethereum: ethereumChunk8TierIds
636
+ }, allTransferDataFunctions, items);
637
+ }
638
+ }
639
+
640
+ function generateBatchScript(batchNumber, tierIds, transferDataFunctions, items) {
641
+ const scriptDir = path.join(__dirname, '..');
642
+ const outputPath = path.join(scriptDir, `AirdropOutfitsBatch${batchNumber}.s.sol`);
643
+
644
+ // Determine which chains this batch handles
645
+ const hasEthereum = tierIds.ethereum && tierIds.ethereum.length > 0;
646
+ const hasOptimism = tierIds.optimism && tierIds.optimism.length > 0;
647
+ const hasBase = tierIds.base && tierIds.base.length > 0;
648
+ const hasArbitrum = tierIds.arbitrum && tierIds.arbitrum.length > 0;
649
+
650
+ // Determine which imports are needed
651
+ let imports = '';
652
+ if (hasEthereum) {
653
+ // Ethereum batches 1-6 map to contracts 1-6, batch 7 (unused assets part 1) maps to contract 7, batch 8 (unused assets part 2) maps to contract 8
654
+ const ethereumContractNum = (batchNumber === 7 || batchNumber === 8) ? batchNumber : batchNumber;
655
+ imports += `import {MigrationContractEthereum${ethereumContractNum}} from "./MigrationContractEthereum${ethereumContractNum}.sol";\n`;
656
+ }
657
+ if (hasOptimism) {
658
+ imports += `import {MigrationContractOptimism} from "./MigrationContractOptimism.sol";\n`;
659
+ }
660
+ if (hasBase) {
661
+ // Base batches 1-4 map to contracts 1-4, batch 5 (unused assets) maps to contract 5
662
+ const baseContractNum = batchNumber === 5 ? 5 : batchNumber;
663
+ imports += `import {MigrationContractBase${baseContractNum}} from "./MigrationContractBase${baseContractNum}.sol";\n`;
664
+ }
665
+ if (hasArbitrum) {
666
+ // Arbitrum batches 1-3 map to contracts 1-3, batch 4 (unused assets) maps to contract 4
667
+ const arbitrumContractNum = batchNumber === 4 ? 4 : batchNumber;
668
+ imports += `import {MigrationContractArbitrum${arbitrumContractNum}} from "./MigrationContractArbitrum${arbitrumContractNum}.sol";\n`;
669
+ }
670
+
671
+ // Generate run() function with only relevant chains
672
+ let runFunction = ` function run() public sphinx {
673
+ uint256 chainId = block.chainid;
674
+
675
+ `;
676
+ if (hasEthereum) {
677
+ runFunction += `if (chainId == 1) {
678
+ // Ethereum Mainnet
679
+ _runEthereum();
680
+ } else `;
681
+ }
682
+ if (hasOptimism) {
683
+ runFunction += `if (chainId == 10) {
684
+ // Optimism
685
+ _runOptimism();
686
+ } else `;
687
+ }
688
+ if (hasBase) {
689
+ runFunction += `if (chainId == 8453) {
690
+ // Base
691
+ _runBase();
692
+ } else `;
693
+ }
694
+ if (hasArbitrum) {
695
+ runFunction += `if (chainId == 42161) {
696
+ // Arbitrum
697
+ _runArbitrum();
698
+ } else `;
699
+ }
700
+ runFunction += `{
701
+ revert("Unsupported chain for batch ${batchNumber}");
702
+ }
703
+ }`;
704
+
705
+ // Generate chain-specific run functions
706
+ let chainRunFunctions = '';
707
+ if (hasEthereum) {
708
+ chainRunFunctions += `
709
+ function _runEthereum() internal {
710
+ address hookAddress = ${toChecksumAddress('0xb4Ec363c2E7DB0cECA9AA1759338d7d1b49d1750')};
711
+ address resolverAddress = ${toChecksumAddress('0x47c011146a4498a70e0bf2e4585acf9cade85954')};
712
+ address v4HookAddress = ${toChecksumAddress('0x2da41cdc79ae49f2725ab549717b2dbcfc42b958')};
713
+ address v4ResolverAddress = ${toChecksumAddress('0xa5f8911d4cfd60a6697479f078409434424fe666')};
714
+ address terminalAddress = ${toChecksumAddress('0x2db6d704058e552defe415753465df8df0361846')};
715
+ address v4ResolverFallback = ${toChecksumAddress('0xfF80c37a57016EFf3d19fb286e9C740eC4537Dd3')};
716
+ _processMigration(
717
+ hookAddress,
718
+ resolverAddress,
719
+ v4HookAddress,
720
+ v4ResolverAddress,
721
+ terminalAddress,
722
+ v4ResolverFallback,
723
+ 1
724
+ );
725
+ }
726
+
727
+ `;
728
+ }
729
+ if (hasOptimism) {
730
+ chainRunFunctions += `
731
+ function _runOptimism() internal {
732
+ address hookAddress = ${toChecksumAddress('0xb4Ec363c2E7DB0cECA9AA1759338d7d1b49d1750')};
733
+ address resolverAddress = ${toChecksumAddress('0x47c011146a4498a70e0bf2e4585acf9cade85954')};
734
+ address v4HookAddress = ${toChecksumAddress('0x2da41cdc79ae49f2725ab549717b2dbcfc42b958')};
735
+ address v4ResolverAddress = ${toChecksumAddress('0xa5f8911d4cfd60a6697479f078409434424fe666')};
736
+ address terminalAddress = ${toChecksumAddress('0x2db6d704058e552defe415753465df8df0361846')};
737
+ address v4ResolverFallback = ${toChecksumAddress('0xfF80c37a57016EFf3d19fb286e9C740eC4537Dd3')};
738
+ _processMigration(
739
+ hookAddress,
740
+ resolverAddress,
741
+ v4HookAddress,
742
+ v4ResolverAddress,
743
+ terminalAddress,
744
+ v4ResolverFallback,
745
+ 10
746
+ );
747
+ }
748
+
749
+ `;
750
+ }
751
+ if (hasBase) {
752
+ chainRunFunctions += `
753
+ function _runBase() internal {
754
+ address hookAddress = ${toChecksumAddress('0xb4Ec363c2E7DB0cECA9AA1759338d7d1b49d1750')};
755
+ address resolverAddress = ${toChecksumAddress('0x47c011146a4498a70e0bf2e4585acf9cade85954')};
756
+ address v4HookAddress = ${toChecksumAddress('0x2da41cdc79ae49f2725ab549717b2dbcfc42b958')};
757
+ address v4ResolverAddress = ${toChecksumAddress('0xa5f8911d4cfd60a6697479f078409434424fe666')};
758
+ address terminalAddress = ${toChecksumAddress('0x2db6d704058e552defe415753465df8df0361846')};
759
+ address v4ResolverFallback = ${toChecksumAddress('0xfF80c37a57016EFf3d19fb286e9C740eC4537Dd3')};
760
+ _processMigration(
761
+ hookAddress,
762
+ resolverAddress,
763
+ v4HookAddress,
764
+ v4ResolverAddress,
765
+ terminalAddress,
766
+ v4ResolverFallback,
767
+ 8453
768
+ );
769
+ }
770
+
771
+ `;
772
+ }
773
+ if (hasArbitrum) {
774
+ chainRunFunctions += `
775
+ function _runArbitrum() internal {
776
+ address hookAddress = ${toChecksumAddress('0xb4Ec363c2E7DB0cECA9AA1759338d7d1b49d1750')};
777
+ address resolverAddress = ${toChecksumAddress('0x47c011146a4498a70e0bf2e4585acf9cade85954')};
778
+ address v4HookAddress = ${toChecksumAddress('0x2da41cdc79ae49f2725ab549717b2dbcfc42b958')};
779
+ address v4ResolverAddress = ${toChecksumAddress('0xa5f8911d4cfd60a6697479f078409434424fe666')};
780
+ address terminalAddress = ${toChecksumAddress('0x2db6d704058e552defe415753465df8df0361846')};
781
+ address v4ResolverFallback = ${toChecksumAddress('0xfF80c37a57016EFf3d19fb286e9C740eC4537Dd3')};
782
+ _processMigration(
783
+ hookAddress,
784
+ resolverAddress,
785
+ v4HookAddress,
786
+ v4ResolverAddress,
787
+ terminalAddress,
788
+ v4ResolverFallback,
789
+ 42161
790
+ );
791
+ }
792
+
793
+ `;
794
+ }
795
+
796
+ // Generate _processMigration function with only relevant batches
797
+ let processMigrationFunction = `
798
+ function _processMigration(address hookAddress, address resolverAddress, address v4HookAddress, address v4ResolverAddress, address terminalAddress, address v4ResolverFallback, uint256 chainId) internal {
799
+ // Validate addresses
800
+ require(hookAddress != address(0), "Hook address not set");
801
+ require(resolverAddress != address(0), "Resolver address not set");
802
+ require(v4HookAddress != address(0), "V4 Hook address not set");
803
+ require(v4ResolverAddress != address(0), "V4 Resolver address not set");
804
+ require(terminalAddress != address(0), "Terminal address not set");
805
+
806
+ IJBTerminal terminal = IJBTerminal(terminalAddress);
807
+ JB721TiersHook hook = JB721TiersHook(hookAddress);
808
+
809
+ // Get project ID from hook
810
+ uint256 projectId = hook.PROJECT_ID();
811
+
812
+ // Deploy the appropriate chain-specific migration contract with transfer data
813
+ `;
814
+
815
+ if (hasEthereum) {
816
+ // Ethereum batches 1-6 map to contracts 1-6, batch 7 (unused assets part 1) maps to contract 7, batch 8 (unused assets part 2) maps to contract 8
817
+ const ethereumContractNum = (batchNumber === 7 || batchNumber === 8) ? batchNumber : batchNumber;
818
+ processMigrationFunction += `
819
+ if (chainId == 1) {
820
+ // Ethereum - Batch ${batchNumber} only
821
+ uint16[] memory tierIds${ethereumContractNum} = new uint16[](${tierIds.ethereum.length});
822
+ ${generateTierIdLoops(tierIds.ethereum, `tierIds${ethereumContractNum}`)}
823
+ address[] memory transferOwners${ethereumContractNum} = _getEthereumTransferOwners${ethereumContractNum}();
824
+ MigrationContractEthereum${ethereumContractNum} migrationContract${ethereumContractNum} = new MigrationContractEthereum${ethereumContractNum}(transferOwners${ethereumContractNum});
825
+ console.log("Ethereum migration contract ${ethereumContractNum} deployed at:", address(migrationContract${ethereumContractNum}));
826
+
827
+ // Mint chunk ${ethereumContractNum} assets to the contract address via pay()
828
+ _mintViaPay(
829
+ terminal,
830
+ hook,
831
+ projectId,
832
+ tierIds${ethereumContractNum},
833
+ address(migrationContract${ethereumContractNum})
834
+ );
835
+ console.log("Minted", tierIds${ethereumContractNum}.length, "tokens to contract ${ethereumContractNum}");
836
+
837
+ migrationContract${ethereumContractNum}.executeMigration(hookAddress, resolverAddress, v4HookAddress, v4ResolverAddress, v4ResolverFallback);
838
+
839
+ } else `;
840
+ }
841
+
842
+ if (hasOptimism) {
843
+ processMigrationFunction += `
844
+ if (chainId == 10) {
845
+ // Optimism tier IDs
846
+ uint16[] memory allTierIds = new uint16[](${tierIds.optimism.length});
847
+ ${generateTierIdLoops(tierIds.optimism)}
848
+ address[] memory transferOwners = _getOptimismTransferOwners();
849
+ MigrationContractOptimism migrationContract = new MigrationContractOptimism(transferOwners);
850
+ console.log("Optimism migration contract deployed at:", address(migrationContract));
851
+
852
+ // Mint all assets to the contract address via pay()
853
+ _mintViaPay(
854
+ terminal,
855
+ hook,
856
+ projectId,
857
+ allTierIds,
858
+ address(migrationContract)
859
+ );
860
+ console.log("Minted", allTierIds.length, "tokens to contract");
861
+
862
+ migrationContract.executeMigration(hookAddress, resolverAddress, v4HookAddress, v4ResolverAddress, v4ResolverFallback);
863
+ } else `;
864
+ }
865
+
866
+ if (hasBase) {
867
+ // Base batches 1-4 map to contracts 1-4, batch 5 (unused assets) maps to contract 5
868
+ const baseContractNum = batchNumber === 5 ? 5 : batchNumber;
869
+ processMigrationFunction += `
870
+ if (chainId == 8453) {
871
+ // Base - Batch ${batchNumber} only
872
+ uint16[] memory tierIds${baseContractNum} = new uint16[](${tierIds.base.length});
873
+ ${generateTierIdLoops(tierIds.base, `tierIds${baseContractNum}`)}
874
+ address[] memory transferOwners${baseContractNum} = _getBaseTransferOwners${baseContractNum}();
875
+ MigrationContractBase${baseContractNum} migrationContract${baseContractNum} = new MigrationContractBase${baseContractNum}(transferOwners${baseContractNum});
876
+ console.log("Base migration contract ${baseContractNum} deployed at:", address(migrationContract${baseContractNum}));
877
+
878
+ // Mint chunk ${baseContractNum} assets to the contract address via pay()
879
+ _mintViaPay(
880
+ terminal,
881
+ hook,
882
+ projectId,
883
+ tierIds${baseContractNum},
884
+ address(migrationContract${baseContractNum})
885
+ );
886
+ console.log("Minted", tierIds${baseContractNum}.length, "tokens to contract ${baseContractNum}");
887
+
888
+ migrationContract${baseContractNum}.executeMigration(hookAddress, resolverAddress, v4HookAddress, v4ResolverAddress, v4ResolverFallback);
889
+
890
+ } else `;
891
+ }
892
+
893
+ if (hasArbitrum) {
894
+ // Arbitrum batches 1-3 map to contracts 1-3, batch 4 (unused assets) maps to contract 4
895
+ const arbitrumContractNum = batchNumber === 4 ? 4 : batchNumber;
896
+ processMigrationFunction += `
897
+ if (chainId == 42161) {
898
+ // Arbitrum - Batch ${batchNumber} only
899
+ uint16[] memory tierIds${arbitrumContractNum} = new uint16[](${tierIds.arbitrum.length});
900
+ ${generateTierIdLoops(tierIds.arbitrum, `tierIds${arbitrumContractNum}`)}
901
+ address[] memory transferOwners${arbitrumContractNum} = _getArbitrumTransferOwners${arbitrumContractNum}();
902
+ MigrationContractArbitrum${arbitrumContractNum} migrationContract${arbitrumContractNum} = new MigrationContractArbitrum${arbitrumContractNum}(transferOwners${arbitrumContractNum});
903
+ console.log("Arbitrum migration contract ${arbitrumContractNum} deployed at:", address(migrationContract${arbitrumContractNum}));
904
+
905
+ // Mint chunk ${arbitrumContractNum} assets to the contract address via pay()
906
+ _mintViaPay(
907
+ terminal,
908
+ hook,
909
+ projectId,
910
+ tierIds${arbitrumContractNum},
911
+ address(migrationContract${arbitrumContractNum})
912
+ );
913
+ console.log("Minted", tierIds${arbitrumContractNum}.length, "tokens to contract ${arbitrumContractNum}");
914
+
915
+ migrationContract${arbitrumContractNum}.executeMigration(hookAddress, resolverAddress, v4HookAddress, v4ResolverAddress, v4ResolverFallback);
916
+
917
+ } else `;
918
+ }
919
+
920
+ processMigrationFunction += `
921
+ {
922
+ revert("Unsupported chain for contract deployment");
923
+ }
924
+ }`;
925
+
926
+ // Generate helper functions (same for all batches)
927
+ const helperFunctions = `
928
+ function _mintViaPay(
929
+ IJBTerminal terminal,
930
+ JB721TiersHook hook,
931
+ uint256 projectId,
932
+ uint16[] memory tierIds,
933
+ address beneficiary
934
+ ) internal {
935
+ uint256 totalTierIds = tierIds.length;
936
+
937
+ // Process tier IDs in batches
938
+ for (uint256 i = 0; i < totalTierIds; i += BATCH_SIZE) {
939
+ uint256 batchSize = i + BATCH_SIZE > totalTierIds ? totalTierIds - i : BATCH_SIZE;
940
+ uint16[] memory batchTierIds = new uint16[](batchSize);
941
+
942
+ // Copy tier IDs for this batch
943
+ for (uint256 j = 0; j < batchSize; j++) {
944
+ batchTierIds[j] = tierIds[i + j];
945
+ }
946
+
947
+ // Build the metadata using the tiers to mint and the overspending flag
948
+ bytes[] memory data = new bytes[](1);
949
+ data[0] = abi.encode(false, batchTierIds);
950
+
951
+ // Get the hook ID
952
+ bytes4[] memory ids = new bytes4[](1);
953
+ ids[0] = JBMetadataResolver.getId("pay", hook.METADATA_ID_TARGET());
954
+
955
+ // Generate the metadata
956
+ bytes memory hookMetadata = JBMetadataResolver.createMetadata(ids, data);
957
+
958
+ // Calculate the amount needed for this batch
959
+ uint256 batchAmount = _calculateTotalPriceForTiers(batchTierIds);
960
+
961
+ // Pay the terminal to mint the NFTs for this batch
962
+ terminal.pay{value: batchAmount}({
963
+ projectId: projectId,
964
+ amount: batchAmount,
965
+ token: JBConstants.NATIVE_TOKEN,
966
+ beneficiary: beneficiary,
967
+ minReturnedTokens: 0,
968
+ memo: "Airdrop mint",
969
+ metadata: hookMetadata
970
+ });
971
+ }
972
+ }
973
+
974
+ function _getPriceForUPC(uint16 upc) internal pure returns (uint256) {
975
+ // Price map: UPC -> price in wei
976
+ // This is generated from raw.json prices
977
+ ${generatePriceMap(items)}
978
+ }
979
+
980
+ function _calculateTotalPriceForTiers(uint16[] memory tierIds) internal pure returns (uint256) {
981
+ uint256 total = 0;
982
+ for (uint256 i = 0; i < tierIds.length; i++) {
983
+ total += _getPriceForUPC(tierIds[i]);
984
+ }
985
+ return total;
986
+ }`;
987
+
988
+ // Filter transfer data functions to only include what's needed for this batch
989
+ let batchTransferDataFunctions = '';
990
+ if (hasEthereum) {
991
+ // Ethereum batches 1-6 map to contracts 1-6, batch 7 (unused assets part 1) maps to contract 7, batch 8 (unused assets part 2) maps to contract 8
992
+ const ethereumContractNum = (batchNumber === 7 || batchNumber === 8) ? batchNumber : batchNumber;
993
+ const regex = new RegExp(`function _getEthereumTransferOwners${ethereumContractNum}\\(\\)[\\s\\S]*?return transferOwners;\\s*\\}`, 'g');
994
+ const match = transferDataFunctions.match(regex);
995
+ if (match) {
996
+ batchTransferDataFunctions += match[0] + '\n ';
997
+ }
998
+ }
999
+ if (hasOptimism) {
1000
+ const regex = /function _getOptimismTransferOwners\(\)[\s\S]*?return transferOwners;\s*\}/g;
1001
+ const match = transferDataFunctions.match(regex);
1002
+ if (match) {
1003
+ batchTransferDataFunctions += match[0] + '\n ';
1004
+ }
1005
+ }
1006
+ if (hasBase) {
1007
+ // Base batches 1-4 map to contracts 1-4, batch 5 (unused assets) maps to contract 5
1008
+ const baseContractNum = batchNumber === 5 ? 5 : batchNumber;
1009
+ const regex = new RegExp(`function _getBaseTransferOwners${baseContractNum}\\(\\)[\\s\\S]*?return transferOwners;\\s*\\}`, 'g');
1010
+ const match = transferDataFunctions.match(regex);
1011
+ if (match) {
1012
+ batchTransferDataFunctions += match[0] + '\n ';
1013
+ }
1014
+ }
1015
+ if (hasArbitrum) {
1016
+ // Arbitrum batches 1-3 map to contracts 1-3, batch 4 (unused assets) maps to contract 4
1017
+ const arbitrumContractNum = batchNumber === 4 ? 4 : batchNumber;
1018
+ const regex = new RegExp(`function _getArbitrumTransferOwners${arbitrumContractNum}\\(\\)[\\s\\S]*?return transferOwners;\\s*\\}`, 'g');
1019
+ const match = transferDataFunctions.match(regex);
1020
+ if (match) {
1021
+ batchTransferDataFunctions += match[0] + '\n ';
1022
+ }
1023
+ }
1024
+
1025
+ const script = `// SPDX-License-Identifier: MIT
1026
+ pragma solidity 0.8.23;
1027
+
1028
+ import {Script} from "forge-std/Script.sol";
1029
+ import {console} from "forge-std/console.sol";
1030
+ ${imports}
1031
+ import {JB721TiersHook} from "@bananapus/721-hook-v5/src/JB721TiersHook.sol";
1032
+ import {Sphinx} from "@sphinx-labs/contracts/SphinxPlugin.sol";
1033
+ import {IJBTerminal} from "@bananapus/core-v5/src/interfaces/IJBTerminal.sol";
1034
+ import {JBConstants} from "@bananapus/core-v5/src/libraries/JBConstants.sol";
1035
+ import {JBMetadataResolver} from "@bananapus/core-v5/src/libraries/JBMetadataResolver.sol";
1036
+
1037
+ contract AirdropOutfitsBatch${batchNumber}Script is Script, Sphinx {
1038
+ // Maximum tier IDs per batch to avoid metadata size limit (255 words max)
1039
+ // Each tier ID takes 1 word, plus overhead for array length, boolean, and metadata structure
1040
+ // Using 100 as a safe batch size to stay well under the limit
1041
+ uint256 private constant BATCH_SIZE = 100;
1042
+
1043
+ function configureSphinx() public override {
1044
+ sphinxConfig.projectName = "banny-core";
1045
+ sphinxConfig.mainnets = ["ethereum", "optimism", "base", "arbitrum"];
1046
+ sphinxConfig.testnets = new string[](0);
1047
+ }
1048
+
1049
+ ${runFunction}
1050
+ ${chainRunFunctions}
1051
+ ${processMigrationFunction}
1052
+ ${helperFunctions}${batchTransferDataFunctions}
1053
+ }`;
1054
+
1055
+ fs.writeFileSync(outputPath, script);
1056
+ console.log(`Generated batch ${batchNumber} script: ${outputPath}`);
1057
+ }
1058
+
1059
+
1060
+ function buildTransferDataForChain(chainItems) {
1061
+ // Process data for this chain
1062
+ const bannys = [];
1063
+ const outfits = [];
1064
+ const backgrounds = [];
1065
+
1066
+ chainItems.forEach(item => {
1067
+ const tokenId = item.metadata.tokenId;
1068
+ const upc = item.metadata.upc;
1069
+ const category = item.metadata.category;
1070
+ const owner = toChecksumAddress(item.owner || (item.wallet ? item.wallet.address : '0x0000000000000000000000000000000000000000'));
1071
+ const productName = item.metadata.productName;
1072
+
1073
+ if (category === 0) {
1074
+ // Banny body
1075
+ bannys.push({
1076
+ tokenId,
1077
+ upc,
1078
+ backgroundId: item.metadata.backgroundId || 0,
1079
+ outfitIds: item.metadata.outfitIds || [],
1080
+ owner,
1081
+ productName
1082
+ });
1083
+ } else if (category === 1) {
1084
+ // Background
1085
+ backgrounds.push({
1086
+ tokenId,
1087
+ upc,
1088
+ owner,
1089
+ productName
1090
+ });
1091
+ } else {
1092
+ // Outfit
1093
+ outfits.push({
1094
+ tokenId,
1095
+ upc,
1096
+ category,
1097
+ owner,
1098
+ productName
1099
+ });
1100
+ }
1101
+ });
1102
+
1103
+ // Collect all outfitIds and backgroundIds that are being used
1104
+ const usedOutfitIds = new Set();
1105
+ const usedBackgroundIds = new Set();
1106
+
1107
+ bannys.forEach(banny => {
1108
+ if (banny.backgroundId && banny.backgroundId !== 0) {
1109
+ usedBackgroundIds.add(banny.backgroundId);
1110
+ }
1111
+ banny.outfitIds.forEach(outfitId => {
1112
+ usedOutfitIds.add(outfitId);
1113
+ });
1114
+ });
1115
+
1116
+ // Build transfer data array
1117
+ const allItems = [...bannys, ...outfits, ...backgrounds];
1118
+ const transferData = [];
1119
+
1120
+ allItems.forEach((item, index) => {
1121
+ // Skip if owner is zero address
1122
+ if (item.owner === '0x0000000000000000000000000000000000000000') {
1123
+ return;
1124
+ }
1125
+
1126
+ // Skip if this is an outfit being worn
1127
+ if (item.tokenId && usedOutfitIds.has(item.tokenId)) {
1128
+ return;
1129
+ }
1130
+
1131
+ // Skip if this is a background being used
1132
+ if (item.tokenId && usedBackgroundIds.has(item.tokenId)) {
1133
+ return;
1134
+ }
1135
+
1136
+ transferData.push({
1137
+ owner: item.owner
1138
+ });
1139
+ });
1140
+
1141
+ return transferData;
1142
+ }
1143
+
1144
+ function generateTierIdLoops(tierIds, varName = 'allTierIds') {
1145
+ // Group consecutive tier IDs to create efficient for loops
1146
+ const groups = [];
1147
+ let currentGroup = null;
1148
+
1149
+ tierIds.forEach((tierId, index) => {
1150
+ if (currentGroup && currentGroup.tierId === tierId) {
1151
+ currentGroup.count++;
1152
+ } else {
1153
+ if (currentGroup) groups.push(currentGroup);
1154
+ currentGroup = { tierId, startIndex: index, count: 1 };
1155
+ }
1156
+ });
1157
+ if (currentGroup) groups.push(currentGroup);
1158
+
1159
+ let loops = '';
1160
+ groups.forEach(group => {
1161
+ loops += `
1162
+ // Add ${group.count} instances of tier ID ${group.tierId}
1163
+ for (uint256 i = 0; i < ${group.count}; i++) {
1164
+ ${varName}[${group.startIndex} + i] = ${group.tierId};
1165
+ }`;
1166
+ });
1167
+
1168
+ return loops;
1169
+ }
1170
+
1171
+ function generateTierBalanceVerification(transferData, tierIdQuantities) {
1172
+ // Collect unique owners from transferData
1173
+ const uniqueOwners = new Set();
1174
+ transferData.forEach(data => {
1175
+ uniqueOwners.add(data.owner);
1176
+ });
1177
+ const ownersArray = Array.from(uniqueOwners);
1178
+
1179
+ // Collect unique tier IDs from tierIdQuantities
1180
+ const uniqueTierIds = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
1181
+
1182
+ if (ownersArray.length === 0 || uniqueTierIds.length === 0) {
1183
+ return ''; // No verification needed if no owners or tier IDs
1184
+ }
1185
+
1186
+ let code = `
1187
+ // Collect unique owners
1188
+ address[] memory uniqueOwners = new address[](${ownersArray.length});
1189
+ `;
1190
+ ownersArray.forEach((owner, idx) => {
1191
+ code += `
1192
+ uniqueOwners[${idx}] = ${toChecksumAddress(owner)};`;
1193
+ });
1194
+
1195
+ code += `
1196
+
1197
+ // Collect unique tier IDs
1198
+ uint256[] memory uniqueTierIds = new uint256[](${uniqueTierIds.length});
1199
+ `;
1200
+ uniqueTierIds.forEach((tierId, idx) => {
1201
+ code += `
1202
+ uniqueTierIds[${idx}] = ${tierId};`;
1203
+ });
1204
+
1205
+ code += `
1206
+
1207
+ // Verify tier balances: V5 should never exceed V4 (except for tiers owned by fallback resolver in V4)
1208
+ MigrationHelper.verifyTierBalances(
1209
+ hookAddress,
1210
+ v4HookAddress,
1211
+ fallbackV4ResolverAddress,
1212
+ uniqueOwners,
1213
+ uniqueTierIds
1214
+ );`;
1215
+
1216
+ return code;
1217
+ }
1218
+
1219
+ function generatePriceMap(items) {
1220
+ // Create a map of UPC to price from raw.json
1221
+ const upcToPrice = new Map();
1222
+ items.forEach(item => {
1223
+ const upc = item.metadata.upc;
1224
+ const price = item.metadata.price;
1225
+ if (!upcToPrice.has(upc)) {
1226
+ upcToPrice.set(upc, price);
1227
+ }
1228
+ });
1229
+
1230
+ // Generate a helper function that returns price for a given UPC
1231
+ const sortedUpcs = Array.from(upcToPrice.keys()).sort((a, b) => a - b);
1232
+ let code = '';
1233
+
1234
+ if (sortedUpcs.length > 0) {
1235
+ sortedUpcs.forEach(upc => {
1236
+ const price = upcToPrice.get(upc);
1237
+ code += `\n if (upc == ${upc}) return ${price};`;
1238
+ });
1239
+ code += '\n return 0;';
1240
+ } else {
1241
+ code = ' return 0;';
1242
+ }
1243
+
1244
+ return code;
1245
+ }
1246
+
1247
+ function generateTokenIdArray(chainItems, transferData, tierIdQuantities, upcStartingUnitNumbers = new Map()) {
1248
+ // Build allItems array in transfer order (bannys, outfits, backgrounds)
1249
+ const bannys = [];
1250
+ const outfits = [];
1251
+ const backgrounds = [];
1252
+ // IMPORTANT: We determine which assets are worn/used by looking at outfitIds and backgroundId
1253
+ // on Banny body entries (category === 0). We do NOT use wornByBannyBodyId from outfit/background
1254
+ // entries as it is not reliable in raw.json.
1255
+ const usedOutfitIds = new Set();
1256
+ const usedBackgroundIds = new Set();
1257
+
1258
+ chainItems.forEach(item => {
1259
+ const tokenId = item.metadata.tokenId;
1260
+ const upc = item.metadata.upc;
1261
+ const category = item.metadata.category;
1262
+ const owner = toChecksumAddress(item.owner || (item.wallet ? item.wallet.address : '0x0000000000000000000000000000000000000000'));
1263
+ const productName = item.metadata.productName;
1264
+
1265
+ if (category === 0) {
1266
+ bannys.push({
1267
+ tokenId,
1268
+ upc,
1269
+ backgroundId: item.metadata.backgroundId || 0,
1270
+ outfitIds: item.metadata.outfitIds || [],
1271
+ owner,
1272
+ productName
1273
+ });
1274
+ if (item.metadata.backgroundId && item.metadata.backgroundId !== 0) {
1275
+ usedBackgroundIds.add(item.metadata.backgroundId);
1276
+ }
1277
+ (item.metadata.outfitIds || []).forEach(outfitId => {
1278
+ usedOutfitIds.add(outfitId);
1279
+ });
1280
+ } else if (category === 1) {
1281
+ backgrounds.push({
1282
+ tokenId,
1283
+ upc,
1284
+ owner,
1285
+ productName
1286
+ });
1287
+ } else {
1288
+ outfits.push({
1289
+ tokenId,
1290
+ upc,
1291
+ category,
1292
+ owner,
1293
+ productName
1294
+ });
1295
+ }
1296
+ });
1297
+
1298
+ // Build itemsForTransfer in the same order as transferData
1299
+ const allItemsOrdered = [...bannys, ...outfits, ...backgrounds];
1300
+ const itemsForTransfer = [];
1301
+
1302
+ allItemsOrdered.forEach(item => {
1303
+ // Skip if owner is zero address
1304
+ if (item.owner === '0x0000000000000000000000000000000000000000') {
1305
+ return;
1306
+ }
1307
+
1308
+ // Skip if this is an outfit being worn
1309
+ if (item.tokenId && usedOutfitIds.has(item.tokenId)) {
1310
+ return;
1311
+ }
1312
+
1313
+ // Skip if this is a background being used
1314
+ if (item.tokenId && usedBackgroundIds.has(item.tokenId)) {
1315
+ return;
1316
+ }
1317
+
1318
+ itemsForTransfer.push(item);
1319
+ });
1320
+
1321
+ // Build tier ID array in mint order (sorted by UPC) to determine unit numbers
1322
+ const tierIdQuantitiesForMinting = new Map();
1323
+ chainItems.forEach(item => {
1324
+ const upc = item.metadata.upc;
1325
+ tierIdQuantitiesForMinting.set(upc, (tierIdQuantitiesForMinting.get(upc) || 0) + 1);
1326
+ });
1327
+
1328
+ const uniqueUpcs = Array.from(tierIdQuantitiesForMinting.keys()).sort((a, b) => a - b);
1329
+ const tierIdsInMintOrder = [];
1330
+ uniqueUpcs.forEach(upc => {
1331
+ const quantity = tierIdQuantitiesForMinting.get(upc);
1332
+ for (let i = 0; i < quantity; i++) {
1333
+ tierIdsInMintOrder.push(upc);
1334
+ }
1335
+ });
1336
+
1337
+ // Build mapping of items to their token IDs based on mint order
1338
+ // Token IDs follow the formula: upc * 1000000000 + unitNumber
1339
+ // where unitNumber is incremented per UPC (starting from 1)
1340
+
1341
+ // Create a map from V4 tokenId to its token ID using the formula
1342
+ const v4TokenIdToTokenId = new Map();
1343
+ const upcCounters = new Map();
1344
+
1345
+ tierIdsInMintOrder.forEach((upc) => {
1346
+ const counter = (upcCounters.get(upc) || 0) + 1;
1347
+ upcCounters.set(upc, counter);
1348
+
1349
+ // Find the V4 token ID that corresponds to this mint position
1350
+ const upcItems = chainItems.filter(item => item.metadata.upc === upc);
1351
+ // Sort by original order in chainItems to maintain consistency
1352
+ const sortedUpcItems = [...upcItems].sort((a, b) => {
1353
+ return chainItems.indexOf(a) - chainItems.indexOf(b);
1354
+ });
1355
+
1356
+ if (counter <= sortedUpcItems.length) {
1357
+ const item = sortedUpcItems[counter - 1];
1358
+ if (item) {
1359
+ // Calculate token ID: upc * 1000000000 + unitNumber
1360
+ // unitNumber = startingUnitNumber (from previous chunks) + counter - 1
1361
+ const startingUnitNumber = upcStartingUnitNumbers.get(upc) || 1;
1362
+ const unitNumber = startingUnitNumber + counter - 1;
1363
+ const v5TokenId = upc * 1000000000 + unitNumber;
1364
+ v4TokenIdToTokenId.set(item.metadata.tokenId, v5TokenId);
1365
+ }
1366
+ }
1367
+ });
1368
+
1369
+ // Generate token IDs for each item in transfer order using the formula
1370
+ // Note: Only banny body token IDs are guaranteed to match between V4 and V5.
1371
+ // Outfits/backgrounds being worn may have different IDs, but they're not transferred.
1372
+ let code = '';
1373
+
1374
+ itemsForTransfer.forEach((item, index) => {
1375
+ const v4TokenId = item.tokenId;
1376
+ const tokenId = v4TokenIdToTokenId.get(v4TokenId);
1377
+
1378
+ if (tokenId) {
1379
+ code += `\n generatedTokenIds[${index}] = ${tokenId}; // Token ID (V4: ${v4TokenId})`;
1380
+ } else {
1381
+ // Fallback: use V4 token ID (shouldn't happen)
1382
+ code += `\n generatedTokenIds[${index}] = ${v4TokenId}; // Fallback: V4 token ID`;
1383
+ }
1384
+ });
1385
+
1386
+ return code;
1387
+ }
1388
+
1389
+ function generateContractVersion(items, tierIds = null) {
1390
+ // Calculate tier IDs for each chain if not provided
1391
+ if (!tierIds) {
1392
+ const ethereumItems = items.filter(item => item.chainId === 1);
1393
+ const optimismItems = items.filter(item => item.chainId === 10);
1394
+ const baseItems = items.filter(item => item.chainId === 8453);
1395
+ const arbitrumItems = items.filter(item => item.chainId === 42161);
1396
+
1397
+ const ethereumTierIds = [];
1398
+ const optimismTierIds = [];
1399
+ const baseTierIds = [];
1400
+ const arbitrumTierIds = [];
1401
+
1402
+ // Build tier ID arrays for each chain
1403
+ [ethereumItems, optimismItems, baseItems, arbitrumItems].forEach((chainItems, index) => {
1404
+ const tierIdQuantities = new Map();
1405
+ chainItems.forEach(item => {
1406
+ const upc = item.metadata.upc;
1407
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
1408
+ });
1409
+
1410
+ const uniqueUpcs = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
1411
+ const tierIds = [];
1412
+ uniqueUpcs.forEach(upc => {
1413
+ const quantity = tierIdQuantities.get(upc);
1414
+ for (let i = 0; i < quantity; i++) {
1415
+ tierIds.push(upc);
1416
+ }
1417
+ });
1418
+
1419
+ if (index === 0) ethereumTierIds.push(...tierIds);
1420
+ else if (index === 1) optimismTierIds.push(...tierIds);
1421
+ else if (index === 2) baseTierIds.push(...tierIds);
1422
+ else if (index === 3) arbitrumTierIds.push(...tierIds);
1423
+ });
1424
+
1425
+ tierIds = { ethereumTierIds, optimismTierIds, baseTierIds, arbitrumTierIds };
1426
+ }
1427
+
1428
+ // Process transfer data for each chain and chunks
1429
+ const chains = [
1430
+ { id: 1, name: 'Ethereum', numChunks: 6 },
1431
+ { id: 10, name: 'Optimism', numChunks: 1 },
1432
+ { id: 8453, name: 'Base', numChunks: 4 },
1433
+ { id: 42161, name: 'Arbitrum', numChunks: 3 }
1434
+ ];
1435
+
1436
+ let transferDataFunctions = '';
1437
+
1438
+ chains.forEach(chain => {
1439
+ const chainItems = items.filter(item => item.chainId === chain.id);
1440
+ if (chainItems.length === 0) return;
1441
+
1442
+ if (chain.numChunks > 1) {
1443
+ // Split into chunks and generate transfer data for each chunk
1444
+ const chunks = splitBanniesIntoChunks(chainItems, chain.numChunks);
1445
+ chunks.forEach((chunk, chunkIndex) => {
1446
+ const transferData = chunk.transferData;
1447
+ transferDataFunctions += `
1448
+ function _get${chain.name}TransferOwners${chunkIndex + 1}() internal pure returns (address[] memory) {
1449
+ address[] memory transferOwners = new address[](${transferData.length});
1450
+ `;
1451
+
1452
+ transferData.forEach((data, index) => {
1453
+ transferDataFunctions += `
1454
+ transferOwners[${index}] = ${toChecksumAddress(data.owner)};`;
1455
+ });
1456
+
1457
+ transferDataFunctions += `
1458
+ return transferOwners;
1459
+ }
1460
+ `;
1461
+ });
1462
+
1463
+ // Generate transfer data function for unused assets (Ethereum, Base, and Arbitrum)
1464
+ if (chain.id === 1 || chain.id === 8453 || chain.id === 42161) {
1465
+ // Collect all token IDs already processed in chunks 1-3 (or 1-2 for Base)
1466
+ const processedTokenIds = new Set();
1467
+ chunks.forEach(chunk => {
1468
+ chunk.allItems.forEach(item => {
1469
+ const tokenId = item.metadata.tokenId;
1470
+ processedTokenIds.add(Number(tokenId));
1471
+ });
1472
+ });
1473
+
1474
+ // Calculate UPC counts from CHUNKS ONLY (not all items) to determine starting unit numbers
1475
+ // This tells us how many tokens of each UPC were already minted in previous chunks
1476
+ const upcCountsFromChunks = new Map();
1477
+ chunks.forEach(chunk => {
1478
+ chunk.allItems.forEach(item => {
1479
+ const upc = item.metadata.upc;
1480
+ upcCountsFromChunks.set(upc, (upcCountsFromChunks.get(upc) || 0) + 1);
1481
+ });
1482
+ });
1483
+ // Convert counts to starting unit numbers (1-indexed, so add 1)
1484
+ // If 6 tokens were minted, the next one should be unit number 7
1485
+ const upcStartingUnitNumbers = new Map();
1486
+ upcCountsFromChunks.forEach((count, upc) => {
1487
+ upcStartingUnitNumbers.set(upc, count + 1);
1488
+ });
1489
+
1490
+ const unusedContractData = generateUnusedAssetsContract(chain, chainItems, upcStartingUnitNumbers, processedTokenIds);
1491
+ if (unusedContractData && unusedContractData.unusedItems.length > 0) {
1492
+ // For Ethereum, split unused assets into two contracts (7 and 8)
1493
+ if (chain.id === 1) {
1494
+ const midPoint = Math.ceil(unusedContractData.unusedItems.length / 2);
1495
+ const unusedItems7 = unusedContractData.unusedItems.slice(0, midPoint);
1496
+ const unusedItems8 = unusedContractData.unusedItems.slice(midPoint);
1497
+
1498
+ // Generate function 7
1499
+ transferDataFunctions += `
1500
+ function _get${chain.name}TransferOwners7() internal pure returns (address[] memory) {
1501
+ address[] memory transferOwners = new address[](${unusedItems7.length});
1502
+ `;
1503
+
1504
+ unusedItems7.forEach((item, index) => {
1505
+ transferDataFunctions += `
1506
+ transferOwners[${index}] = ${toChecksumAddress(item.owner)};`;
1507
+ });
1508
+
1509
+ transferDataFunctions += `
1510
+ return transferOwners;
1511
+ }
1512
+ `;
1513
+
1514
+ // Generate function 8 if there are items for contract 8
1515
+ if (unusedItems8.length > 0) {
1516
+ transferDataFunctions += `
1517
+ function _get${chain.name}TransferOwners8() internal pure returns (address[] memory) {
1518
+ address[] memory transferOwners = new address[](${unusedItems8.length});
1519
+ `;
1520
+
1521
+ unusedItems8.forEach((item, index) => {
1522
+ transferDataFunctions += `
1523
+ transferOwners[${index}] = ${toChecksumAddress(item.owner)};`;
1524
+ });
1525
+
1526
+ transferDataFunctions += `
1527
+ return transferOwners;
1528
+ }
1529
+ `;
1530
+ }
1531
+ } else {
1532
+ // For other chains (Base, Arbitrum), generate single function
1533
+ const chunkIndex = chain.numChunks;
1534
+ transferDataFunctions += `
1535
+ function _get${chain.name}TransferOwners${chunkIndex + 1}() internal pure returns (address[] memory) {
1536
+ address[] memory transferOwners = new address[](${unusedContractData.unusedItems.length});
1537
+ `;
1538
+
1539
+ unusedContractData.unusedItems.forEach((item, index) => {
1540
+ transferDataFunctions += `
1541
+ transferOwners[${index}] = ${toChecksumAddress(item.owner)};`;
1542
+ });
1543
+
1544
+ transferDataFunctions += `
1545
+ return transferOwners;
1546
+ }
1547
+ `;
1548
+ }
1549
+ }
1550
+ }
1551
+ } else {
1552
+ // Single contract (no splitting)
1553
+ const transferData = buildTransferDataForChain(chainItems);
1554
+
1555
+ transferDataFunctions += `
1556
+ function _get${chain.name}TransferOwners() internal pure returns (address[] memory) {
1557
+ address[] memory transferOwners = new address[](${transferData.length});
1558
+ `;
1559
+
1560
+ transferData.forEach((data, index) => {
1561
+ transferDataFunctions += `
1562
+ transferOwners[${index}] = ${toChecksumAddress(data.owner)};`;
1563
+ });
1564
+
1565
+ transferDataFunctions += `
1566
+ return transferOwners;
1567
+ }
1568
+ `;
1569
+ }
1570
+ });
1571
+
1572
+ // Generate a contract-based version that deploys a migration contract
1573
+ // and makes a single call to it
1574
+ return `// SPDX-License-Identifier: MIT
1575
+ pragma solidity 0.8.23;
1576
+
1577
+ import {Script} from "forge-std/Script.sol";
1578
+ import {console} from "forge-std/console.sol";
1579
+ import {MigrationContractEthereum1} from "./MigrationContractEthereum1.sol";
1580
+ import {MigrationContractEthereum2} from "./MigrationContractEthereum2.sol";
1581
+ import {MigrationContractEthereum3} from "./MigrationContractEthereum3.sol";
1582
+ import {MigrationContractEthereum4} from "./MigrationContractEthereum4.sol";
1583
+ import {MigrationContractEthereum5} from "./MigrationContractEthereum5.sol";
1584
+ import {MigrationContractEthereum6} from "./MigrationContractEthereum6.sol";
1585
+ import {MigrationContractEthereum7} from "./MigrationContractEthereum7.sol";
1586
+ import {MigrationContractEthereum8} from "./MigrationContractEthereum8.sol";
1587
+ import {MigrationContractOptimism} from "./MigrationContractOptimism.sol";
1588
+ import {MigrationContractBase1} from "./MigrationContractBase1.sol";
1589
+ import {MigrationContractBase2} from "./MigrationContractBase2.sol";
1590
+ import {MigrationContractBase3} from "./MigrationContractBase3.sol";
1591
+ import {MigrationContractBase4} from "./MigrationContractBase4.sol";
1592
+ import {MigrationContractBase5} from "./MigrationContractBase5.sol";
1593
+ import {MigrationContractArbitrum1} from "./MigrationContractArbitrum1.sol";
1594
+ import {MigrationContractArbitrum2} from "./MigrationContractArbitrum2.sol";
1595
+ import {MigrationContractArbitrum3} from "./MigrationContractArbitrum3.sol";
1596
+ import {MigrationContractArbitrum4} from "./MigrationContractArbitrum4.sol";
1597
+
1598
+ import {JB721TiersHook} from "@bananapus/721-hook-v5/src/JB721TiersHook.sol";
1599
+ import {Sphinx} from "@sphinx-labs/contracts/SphinxPlugin.sol";
1600
+ import {IJBTerminal} from "@bananapus/core-v5/src/interfaces/IJBTerminal.sol";
1601
+ import {JBConstants} from "@bananapus/core-v5/src/libraries/JBConstants.sol";
1602
+ import {JBMetadataResolver} from "@bananapus/core-v5/src/libraries/JBMetadataResolver.sol";
1603
+
1604
+ contract AirdropOutfitsScript is Script, Sphinx {
1605
+ // Maximum tier IDs per batch to avoid metadata size limit (255 words max)
1606
+ // Each tier ID takes 1 word, plus overhead for array length, boolean, and metadata structure
1607
+ // Using 100 as a safe batch size to stay well under the limit
1608
+ uint256 private constant BATCH_SIZE = 100;
1609
+ function configureSphinx() public override {
1610
+ sphinxConfig.projectName = "banny-core";
1611
+ sphinxConfig.mainnets = ["ethereum", "optimism", "base", "arbitrum"];
1612
+ sphinxConfig.testnets = new string[](0);
1613
+ }
1614
+
1615
+ function run() public sphinx {
1616
+ uint256 chainId = block.chainid;
1617
+
1618
+ if (chainId == 1) {
1619
+ // Ethereum Mainnet
1620
+ _runEthereum();
1621
+ } else if (chainId == 10) {
1622
+ // Optimism
1623
+ _runOptimism();
1624
+ } else if (chainId == 8453) {
1625
+ // Base
1626
+ _runBase();
1627
+ } else if (chainId == 42161) {
1628
+ // Arbitrum
1629
+ _runArbitrum();
1630
+ } else {
1631
+ revert("Unsupported chain");
1632
+ }
1633
+ }
1634
+
1635
+ function _runEthereum() internal {
1636
+ address hookAddress = ${toChecksumAddress('0xb4Ec363c2E7DB0cECA9AA1759338d7d1b49d1750')};
1637
+ address resolverAddress = ${toChecksumAddress('0x47c011146a4498a70e0bf2e4585acf9cade85954')};
1638
+ address v4HookAddress = ${toChecksumAddress('0x2da41cdc79ae49f2725ab549717b2dbcfc42b958')};
1639
+ address v4ResolverAddress = ${toChecksumAddress('0xa5f8911d4cfd60a6697479f078409434424fe666')};
1640
+ address terminalAddress = ${toChecksumAddress('0x2db6d704058e552defe415753465df8df0361846')};
1641
+ address v4ResolverFallback = ${toChecksumAddress('0xfF80c37a57016EFf3d19fb286e9C740eC4537Dd3')};
1642
+ _processMigration(
1643
+ hookAddress,
1644
+ resolverAddress,
1645
+ v4HookAddress,
1646
+ v4ResolverAddress,
1647
+ terminalAddress,
1648
+ v4ResolverFallback,
1649
+ 1
1650
+ );
1651
+ }
1652
+
1653
+ function _runOptimism() internal {
1654
+ address hookAddress = ${toChecksumAddress('0xb4Ec363c2E7DB0cECA9AA1759338d7d1b49d1750')};
1655
+ address resolverAddress = ${toChecksumAddress('0x47c011146a4498a70e0bf2e4585acf9cade85954')};
1656
+ address v4HookAddress = ${toChecksumAddress('0x2da41cdc79ae49f2725ab549717b2dbcfc42b958')};
1657
+ address v4ResolverAddress = ${toChecksumAddress('0xa5f8911d4cfd60a6697479f078409434424fe666')};
1658
+ address terminalAddress = ${toChecksumAddress('0x2db6d704058e552defe415753465df8df0361846')};
1659
+ address v4ResolverFallback = ${toChecksumAddress('0xfF80c37a57016EFf3d19fb286e9C740eC4537Dd3')};
1660
+ _processMigration(
1661
+ hookAddress,
1662
+ resolverAddress,
1663
+ v4HookAddress,
1664
+ v4ResolverAddress,
1665
+ terminalAddress,
1666
+ v4ResolverFallback,
1667
+ 10
1668
+ );
1669
+ }
1670
+
1671
+ function _runBase() internal {
1672
+ address hookAddress = ${toChecksumAddress('0xb4Ec363c2E7DB0cECA9AA1759338d7d1b49d1750')};
1673
+ address resolverAddress = ${toChecksumAddress('0x47c011146a4498a70e0bf2e4585acf9cade85954')};
1674
+ address v4HookAddress = ${toChecksumAddress('0x2da41cdc79ae49f2725ab549717b2dbcfc42b958')};
1675
+ address v4ResolverAddress = ${toChecksumAddress('0xa5f8911d4cfd60a6697479f078409434424fe666')};
1676
+ address terminalAddress = ${toChecksumAddress('0x2db6d704058e552defe415753465df8df0361846')};
1677
+ address v4ResolverFallback = ${toChecksumAddress('0xfF80c37a57016EFf3d19fb286e9C740eC4537Dd3')};
1678
+ _processMigration(
1679
+ hookAddress,
1680
+ resolverAddress,
1681
+ v4HookAddress,
1682
+ v4ResolverAddress,
1683
+ terminalAddress,
1684
+ v4ResolverFallback,
1685
+ 8453
1686
+ );
1687
+ }
1688
+
1689
+ function _runArbitrum() internal {
1690
+ address hookAddress = ${toChecksumAddress('0xb4Ec363c2E7DB0cECA9AA1759338d7d1b49d1750')};
1691
+ address resolverAddress = ${toChecksumAddress('0x47c011146a4498a70e0bf2e4585acf9cade85954')};
1692
+ address v4HookAddress = ${toChecksumAddress('0x2da41cdc79ae49f2725ab549717b2dbcfc42b958')};
1693
+ address v4ResolverAddress = ${toChecksumAddress('0xa5f8911d4cfd60a6697479f078409434424fe666')};
1694
+ address terminalAddress = ${toChecksumAddress('0x2db6d704058e552defe415753465df8df0361846')};
1695
+ address v4ResolverFallback = ${toChecksumAddress('0xfF80c37a57016EFf3d19fb286e9C740eC4537Dd3')};
1696
+ _processMigration(
1697
+ hookAddress,
1698
+ resolverAddress,
1699
+ v4HookAddress,
1700
+ v4ResolverAddress,
1701
+ terminalAddress,
1702
+ v4ResolverFallback,
1703
+ 42161
1704
+ );
1705
+ }
1706
+
1707
+ function _processMigration(address hookAddress, address resolverAddress, address v4HookAddress, address v4ResolverAddress, address terminalAddress, address v4ResolverFallback, uint256 chainId) internal {
1708
+ // Validate addresses
1709
+ require(hookAddress != address(0), "Hook address not set");
1710
+ require(resolverAddress != address(0), "Resolver address not set");
1711
+ require(v4HookAddress != address(0), "V4 Hook address not set");
1712
+ require(v4ResolverAddress != address(0), "V4 Resolver address not set");
1713
+ require(terminalAddress != address(0), "Terminal address not set");
1714
+
1715
+ IJBTerminal terminal = IJBTerminal(terminalAddress);
1716
+ JB721TiersHook hook = JB721TiersHook(hookAddress);
1717
+
1718
+ // Get project ID from hook
1719
+ uint256 projectId = hook.PROJECT_ID();
1720
+
1721
+ // Deploy the appropriate chain-specific migration contract with transfer data
1722
+ if (chainId == 1) {
1723
+ // Ethereum - 6 chunks (plus optional unused assets chunks 7 and 8)
1724
+ ${(() => {
1725
+ const regularChunks = tierIds.ethereumChunkTierIds.slice(0, 6);
1726
+ const unusedChunk7 = tierIds.ethereumChunkTierIds.length > 6 ? tierIds.ethereumChunkTierIds[6] : null;
1727
+ const unusedChunk8 = tierIds.ethereumChunkTierIds.length > 7 ? tierIds.ethereumChunkTierIds[7] : null;
1728
+ let code = '';
1729
+
1730
+ // Generate code for regular chunks (1-6)
1731
+ regularChunks.forEach((chunkTierIds, chunkIndex) => {
1732
+ const varName = `tierIds${chunkIndex + 1}`;
1733
+ code += `
1734
+ // Deploy and execute contract ${chunkIndex + 1}
1735
+ uint16[] memory ${varName} = new uint16[](${chunkTierIds.length});
1736
+ ${generateTierIdLoops(chunkTierIds, varName)}
1737
+ address[] memory transferOwners${chunkIndex + 1} = _getEthereumTransferOwners${chunkIndex + 1}();
1738
+ MigrationContractEthereum${chunkIndex + 1} migrationContract${chunkIndex + 1} = new MigrationContractEthereum${chunkIndex + 1}(transferOwners${chunkIndex + 1});
1739
+ console.log("Ethereum migration contract ${chunkIndex + 1} deployed at:", address(migrationContract${chunkIndex + 1}));
1740
+
1741
+ // Mint chunk ${chunkIndex + 1} assets to the contract address via pay()
1742
+ _mintViaPay(
1743
+ terminal,
1744
+ hook,
1745
+ projectId,
1746
+ ${varName},
1747
+ address(migrationContract${chunkIndex + 1})
1748
+ );
1749
+ console.log("Minted", ${varName}.length, "tokens to contract ${chunkIndex + 1}");
1750
+
1751
+ migrationContract${chunkIndex + 1}.executeMigration(hookAddress, resolverAddress, v4HookAddress, v4ResolverAddress, v4ResolverFallback);
1752
+ `;
1753
+ });
1754
+
1755
+ // Generate code for unused assets chunk 7 if it exists
1756
+ if (unusedChunk7 && unusedChunk7.length > 0) {
1757
+ code += `
1758
+ // Deploy and execute contract 7 (unused outfits/backgrounds - part 1)
1759
+ uint16[] memory tierIds7 = new uint16[](${unusedChunk7.length});
1760
+ ${generateTierIdLoops(unusedChunk7, 'tierIds7')}
1761
+ address[] memory transferOwners7 = _getEthereumTransferOwners7();
1762
+ MigrationContractEthereum7 migrationContract7 = new MigrationContractEthereum7(transferOwners7);
1763
+ console.log("Ethereum migration contract 7 deployed at:", address(migrationContract7));
1764
+
1765
+ // Mint chunk 7 assets to the contract address via pay()
1766
+ _mintViaPay(
1767
+ terminal,
1768
+ hook,
1769
+ projectId,
1770
+ tierIds7,
1771
+ address(migrationContract7)
1772
+ );
1773
+ console.log("Minted", tierIds7.length, "tokens to contract 7");
1774
+
1775
+ migrationContract7.executeMigration(hookAddress, resolverAddress, v4HookAddress, v4ResolverAddress, v4ResolverFallback);
1776
+ `;
1777
+ }
1778
+
1779
+ // Generate code for unused assets chunk 8 if it exists
1780
+ if (unusedChunk8 && unusedChunk8.length > 0) {
1781
+ code += `
1782
+ // Deploy and execute contract 8 (unused outfits/backgrounds - part 2)
1783
+ uint16[] memory tierIds8 = new uint16[](${unusedChunk8.length});
1784
+ ${generateTierIdLoops(unusedChunk8, 'tierIds8')}
1785
+ address[] memory transferOwners8 = _getEthereumTransferOwners8();
1786
+ MigrationContractEthereum8 migrationContract8 = new MigrationContractEthereum8(transferOwners8);
1787
+ console.log("Ethereum migration contract 8 deployed at:", address(migrationContract8));
1788
+
1789
+ // Mint chunk 8 assets to the contract address via pay()
1790
+ _mintViaPay(
1791
+ terminal,
1792
+ hook,
1793
+ projectId,
1794
+ tierIds8,
1795
+ address(migrationContract8)
1796
+ );
1797
+ console.log("Minted", tierIds8.length, "tokens to contract 8");
1798
+
1799
+ migrationContract8.executeMigration(hookAddress, resolverAddress, v4HookAddress, v4ResolverAddress, v4ResolverFallback);
1800
+ `;
1801
+ }
1802
+
1803
+ return code;
1804
+ })()}
1805
+ } else if (chainId == 10) {
1806
+ // Optimism tier IDs
1807
+ uint16[] memory allTierIds = new uint16[](${tierIds.optimismTierIds.length});
1808
+ ${generateTierIdLoops(tierIds.optimismTierIds)}
1809
+ address[] memory transferOwners = _getOptimismTransferOwners();
1810
+ MigrationContractOptimism migrationContract = new MigrationContractOptimism(transferOwners);
1811
+ console.log("Optimism migration contract deployed at:", address(migrationContract));
1812
+
1813
+ // Mint all assets to the contract address via pay()
1814
+ _mintViaPay(
1815
+ terminal,
1816
+ hook,
1817
+ projectId,
1818
+ allTierIds,
1819
+ address(migrationContract)
1820
+ );
1821
+ console.log("Minted", allTierIds.length, "tokens to contract");
1822
+
1823
+ migrationContract.executeMigration(hookAddress, resolverAddress, v4HookAddress, v4ResolverAddress, v4ResolverFallback);
1824
+ } else if (chainId == 8453) {
1825
+ // Base - 4 chunks (plus optional unused assets chunk)
1826
+ ${(() => {
1827
+ // Ensure baseChunkTierIds exists and has the correct structure
1828
+ const baseChunks = (tierIds && tierIds.baseChunkTierIds) ? tierIds.baseChunkTierIds : [];
1829
+ const regularChunks = baseChunks.slice(0, 4);
1830
+ const hasUnusedChunk = baseChunks.length > 4;
1831
+ const unusedChunk = hasUnusedChunk ? baseChunks[4] : null;
1832
+ let code = '';
1833
+
1834
+ // Generate code for regular chunks (1-4)
1835
+ regularChunks.forEach((chunkTierIds, chunkIndex) => {
1836
+ const varName = `tierIds${chunkIndex + 1}`;
1837
+ code += `
1838
+ // Deploy and execute contract ${chunkIndex + 1}
1839
+ uint16[] memory ${varName} = new uint16[](${chunkTierIds.length});
1840
+ ${generateTierIdLoops(chunkTierIds, varName)}
1841
+ address[] memory transferOwners${chunkIndex + 1} = _getBaseTransferOwners${chunkIndex + 1}();
1842
+ MigrationContractBase${chunkIndex + 1} migrationContract${chunkIndex + 1} = new MigrationContractBase${chunkIndex + 1}(transferOwners${chunkIndex + 1});
1843
+ console.log("Base migration contract ${chunkIndex + 1} deployed at:", address(migrationContract${chunkIndex + 1}));
1844
+
1845
+ // Mint chunk ${chunkIndex + 1} assets to the contract address via pay()
1846
+ _mintViaPay(
1847
+ terminal,
1848
+ hook,
1849
+ projectId,
1850
+ ${varName},
1851
+ address(migrationContract${chunkIndex + 1})
1852
+ );
1853
+ console.log("Minted", ${varName}.length, "tokens to contract ${chunkIndex + 1}");
1854
+
1855
+ migrationContract${chunkIndex + 1}.executeMigration(hookAddress, resolverAddress, v4HookAddress, v4ResolverAddress, v4ResolverFallback);
1856
+ `;
1857
+ });
1858
+
1859
+ // Generate code for unused assets chunk (5) if it exists
1860
+ if (hasUnusedChunk && unusedChunk && unusedChunk.length > 0) {
1861
+ code += `
1862
+ // Deploy and execute contract 5 (unused outfits/backgrounds)
1863
+ uint16[] memory tierIds5 = new uint16[](${unusedChunk.length});
1864
+ ${generateTierIdLoops(unusedChunk, 'tierIds5')}
1865
+ address[] memory transferOwners5 = _getBaseTransferOwners5();
1866
+ MigrationContractBase5 migrationContract5 = new MigrationContractBase5(transferOwners5);
1867
+ console.log("Base migration contract 5 deployed at:", address(migrationContract5));
1868
+
1869
+ // Mint chunk 5 assets to the contract address via pay()
1870
+ _mintViaPay(
1871
+ terminal,
1872
+ hook,
1873
+ projectId,
1874
+ tierIds5,
1875
+ address(migrationContract5)
1876
+ );
1877
+ console.log("Minted", tierIds5.length, "tokens to contract 5");
1878
+
1879
+ migrationContract5.executeMigration(hookAddress, resolverAddress, v4HookAddress, v4ResolverAddress, v4ResolverFallback);
1880
+ `;
1881
+ }
1882
+
1883
+ return code;
1884
+ })()}
1885
+ } else if (chainId == 42161) {
1886
+ // Arbitrum - 3 chunks (plus optional unused assets chunk)
1887
+ ${(() => {
1888
+ // Ensure arbitrumChunkTierIds exists and has the correct structure
1889
+ const arbitrumChunks = (tierIds && tierIds.arbitrumChunkTierIds) ? tierIds.arbitrumChunkTierIds : [];
1890
+ const regularChunks = arbitrumChunks.slice(0, 3);
1891
+ const hasUnusedChunk = arbitrumChunks.length > 3;
1892
+ const unusedChunk = hasUnusedChunk ? arbitrumChunks[3] : null;
1893
+ let code = '';
1894
+
1895
+ // Generate code for regular chunks (1-3)
1896
+ regularChunks.forEach((chunkTierIds, chunkIndex) => {
1897
+ const varName = `tierIds${chunkIndex + 1}`;
1898
+ code += `
1899
+ // Deploy and execute contract ${chunkIndex + 1}
1900
+ uint16[] memory ${varName} = new uint16[](${chunkTierIds.length});
1901
+ ${generateTierIdLoops(chunkTierIds, varName)}
1902
+ address[] memory transferOwners${chunkIndex + 1} = _getArbitrumTransferOwners${chunkIndex + 1}();
1903
+ MigrationContractArbitrum${chunkIndex + 1} migrationContract${chunkIndex + 1} = new MigrationContractArbitrum${chunkIndex + 1}(transferOwners${chunkIndex + 1});
1904
+ console.log("Arbitrum migration contract ${chunkIndex + 1} deployed at:", address(migrationContract${chunkIndex + 1}));
1905
+
1906
+ // Mint chunk ${chunkIndex + 1} assets to the contract address via pay()
1907
+ _mintViaPay(
1908
+ terminal,
1909
+ hook,
1910
+ projectId,
1911
+ ${varName},
1912
+ address(migrationContract${chunkIndex + 1})
1913
+ );
1914
+ console.log("Minted", ${varName}.length, "tokens to contract ${chunkIndex + 1}");
1915
+
1916
+ migrationContract${chunkIndex + 1}.executeMigration(hookAddress, resolverAddress, v4HookAddress, v4ResolverAddress, v4ResolverFallback);
1917
+ `;
1918
+ });
1919
+
1920
+ // Generate code for unused assets chunk (4) if it exists
1921
+ if (hasUnusedChunk && unusedChunk && unusedChunk.length > 0) {
1922
+ code += `
1923
+ // Deploy and execute contract 4 (unused outfits/backgrounds)
1924
+ uint16[] memory tierIds4 = new uint16[](${unusedChunk.length});
1925
+ ${generateTierIdLoops(unusedChunk, 'tierIds4')}
1926
+ address[] memory transferOwners4 = _getArbitrumTransferOwners4();
1927
+ MigrationContractArbitrum4 migrationContract4 = new MigrationContractArbitrum4(transferOwners4);
1928
+ console.log("Arbitrum migration contract 4 deployed at:", address(migrationContract4));
1929
+
1930
+ // Mint chunk 4 assets to the contract address via pay()
1931
+ _mintViaPay(
1932
+ terminal,
1933
+ hook,
1934
+ projectId,
1935
+ tierIds4,
1936
+ address(migrationContract4)
1937
+ );
1938
+ console.log("Minted", tierIds4.length, "tokens to contract 4");
1939
+
1940
+ migrationContract4.executeMigration(hookAddress, resolverAddress, v4HookAddress, v4ResolverAddress, v4ResolverFallback);
1941
+ `;
1942
+ }
1943
+
1944
+ return code;
1945
+ })()}
1946
+ } else {
1947
+ revert("Unsupported chain for contract deployment");
1948
+ }
1949
+ }
1950
+
1951
+ function _mintViaPay(
1952
+ IJBTerminal terminal,
1953
+ JB721TiersHook hook,
1954
+ uint256 projectId,
1955
+ uint16[] memory tierIds,
1956
+ address beneficiary
1957
+ ) internal {
1958
+ uint256 totalTierIds = tierIds.length;
1959
+
1960
+ // Process tier IDs in batches
1961
+ for (uint256 i = 0; i < totalTierIds; i += BATCH_SIZE) {
1962
+ uint256 batchSize = i + BATCH_SIZE > totalTierIds ? totalTierIds - i : BATCH_SIZE;
1963
+ uint16[] memory batchTierIds = new uint16[](batchSize);
1964
+
1965
+ // Copy tier IDs for this batch
1966
+ for (uint256 j = 0; j < batchSize; j++) {
1967
+ batchTierIds[j] = tierIds[i + j];
1968
+ }
1969
+
1970
+ // Build the metadata using the tiers to mint and the overspending flag
1971
+ bytes[] memory data = new bytes[](1);
1972
+ data[0] = abi.encode(false, batchTierIds);
1973
+
1974
+ // Get the hook ID
1975
+ bytes4[] memory ids = new bytes4[](1);
1976
+ ids[0] = JBMetadataResolver.getId("pay", hook.METADATA_ID_TARGET());
1977
+
1978
+ // Generate the metadata
1979
+ bytes memory hookMetadata = JBMetadataResolver.createMetadata(ids, data);
1980
+
1981
+ // Calculate the amount needed for this batch
1982
+ uint256 batchAmount = _calculateTotalPriceForTiers(batchTierIds);
1983
+
1984
+ // Pay the terminal to mint the NFTs for this batch
1985
+ terminal.pay{value: batchAmount}({
1986
+ projectId: projectId,
1987
+ amount: batchAmount,
1988
+ token: JBConstants.NATIVE_TOKEN,
1989
+ beneficiary: beneficiary,
1990
+ minReturnedTokens: 0,
1991
+ memo: "Airdrop mint",
1992
+ metadata: hookMetadata
1993
+ });
1994
+ }
1995
+ }
1996
+
1997
+ function _getPriceForUPC(uint16 upc) internal pure returns (uint256) {
1998
+ // Price map: UPC -> price in wei
1999
+ // This is generated from raw.json prices
2000
+ ${generatePriceMap(items)}
2001
+ }
2002
+
2003
+ function _calculateTotalPriceForTiers(uint16[] memory tierIds) internal pure returns (uint256) {
2004
+ uint256 total = 0;
2005
+ for (uint256 i = 0; i < tierIds.length; i++) {
2006
+ total += _getPriceForUPC(tierIds[i]);
2007
+ }
2008
+ return total;
2009
+ }${transferDataFunctions}
2010
+ }`;
2011
+ }
2012
+
2013
+ // Split bannies into chunks and collect their dependencies (outfits/backgrounds)
2014
+ function splitBanniesIntoChunks(chainItems, numChunks) {
2015
+ // First, extract all bannies
2016
+ const bannys = [];
2017
+ chainItems.forEach(item => {
2018
+ if (item.metadata.category === 0) {
2019
+ bannys.push({
2020
+ tokenId: item.metadata.tokenId,
2021
+ upc: item.metadata.upc,
2022
+ backgroundId: item.metadata.backgroundId || 0,
2023
+ outfitIds: item.metadata.outfitIds || [],
2024
+ owner: toChecksumAddress(item.owner || (item.wallet ? item.wallet.address : '0x0000000000000000000000000000000000000000')),
2025
+ productName: item.metadata.productName
2026
+ });
2027
+ }
2028
+ });
2029
+
2030
+ // Split bannies into chunks
2031
+ const chunks = [];
2032
+ const chunkSize = Math.ceil(bannys.length / numChunks);
2033
+
2034
+ for (let i = 0; i < numChunks; i++) {
2035
+ const startIdx = i * chunkSize;
2036
+ const endIdx = Math.min(startIdx + chunkSize, bannys.length);
2037
+ const bannyChunk = bannys.slice(startIdx, endIdx);
2038
+
2039
+ // Collect all outfit and background IDs needed by this chunk
2040
+ const neededOutfitIds = new Set();
2041
+ const neededBackgroundIds = new Set();
2042
+ const neededBannyTokenIds = new Set();
2043
+
2044
+ bannyChunk.forEach(banny => {
2045
+ neededBannyTokenIds.add(banny.tokenId);
2046
+ if (banny.backgroundId && banny.backgroundId !== 0) {
2047
+ neededBackgroundIds.add(banny.backgroundId);
2048
+ }
2049
+ banny.outfitIds.forEach(outfitId => {
2050
+ neededOutfitIds.add(outfitId);
2051
+ });
2052
+ });
2053
+
2054
+ // Find all items that belong to this chunk:
2055
+ // 1. Banny bodies in the chunk
2056
+ // 2. Outfits needed by those bannies
2057
+ // 3. Backgrounds needed by those bannies
2058
+ const chunkItems = chainItems.filter(item => {
2059
+ const tokenId = item.metadata.tokenId;
2060
+ return neededBannyTokenIds.has(tokenId) ||
2061
+ neededOutfitIds.has(tokenId) ||
2062
+ neededBackgroundIds.has(tokenId);
2063
+ });
2064
+
2065
+ // Build transfer data for this chunk (items that need to be transferred)
2066
+ // IMPORTANT: We determine which assets are worn/used by looking at outfitIds and backgroundId
2067
+ // on Banny body entries (category === 0). We do NOT use wornByBannyBodyId from outfit/background
2068
+ // entries as it is not reliable in raw.json.
2069
+ const usedOutfitIds = new Set();
2070
+ const usedBackgroundIds = new Set();
2071
+
2072
+ bannyChunk.forEach(banny => {
2073
+ if (banny.backgroundId && banny.backgroundId !== 0) {
2074
+ usedBackgroundIds.add(banny.backgroundId);
2075
+ }
2076
+ banny.outfitIds.forEach(outfitId => {
2077
+ usedOutfitIds.add(outfitId);
2078
+ });
2079
+ });
2080
+
2081
+ const transferData = [];
2082
+ chunkItems.forEach(item => {
2083
+ const owner = toChecksumAddress(item.owner || (item.wallet ? item.wallet.address : '0x0000000000000000000000000000000000000000'));
2084
+ if (owner === '0x0000000000000000000000000000000000000000') {
2085
+ return;
2086
+ }
2087
+
2088
+ // Skip outfits being worn
2089
+ if (item.metadata.tokenId && usedOutfitIds.has(item.metadata.tokenId)) {
2090
+ return;
2091
+ }
2092
+
2093
+ // Skip backgrounds being used
2094
+ if (item.metadata.tokenId && usedBackgroundIds.has(item.metadata.tokenId)) {
2095
+ return;
2096
+ }
2097
+
2098
+ transferData.push({
2099
+ item: item,
2100
+ owner: owner
2101
+ });
2102
+ });
2103
+
2104
+ chunks.push({
2105
+ bannies: bannyChunk,
2106
+ allItems: chunkItems,
2107
+ transferData: transferData
2108
+ });
2109
+ }
2110
+
2111
+ return chunks;
2112
+ }
2113
+
2114
+ function generateChainSpecificContracts(inputFile) {
2115
+ console.log(`\n=== Generating chain-specific migration contracts from ${inputFile} ===`);
2116
+
2117
+ // Load the raw data
2118
+ const rawDataPath = path.join(__dirname, inputFile);
2119
+ const rawData = JSON.parse(fs.readFileSync(rawDataPath, 'utf8'));
2120
+
2121
+ const items = rawData.data.nfts.items;
2122
+
2123
+ const chains = [
2124
+ { id: 1, name: 'Ethereum', fileName: 'MigrationContractEthereum.sol', numChunks: 6 },
2125
+ { id: 10, name: 'Optimism', fileName: 'MigrationContractOptimism.sol', numChunks: 1 },
2126
+ { id: 8453, name: 'Base', fileName: 'MigrationContractBase.sol', numChunks: 4 },
2127
+ { id: 42161, name: 'Arbitrum', fileName: 'MigrationContractArbitrum.sol', numChunks: 3 },
2128
+ ];
2129
+
2130
+ // Track total items processed per chain for verification
2131
+ const chainTotals = new Map();
2132
+
2133
+ chains.forEach(chain => {
2134
+ const chainItems = items.filter(item => item.chainId === chain.id);
2135
+ console.log(`Processing chain ${chain.id} (${chain.name}): ${chainItems.length} items`);
2136
+
2137
+ // Store total items for this chain
2138
+ chainTotals.set(chain.id, { total: chainItems.length, processed: 0 });
2139
+
2140
+ if (chainItems.length === 0) {
2141
+ console.log(`Skipping ${chain.name} - no items found`);
2142
+ return;
2143
+ }
2144
+
2145
+ if (chain.numChunks > 1) {
2146
+ // Split into multiple contracts
2147
+ const chunks = splitBanniesIntoChunks(chainItems, chain.numChunks);
2148
+
2149
+ // Calculate starting unitNumber for each UPC across chunks
2150
+ // Token IDs follow: upc * 1000000000 + unitNumber
2151
+ // unitNumber is incremented per UPC globally across all chunks
2152
+ const upcCounts = new Map(); // Map<upc, count> - tracks how many items of each UPC were minted in previous chunks
2153
+
2154
+ chunks.forEach((chunk, chunkIndex) => {
2155
+ // Calculate starting unitNumbers for each UPC in this chunk
2156
+ const upcStartingUnitNumbers = new Map(); // Map<upc, startingUnitNumber>
2157
+
2158
+ // Count items per UPC in this chunk
2159
+ const chunkUpcCounts = new Map();
2160
+ chunk.allItems.forEach(item => {
2161
+ const upc = item.metadata.upc;
2162
+ chunkUpcCounts.set(upc, (chunkUpcCounts.get(upc) || 0) + 1);
2163
+ });
2164
+
2165
+ // Set starting unitNumber for each UPC (1 + count from previous chunks)
2166
+ chunkUpcCounts.forEach((count, upc) => {
2167
+ const previousCount = upcCounts.get(upc) || 0;
2168
+ upcStartingUnitNumbers.set(upc, previousCount + 1);
2169
+ });
2170
+
2171
+ const contract = generateChunkContract(chain, chainItems, chunk, chunkIndex + 1, chain.numChunks, upcStartingUnitNumbers);
2172
+
2173
+ // Update UPC counts for next chunk
2174
+ chunkUpcCounts.forEach((count, upc) => {
2175
+ upcCounts.set(upc, (upcCounts.get(upc) || 0) + count);
2176
+ });
2177
+
2178
+ // Write the contract to file
2179
+ const fileName = chain.fileName.replace('.sol', `${chunkIndex + 1}.sol`);
2180
+ const outputPath = path.join(__dirname, '..', fileName);
2181
+ fs.writeFileSync(outputPath, contract);
2182
+
2183
+ console.log(`Generated ${fileName} with ${chunk.bannies.length} Bannys to dress, ${chunk.allItems.length} items to transfer`);
2184
+
2185
+ // Track items processed in this chunk
2186
+ const chainTotal = chainTotals.get(chain.id);
2187
+ chainTotal.processed += chunk.allItems.length;
2188
+ });
2189
+
2190
+ // Generate unused assets contract for Ethereum and Base
2191
+ if ((chain.id === 1 || chain.id === 8453 || chain.id === 42161) && chain.numChunks > 1) {
2192
+ // Collect all token IDs already processed in chunks 1-3 (or 1-2 for Base)
2193
+ const processedTokenIds = new Set();
2194
+ chunks.forEach(chunk => {
2195
+ chunk.allItems.forEach(item => {
2196
+ const tokenId = item.metadata.tokenId;
2197
+ processedTokenIds.add(Number(tokenId));
2198
+ });
2199
+ });
2200
+
2201
+ // Calculate UPC counts from CHUNKS ONLY (not all items) to determine starting unit numbers
2202
+ // This tells us how many tokens of each UPC were already minted in previous chunks
2203
+ const upcCountsFromChunks = new Map();
2204
+ chunks.forEach(chunk => {
2205
+ chunk.allItems.forEach(item => {
2206
+ const upc = item.metadata.upc;
2207
+ upcCountsFromChunks.set(upc, (upcCountsFromChunks.get(upc) || 0) + 1);
2208
+ });
2209
+ });
2210
+ // Convert counts to starting unit numbers (1-indexed, so add 1)
2211
+ // If 6 tokens were minted, the next one should be unit number 7
2212
+ const upcStartingUnitNumbersMap = new Map();
2213
+ upcCountsFromChunks.forEach((count, upc) => {
2214
+ upcStartingUnitNumbersMap.set(upc, count + 1);
2215
+ });
2216
+
2217
+ // Calculate total items processed in chunks for filtered count
2218
+ let totalProcessedInChunks = 0;
2219
+ chunks.forEach(chunk => {
2220
+ totalProcessedInChunks += chunk.allItems.length;
2221
+ });
2222
+
2223
+ const unusedContractData = generateUnusedAssetsContract(chain, chainItems, upcStartingUnitNumbersMap, processedTokenIds, totalProcessedInChunks);
2224
+
2225
+ if (unusedContractData && unusedContractData.unusedItems.length > 0) {
2226
+ // For Ethereum, split unused assets into two contracts (7 and 8)
2227
+ if (chain.id === 1) {
2228
+ const midPoint = Math.ceil(unusedContractData.unusedItems.length / 2);
2229
+ const unusedItems7 = unusedContractData.unusedItems.slice(0, midPoint);
2230
+ const unusedItems8 = unusedContractData.unusedItems.slice(midPoint);
2231
+
2232
+ // Generate contract 7
2233
+ const contract7 = generateUnusedAssetsContractFromItems(chain, unusedItems7, upcStartingUnitNumbersMap, 7);
2234
+ const fileName7 = chain.fileName.replace('.sol', '7.sol');
2235
+ const outputPath7 = path.join(__dirname, '..', fileName7);
2236
+ fs.writeFileSync(outputPath7, contract7);
2237
+ console.log(`Generated ${fileName7} with ${unusedItems7.length} unused outfits/backgrounds to transfer`);
2238
+
2239
+ // Update UPC starting numbers for contract 8
2240
+ const upcCountsFromContract7 = new Map();
2241
+ unusedItems7.forEach(item => {
2242
+ const upc = item.upc;
2243
+ upcCountsFromContract7.set(upc, (upcCountsFromContract7.get(upc) || 0) + 1);
2244
+ });
2245
+ const upcStartingUnitNumbersFor8 = new Map(upcStartingUnitNumbersMap);
2246
+ upcCountsFromContract7.forEach((count, upc) => {
2247
+ const currentStart = upcStartingUnitNumbersFor8.get(upc) || 1;
2248
+ upcStartingUnitNumbersFor8.set(upc, currentStart + count);
2249
+ });
2250
+
2251
+ // Generate contract 8
2252
+ if (unusedItems8.length > 0) {
2253
+ const contract8 = generateUnusedAssetsContractFromItems(chain, unusedItems8, upcStartingUnitNumbersFor8, 8);
2254
+ const fileName8 = chain.fileName.replace('.sol', '8.sol');
2255
+ const outputPath8 = path.join(__dirname, '..', fileName8);
2256
+ fs.writeFileSync(outputPath8, contract8);
2257
+ console.log(`Generated ${fileName8} with ${unusedItems8.length} unused outfits/backgrounds to transfer`);
2258
+ }
2259
+
2260
+ // Track unused items processed
2261
+ const chainTotal = chainTotals.get(chain.id);
2262
+ chainTotal.processed += unusedContractData.unusedItems.length;
2263
+ } else {
2264
+ // For other chains (Base, Arbitrum), generate single contract
2265
+ const fileName = chain.fileName.replace('.sol', `${chain.numChunks + 1}.sol`);
2266
+ const outputPath = path.join(__dirname, '..', fileName);
2267
+ fs.writeFileSync(outputPath, unusedContractData.contract);
2268
+
2269
+ console.log(`Generated ${fileName} with ${unusedContractData.unusedItems.length} unused outfits/backgrounds to transfer`);
2270
+
2271
+ // Track unused items processed
2272
+ const chainTotal = chainTotals.get(chain.id);
2273
+ chainTotal.processed += unusedContractData.unusedItems.length;
2274
+ }
2275
+
2276
+ // Report filtered items for debugging
2277
+ if (unusedContractData.filteredCount > 0) {
2278
+ console.log(` (${unusedContractData.filteredCount} items filtered out: resolver-owned, zero-address, or already processed)`);
2279
+ if (unusedContractData.filteredItems && unusedContractData.filteredItems.length > 0) {
2280
+ console.log(` Filtered items details:`);
2281
+ unusedContractData.filteredItems.forEach(item => {
2282
+ const reasons = item.reasons.join(', ');
2283
+ console.log(` - Token ID: ${item.tokenId}, UPC: ${item.upc}, Category: ${item.category}, Owner: ${item.owner}, Reasons: ${reasons}`);
2284
+ });
2285
+ }
2286
+ }
2287
+ } else {
2288
+ console.log(`No unused assets found for ${chain.name}, skipping unused assets contract`);
2289
+ }
2290
+ }
2291
+ } else {
2292
+ // Single contract (no splitting)
2293
+ const contract = generateSingleChainContract(chain, chainItems);
2294
+
2295
+ // Write the contract to file
2296
+ const outputPath = path.join(__dirname, '..', chain.fileName);
2297
+ fs.writeFileSync(outputPath, contract);
2298
+
2299
+ console.log(`Generated ${chain.fileName} with ${chainItems.length} items`);
2300
+
2301
+ // Track items processed
2302
+ const chainTotal = chainTotals.get(chain.id);
2303
+ chainTotal.processed += chainItems.length;
2304
+ }
2305
+ });
2306
+
2307
+ // Verify all items from raw.json were processed
2308
+ console.log(`\n=== Verification Summary ===`);
2309
+ let allChainsValid = true;
2310
+ chainTotals.forEach((totals, chainId) => {
2311
+ const chain = chains.find(c => c.id === chainId);
2312
+ const chainName = chain ? chain.name : `Chain ${chainId}`;
2313
+ if (totals.total !== totals.processed) {
2314
+ console.error(`❌ ${chainName}: Expected ${totals.total} items, but processed ${totals.processed} items`);
2315
+ allChainsValid = false;
2316
+ } else {
2317
+ console.log(`✅ ${chainName}: All ${totals.total} items accounted for`);
2318
+ }
2319
+ });
2320
+
2321
+ if (!allChainsValid) {
2322
+ console.error(`\n⚠️ WARNING: Some items from raw.json were not processed!`);
2323
+ process.exit(1);
2324
+ } else {
2325
+ console.log(`\n✅ All items from raw.json are accounted for across all migration contracts`);
2326
+ }
2327
+ }
2328
+
2329
+ function generateChunkContract(chain, chainItems, chunk, chunkIndex, totalChunks, upcStartingUnitNumbers = new Map()) {
2330
+ const bannys = chunk.bannies;
2331
+ const chunkItems = chunk.allItems;
2332
+ const transferData = chunk.transferData;
2333
+
2334
+ // Calculate tier ID quantities for this chunk only
2335
+ const tierIdQuantities = new Map();
2336
+ chunkItems.forEach(item => {
2337
+ const upc = item.metadata.upc;
2338
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
2339
+ });
2340
+
2341
+ let contract = `// SPDX-License-Identifier: MIT
2342
+ pragma solidity 0.8.23;
2343
+
2344
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
2345
+ import {JB721TiersHook} from "@bananapus/721-hook-v5/src/JB721TiersHook.sol";
2346
+ import {Banny721TokenUriResolver} from "../src/Banny721TokenUriResolver.sol";
2347
+ import {MigrationHelper} from "./helpers/MigrationHelper.sol";
2348
+
2349
+ contract MigrationContract${chain.name}${chunkIndex} {
2350
+ address[] private transferOwners;
2351
+
2352
+ constructor(address[] memory _transferOwners) {
2353
+ transferOwners = _transferOwners;
2354
+ }
2355
+
2356
+ function executeMigration(
2357
+ address hookAddress,
2358
+ address resolverAddress,
2359
+ address v4HookAddress,
2360
+ address v4ResolverAddress,
2361
+ address fallbackV4ResolverAddress
2362
+ ) external {
2363
+
2364
+ // Validate addresses
2365
+ require(hookAddress != address(0), "Hook address not set");
2366
+ require(resolverAddress != address(0), "Resolver address not set");
2367
+ require(v4HookAddress != address(0), "V4 Hook address not set");
2368
+ require(v4ResolverAddress != address(0), "V4 Resolver address not set");
2369
+ require(fallbackV4ResolverAddress != address(0), "V4 fallback resolver address not set");
2370
+
2371
+ JB721TiersHook hook = JB721TiersHook(hookAddress);
2372
+ Banny721TokenUriResolver resolver = Banny721TokenUriResolver(resolverAddress);
2373
+ IERC721 v4Hook = IERC721(v4HookAddress);
2374
+ Banny721TokenUriResolver v4Resolver = Banny721TokenUriResolver(v4ResolverAddress);
2375
+ Banny721TokenUriResolver fallbackV4Resolver = Banny721TokenUriResolver(fallbackV4ResolverAddress);
2376
+
2377
+ // ${chain.name} migration chunk ${chunkIndex}/${totalChunks} - ${chunkItems.length} items
2378
+
2379
+ // Step 1: Assets are already minted to this contract by the deployer
2380
+ `;
2381
+
2382
+ // Generate struct definition for minted token IDs
2383
+ const uniqueUpcs = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
2384
+
2385
+ // Generate struct definition at contract level
2386
+ let structDefinition = `
2387
+ // Define struct to hold all UPC minted tokenIds
2388
+ struct MintedIds {
2389
+ `;
2390
+
2391
+ uniqueUpcs.forEach(upc => {
2392
+ const quantity = tierIdQuantities.get(upc);
2393
+ structDefinition += ` uint256[${quantity}] upc${upc};\n`;
2394
+ });
2395
+
2396
+ structDefinition += ` }
2397
+
2398
+ `;
2399
+
2400
+ // Insert struct definition into contract
2401
+ const contractStart = `contract MigrationContract${chain.name}${chunkIndex} {`;
2402
+ const replacement = `${contractStart}${structDefinition}address[] private transferOwners;`;
2403
+ contract = contract.replace(`${contractStart}\n address[] private transferOwners;`, replacement);
2404
+
2405
+ contract += `
2406
+
2407
+ // Assets are already minted to this contract by the deployer
2408
+ `;
2409
+
2410
+ // Build mapping of items to their token IDs based on mint order
2411
+ // Token IDs follow the formula: upc * 1000000000 + unitNumber
2412
+ // where unitNumber is incremented per UPC (starting from 1)
2413
+ // We need to track which items of each UPC are minted in this chunk and assign unitNumbers
2414
+
2415
+ // Build tier ID array in the same order as it will be minted (matching tierIds array generation)
2416
+ const tierIdsInMintOrder = [];
2417
+ uniqueUpcs.forEach(upc => {
2418
+ const quantity = tierIdQuantities.get(upc);
2419
+ for (let i = 0; i < quantity; i++) {
2420
+ tierIdsInMintOrder.push(upc);
2421
+ }
2422
+ });
2423
+
2424
+ // Map each tier ID position to the actual V4 token ID and calculate the token ID
2425
+ // Token ID = upc * 1000000000 + unitNumber (where unitNumber is 1-indexed per UPC, globally across chunks)
2426
+ const v4TokenIdToTokenId = new Map(); // Map<v4TokenId, v5TokenId>
2427
+ const upcCounters = new Map();
2428
+
2429
+ tierIdsInMintOrder.forEach((upc) => {
2430
+ const counter = (upcCounters.get(upc) || 0) + 1;
2431
+ upcCounters.set(upc, counter);
2432
+
2433
+ // Find the V4 token ID that corresponds to this mint position
2434
+ const upcItems = chunkItems.filter(item => item.metadata.upc === upc);
2435
+ // Sort by original order in chunkItems to maintain consistency
2436
+ const sortedUpcItems = [...upcItems].sort((a, b) => {
2437
+ return chunkItems.indexOf(a) - chunkItems.indexOf(b);
2438
+ });
2439
+
2440
+ if (counter <= sortedUpcItems.length) {
2441
+ const item = sortedUpcItems[counter - 1];
2442
+ if (item) {
2443
+ // Calculate token ID: upc * 1000000000 + unitNumber
2444
+ // unitNumber = startingUnitNumber (from previous chunks) + counter - 1
2445
+ const startingUnitNumber = upcStartingUnitNumbers.get(upc) || 1;
2446
+ const unitNumber = startingUnitNumber + counter - 1;
2447
+ const v5TokenId = upc * 1000000000 + unitNumber;
2448
+ v4TokenIdToTokenId.set(item.metadata.tokenId, v5TokenId);
2449
+ }
2450
+ }
2451
+ });
2452
+
2453
+ // Build UPC to token IDs mapping (in the order items will be minted)
2454
+ const upcTokenIds = new Map(); // Map<upc, Array<tokenId>>
2455
+ uniqueUpcs.forEach(upc => {
2456
+ upcTokenIds.set(upc, []);
2457
+ });
2458
+
2459
+ // Populate in mint order (same as tierIdsInMintOrder)
2460
+ upcCounters.clear();
2461
+ tierIdsInMintOrder.forEach((upc) => {
2462
+ const counter = (upcCounters.get(upc) || 0) + 1;
2463
+ upcCounters.set(upc, counter);
2464
+
2465
+ const upcItems = chunkItems.filter(item => item.metadata.upc === upc);
2466
+ const sortedUpcItems = [...upcItems].sort((a, b) => {
2467
+ return chunkItems.indexOf(a) - chunkItems.indexOf(b);
2468
+ });
2469
+
2470
+ if (counter <= sortedUpcItems.length) {
2471
+ const item = sortedUpcItems[counter - 1];
2472
+ if (item) {
2473
+ const tokenId = v4TokenIdToTokenId.get(item.metadata.tokenId);
2474
+ if (tokenId && upcTokenIds.has(upc)) {
2475
+ upcTokenIds.get(upc).push(tokenId);
2476
+ }
2477
+ }
2478
+ }
2479
+ });
2480
+
2481
+ // Create a mapping from UPC to minted tokenIds for dressing
2482
+ const upcToMintedIds = new Map();
2483
+
2484
+ contract += `
2485
+ // Create and populate the struct
2486
+ // Token IDs follow formula: upc * 1000000000 + unitNumber (unitNumber starts at 1 per UPC)
2487
+ MintedIds memory sortedMintedIds;
2488
+ `;
2489
+
2490
+ // Populate sortedMintedIds with token IDs using the formula
2491
+ uniqueUpcs.forEach(upc => {
2492
+ const tokenIds = upcTokenIds.get(upc) || [];
2493
+ const quantity = tokenIds.length;
2494
+ if (quantity > 0) {
2495
+ contract += `
2496
+ // Populate UPC ${upc} minted tokenIds (${quantity} items)`;
2497
+ tokenIds.forEach((tokenId, index) => {
2498
+ contract += `
2499
+ sortedMintedIds.upc${upc}[${index}] = ${tokenId}; // Token ID: ${upc} * 1000000000 + ${index + 1}`;
2500
+ });
2501
+ }
2502
+ upcToMintedIds.set(upc, `sortedMintedIds.upc${upc}`);
2503
+ });
2504
+
2505
+ // Always add approval for decorating
2506
+ contract += `
2507
+ // Step 1.5: Approve resolver to transfer all tokens owned by this contract
2508
+ // The resolver needs approval to transfer outfits and backgrounds to itself during decoration
2509
+ IERC721(address(hook)).setApprovalForAll(address(resolver), true);
2510
+ `;
2511
+
2512
+ contract += `
2513
+ // Step 2: Process each Banny body and dress them
2514
+ `;
2515
+
2516
+ // Add Banny dressing calls
2517
+ bannys.forEach((banny, index) => {
2518
+ if (banny.outfitIds.length > 0 || (banny.backgroundId && banny.backgroundId !== 0)) {
2519
+ contract += `
2520
+ // Dress Banny ${banny.tokenId} (${banny.productName})
2521
+ {
2522
+ uint256[] memory outfitIds = new uint256[](${banny.outfitIds.length});
2523
+ `;
2524
+
2525
+ banny.outfitIds.forEach((v4OutfitId, outfitIndex) => {
2526
+ // Find the token ID for this outfit using the formula
2527
+ const tokenId = v4TokenIdToTokenId.get(v4OutfitId);
2528
+ if (tokenId) {
2529
+ contract += ` outfitIds[${outfitIndex}] = ${tokenId}; // V4: ${v4OutfitId} -> V5: ${tokenId}\n`;
2530
+ } else {
2531
+ // Fallback: use V4 token ID (shouldn't happen if outfit is in chunk)
2532
+ contract += ` outfitIds[${outfitIndex}] = ${v4OutfitId}; // Fallback: V4 token ID (outfit not found in chunk)\n`;
2533
+ }
2534
+ });
2535
+
2536
+ // Find the token ID for the background using the formula
2537
+ let v5BackgroundId = 0;
2538
+ if (banny.backgroundId && banny.backgroundId !== 0) {
2539
+ const bgTokenId = v4TokenIdToTokenId.get(banny.backgroundId);
2540
+ if (bgTokenId) {
2541
+ v5BackgroundId = bgTokenId;
2542
+ } else {
2543
+ v5BackgroundId = banny.backgroundId; // Fallback
2544
+ }
2545
+ }
2546
+
2547
+ // Get token ID for the banny body using the formula
2548
+ const bannyTokenId = v4TokenIdToTokenId.get(banny.tokenId) || banny.tokenId;
2549
+
2550
+ contract += `
2551
+ resolver.decorateBannyWith(
2552
+ address(hook),
2553
+ ${bannyTokenId},
2554
+ ${v5BackgroundId},
2555
+ outfitIds
2556
+ );
2557
+ `;
2558
+
2559
+ contract += `
2560
+ MigrationHelper.verifyV4AssetMatch(
2561
+ resolver,
2562
+ v4Resolver,
2563
+ fallbackV4Resolver,
2564
+ address(hook),
2565
+ v4HookAddress,
2566
+ ${banny.tokenId}
2567
+ );
2568
+ `;
2569
+
2570
+ contract += `
2571
+ }
2572
+ `;
2573
+ }
2574
+ });
2575
+
2576
+ contract += `
2577
+ // Step 3: Transfer all assets to rightful owners using constructor data
2578
+ // Generate token IDs in the same order as items appear (matching mint order)
2579
+ // Token ID format: UPC * 1000000000 + unitNumber
2580
+ // Note: Only banny body token IDs are guaranteed to match between V4 and V5.
2581
+ // Outfits/backgrounds being worn by bannys may have different IDs, but that's OK
2582
+ // since they're not transferred (only used in decorateBannyWith calls).
2583
+ uint256[] memory generatedTokenIds = new uint256[](transferOwners.length);
2584
+ ${generateTokenIdArray(chunkItems, transferData, tierIdQuantities, upcStartingUnitNumbers)}
2585
+
2586
+ uint256 successfulTransfers = 0;
2587
+ uint256 skippedResolverOwned = 0;
2588
+
2589
+ for (uint256 i = 0; i < transferOwners.length; i++) {
2590
+ uint256 tokenId = generatedTokenIds[i];
2591
+ // Verify V4 ownership before transferring V5
2592
+ address v4Owner = v4Hook.ownerOf(tokenId);
2593
+ require(v4Owner == transferOwners[i] || v4Owner == address(fallbackV4ResolverAddress), "V4/V5 ownership mismatch for token");
2594
+
2595
+ // Skip transfer if V4 owner is the resolver (resolver holds these tokens, we shouldn't transfer to resolver)
2596
+ if (v4Owner == address(v4ResolverAddress) || v4Owner == address(fallbackV4ResolverAddress)) {
2597
+ // Token is held by resolver, skip transfer
2598
+ skippedResolverOwned++;
2599
+ continue;
2600
+ }
2601
+
2602
+ IERC721(address(hook)).safeTransferFrom(
2603
+ address(this),
2604
+ transferOwners[i],
2605
+ tokenId
2606
+ );
2607
+ successfulTransfers++;
2608
+ }
2609
+
2610
+ // Verify all expected items were processed (transferred or skipped as expected)
2611
+ require(
2612
+ successfulTransfers + skippedResolverOwned == transferOwners.length,
2613
+ "Not all items were processed"
2614
+ );
2615
+
2616
+ // Final verification: Ensure this contract no longer owns any tokens
2617
+ // This ensures all transfers completed successfully and no tokens were left behind
2618
+ require(hook.balanceOf(address(this)) == 0, "Contract still owns tokens after migration");
2619
+
2620
+ // Verify tier balances: V5 should never exceed V4 (except for tiers owned by fallback resolver in V4)
2621
+ ${generateTierBalanceVerification(transferData, tierIdQuantities)}
2622
+ }
2623
+ }`;
2624
+
2625
+ // Fix indentation issues
2626
+ contract = contract.replace(/^ outfitIds\[0\] =/gm, ' outfitIds[0] =');
2627
+ contract = contract.replace(/^ }$/gm, ' }'); // Fix struct closing bracket indentation
2628
+
2629
+ return contract;
2630
+ }
2631
+
2632
+ function generateSingleChainContract(chain, chainItems) {
2633
+ // Process data for this chain
2634
+ const bannys = [];
2635
+ const outfits = [];
2636
+ const backgrounds = [];
2637
+ const tierIdQuantities = new Map(); // Map UPC to quantity needed
2638
+ const transferData = []; // Array of {tokenIndex, owner} for transfers
2639
+
2640
+ chainItems.forEach(item => {
2641
+ const tokenId = item.metadata.tokenId;
2642
+ const upc = item.metadata.upc;
2643
+ const category = item.metadata.category;
2644
+ const categoryName = item.metadata.categoryName;
2645
+ const owner = toChecksumAddress(item.owner || (item.wallet ? item.wallet.address : '0x0000000000000000000000000000000000000000'));
2646
+ const productName = item.metadata.productName;
2647
+
2648
+ // Count how many of each UPC we need
2649
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
2650
+
2651
+ if (category === 0) {
2652
+ // Banny body
2653
+ bannys.push({
2654
+ tokenId,
2655
+ upc,
2656
+ backgroundId: item.metadata.backgroundId || 0,
2657
+ outfitIds: item.metadata.outfitIds || [],
2658
+ owner,
2659
+ productName
2660
+ });
2661
+ } else if (category === 1) {
2662
+ // Background
2663
+ backgrounds.push({
2664
+ tokenId,
2665
+ upc,
2666
+ owner,
2667
+ productName
2668
+ });
2669
+ } else {
2670
+ // Outfit
2671
+ outfits.push({
2672
+ tokenId,
2673
+ upc,
2674
+ category,
2675
+ categoryName,
2676
+ owner,
2677
+ productName
2678
+ });
2679
+ }
2680
+ });
2681
+
2682
+ // Collect all outfitIds and backgroundIds that are being used
2683
+ // IMPORTANT: We determine which assets are worn/used by looking at outfitIds and backgroundId
2684
+ // on Banny body entries (category === 0). We do NOT use wornByBannyBodyId from outfit/background
2685
+ // entries as it is not reliable in raw.json.
2686
+ const usedOutfitIds = new Set();
2687
+ const usedBackgroundIds = new Set();
2688
+
2689
+ bannys.forEach(banny => {
2690
+ if (banny.backgroundId && banny.backgroundId !== 0) {
2691
+ usedBackgroundIds.add(banny.backgroundId);
2692
+ }
2693
+ banny.outfitIds.forEach(outfitId => {
2694
+ usedOutfitIds.add(outfitId);
2695
+ });
2696
+ });
2697
+
2698
+ // Build transfer data array
2699
+ const allItems = [...bannys, ...outfits, ...backgrounds];
2700
+ let transferIndex = 0;
2701
+
2702
+ allItems.forEach((item, index) => {
2703
+ // Skip if owner is zero address
2704
+ if (item.owner === '0x0000000000000000000000000000000000000000') {
2705
+ return;
2706
+ }
2707
+
2708
+ // Skip if this is an outfit being worn
2709
+ if (item.tokenId && usedOutfitIds.has(item.tokenId)) {
2710
+ return;
2711
+ }
2712
+
2713
+ // Skip if this is a background being used
2714
+ if (item.tokenId && usedBackgroundIds.has(item.tokenId)) {
2715
+ return;
2716
+ }
2717
+
2718
+ transferData.push({
2719
+ tokenIndex: transferIndex,
2720
+ owner: item.owner
2721
+ });
2722
+ transferIndex++;
2723
+ });
2724
+
2725
+ let contract = `// SPDX-License-Identifier: MIT
2726
+ pragma solidity 0.8.23;
2727
+
2728
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
2729
+ import {JB721TiersHook} from "@bananapus/721-hook-v5/src/JB721TiersHook.sol";
2730
+ import {Banny721TokenUriResolver} from "../src/Banny721TokenUriResolver.sol";
2731
+ import {MigrationHelper} from "./helpers/MigrationHelper.sol";
2732
+
2733
+ contract MigrationContract${chain.name} {
2734
+ address[] private transferOwners;
2735
+
2736
+ constructor(address[] memory _transferOwners) {
2737
+ transferOwners = _transferOwners;
2738
+ }
2739
+
2740
+ function executeMigration(
2741
+ address hookAddress,
2742
+ address resolverAddress,
2743
+ address v4HookAddress,
2744
+ address v4ResolverAddress,
2745
+ address fallbackV4ResolverAddress
2746
+ ) external {
2747
+
2748
+ // Validate addresses
2749
+ require(hookAddress != address(0), "Hook address not set");
2750
+ require(resolverAddress != address(0), "Resolver address not set");
2751
+ require(v4HookAddress != address(0), "V4 Hook address not set");
2752
+ require(v4ResolverAddress != address(0), "V4 Resolver address not set");
2753
+ require(fallbackV4ResolverAddress != address(0), "V4 fallback resolver address not set");
2754
+
2755
+ JB721TiersHook hook = JB721TiersHook(hookAddress);
2756
+ Banny721TokenUriResolver resolver = Banny721TokenUriResolver(resolverAddress);
2757
+ IERC721 v4Hook = IERC721(v4HookAddress);
2758
+ Banny721TokenUriResolver v4Resolver = Banny721TokenUriResolver(v4ResolverAddress);
2759
+ Banny721TokenUriResolver fallbackV4Resolver = Banny721TokenUriResolver(fallbackV4ResolverAddress);
2760
+
2761
+ // ${chain.name} migration - ${chainItems.length} items
2762
+
2763
+ // Step 1: Assets are already minted to this contract by the deployer
2764
+ `;
2765
+
2766
+ // Generate struct definition for minted token IDs
2767
+ const uniqueUpcs = Array.from(tierIdQuantities.keys()).sort((a, b) => a - b);
2768
+
2769
+ // Generate struct definition at contract level
2770
+ let structDefinition = `
2771
+ // Define struct to hold all UPC minted tokenIds
2772
+ struct MintedIds {
2773
+ `;
2774
+
2775
+ uniqueUpcs.forEach(upc => {
2776
+ const quantity = tierIdQuantities.get(upc);
2777
+ structDefinition += ` uint256[${quantity}] upc${upc};\n`;
2778
+ });
2779
+
2780
+ structDefinition += ` }
2781
+
2782
+ `;
2783
+
2784
+ // Insert struct definition into contract
2785
+ const contractStart = `contract MigrationContract${chain.name} {`;
2786
+ const replacement = `${contractStart}${structDefinition}address[] private transferOwners;`;
2787
+ contract = contract.replace(`${contractStart}\n address[] private transferOwners;`, replacement);
2788
+
2789
+ contract += `
2790
+
2791
+ // Assets are already minted to this contract by the deployer
2792
+ `;
2793
+
2794
+ // Create a mapping from UPC to minted tokenIds for dressing
2795
+ const upcToMintedIds = new Map();
2796
+
2797
+ contract += `
2798
+ // Create and populate the struct
2799
+ // Token IDs are generated as: UPC * 1000000000 + unitNumber (where unitNumber starts at 1)
2800
+ MintedIds memory sortedMintedIds;
2801
+ `;
2802
+
2803
+ uniqueUpcs.forEach(upc => {
2804
+ const quantity = tierIdQuantities.get(upc);
2805
+ contract += `
2806
+ // Populate UPC ${upc} minted tokenIds (${quantity} items)
2807
+ for (uint256 i = 0; i < ${quantity}; i++) {
2808
+ sortedMintedIds.upc${upc}[i] = ${upc} * 1000000000 + (i + 1);
2809
+ }`;
2810
+ upcToMintedIds.set(upc, `sortedMintedIds.upc${upc}`);
2811
+ });
2812
+
2813
+ // Check if there are any outfits or backgrounds that need approval
2814
+ const hasOutfitsOrBackgrounds = bannys.some(banny =>
2815
+ (banny.outfitIds && banny.outfitIds.length > 0) ||
2816
+ (banny.backgroundId && banny.backgroundId !== 0)
2817
+ );
2818
+
2819
+ // Generate approval code using setApprovalForAll
2820
+ if (hasOutfitsOrBackgrounds) {
2821
+ contract += `
2822
+ // Step 1.5: Approve resolver to transfer all tokens owned by this contract
2823
+ // The resolver needs approval to transfer outfits and backgrounds to itself during decoration
2824
+ IERC721(address(hook)).setApprovalForAll(address(resolver), true);
2825
+ `;
2826
+ }
2827
+
2828
+ contract += `
2829
+ // Step 2: Process each Banny body and dress them
2830
+ `;
2831
+
2832
+ // Add Banny dressing calls
2833
+ bannys.forEach((banny, index) => {
2834
+ if (banny.outfitIds.length > 0) {
2835
+ contract += `
2836
+ // Dress Banny ${banny.tokenId} (${banny.productName})
2837
+ {
2838
+ uint256[] memory outfitIds = new uint256[](${banny.outfitIds.length});
2839
+ `;
2840
+
2841
+ banny.outfitIds.forEach((v4OutfitId, outfitIndex) => {
2842
+ // Find which UPC this V4 outfitId corresponds to
2843
+ const matchingItem = chainItems.find(item => item.metadata.tokenId === v4OutfitId);
2844
+ if (matchingItem) {
2845
+ const upc = matchingItem.metadata.upc;
2846
+ const upcArrayName = upcToMintedIds.get(upc);
2847
+ // Find the index of this specific outfitId within its UPC
2848
+ const upcItems = chainItems.filter(item => item.metadata.upc === upc);
2849
+ const itemIndex = upcItems.findIndex(item => item.metadata.tokenId === v4OutfitId);
2850
+
2851
+ contract += ` outfitIds[${outfitIndex}] = ${upcArrayName}[${itemIndex}]; // V4: ${v4OutfitId} -> V5: ${upcArrayName}[${itemIndex}]\n`;
2852
+ } else {
2853
+ // Fallback to V4 outfitId if we can't find the mapping
2854
+ contract += ` outfitIds[${outfitIndex}] = ${v4OutfitId}; // Fallback: using V4 outfitId\n`;
2855
+ }
2856
+ });
2857
+
2858
+ // Map backgroundId to V5 minted tokenId
2859
+ let v5BackgroundId = banny.backgroundId;
2860
+ if (banny.backgroundId && banny.backgroundId !== 0) {
2861
+ const backgroundItem = chainItems.find(item => item.metadata.tokenId === banny.backgroundId);
2862
+ if (backgroundItem) {
2863
+ const upc = backgroundItem.metadata.upc;
2864
+ const upcArrayName = upcToMintedIds.get(upc);
2865
+ const upcItems = chainItems.filter(item => item.metadata.upc === upc);
2866
+ const itemIndex = upcItems.findIndex(item => item.metadata.tokenId === banny.backgroundId);
2867
+ v5BackgroundId = `${upcArrayName}[${itemIndex}]`;
2868
+ }
2869
+ }
2870
+
2871
+ contract += `
2872
+ resolver.decorateBannyWith(
2873
+ address(hook),
2874
+ ${banny.tokenId},
2875
+ ${v5BackgroundId},
2876
+ outfitIds
2877
+ );
2878
+ `;
2879
+
2880
+ contract += `
2881
+ MigrationHelper.verifyV4AssetMatch(
2882
+ resolver,
2883
+ v4Resolver,
2884
+ fallbackV4Resolver,
2885
+ address(hook),
2886
+ v4HookAddress,
2887
+ ${banny.tokenId}
2888
+ );
2889
+ `;
2890
+
2891
+ contract += `
2892
+ }
2893
+ `;
2894
+ }
2895
+ });
2896
+
2897
+ contract += `
2898
+ // Step 3: Transfer all assets to rightful owners using constructor data
2899
+ // Generate token IDs in the same order as items appear (matching mint order)
2900
+ // Token ID format: UPC * 1000000000 + unitNumber
2901
+ uint256[] memory generatedTokenIds = new uint256[](transferOwners.length);
2902
+ ${generateTokenIdArray(chainItems, transferData, tierIdQuantities)}
2903
+
2904
+ uint256 successfulTransfers = 0;
2905
+ uint256 skippedResolverOwned = 0;
2906
+
2907
+ for (uint256 i = 0; i < transferOwners.length; i++) {
2908
+ uint256 tokenId = generatedTokenIds[i];
2909
+ // Verify V4 ownership before transferring V5
2910
+ address v4Owner = v4Hook.ownerOf(tokenId);
2911
+ require(v4Owner == transferOwners[i] || v4Owner == address(fallbackV4ResolverAddress), "V4/V5 ownership mismatch for token");
2912
+
2913
+ // Skip transfer if V4 owner is the resolver (resolver holds these tokens, we shouldn't transfer to resolver)
2914
+ if (v4Owner == address(v4ResolverAddress) || v4Owner == address(fallbackV4ResolverAddress)) {
2915
+ // Token is held by resolver, skip transfer
2916
+ skippedResolverOwned++;
2917
+ continue;
2918
+ }
2919
+
2920
+ IERC721(address(hook)).transferFrom(
2921
+ address(this),
2922
+ transferOwners[i],
2923
+ tokenId
2924
+ );
2925
+ successfulTransfers++;
2926
+ }
2927
+
2928
+ // Verify all expected items were processed (transferred or skipped as expected)
2929
+ require(
2930
+ successfulTransfers + skippedResolverOwned == transferOwners.length,
2931
+ "Not all items were processed"
2932
+ );
2933
+
2934
+ // Final verification: Ensure this contract no longer owns any tokens
2935
+ // This ensures all transfers completed successfully and no tokens were left behind
2936
+ require(hook.balanceOf(address(this)) == 0, "Contract still owns tokens after migration");
2937
+
2938
+ // Verify tier balances: V5 should never exceed V4 (except for tiers owned by fallback resolver in V4)
2939
+ ${generateTierBalanceVerification(transferData, tierIdQuantities)}
2940
+ }
2941
+ }`;
2942
+
2943
+ // Fix indentation issues
2944
+ contract = contract.replace(/^ outfitIds\[0\] =/gm, ' outfitIds[0] =');
2945
+ contract = contract.replace(/^ }$/gm, ' }'); // Fix struct closing bracket indentation
2946
+
2947
+ return contract;
2948
+ }
2949
+
2950
+ function generateUnusedAssetsContract(chain, chainItems, upcStartingUnitNumbers = new Map(), processedTokenIds = new Set(), totalProcessedInChunks = 0) {
2951
+ // Process data for this chain
2952
+ const bannys = [];
2953
+ const outfits = [];
2954
+ const backgrounds = [];
2955
+
2956
+ chainItems.forEach(item => {
2957
+ const tokenId = item.metadata.tokenId;
2958
+ const upc = item.metadata.upc;
2959
+ const category = item.metadata.category;
2960
+ const owner = toChecksumAddress(item.owner || (item.wallet ? item.wallet.address : '0x0000000000000000000000000000000000000000'));
2961
+
2962
+ if (category === 0) {
2963
+ // Banny body
2964
+ bannys.push({
2965
+ tokenId,
2966
+ upc,
2967
+ backgroundId: item.metadata.backgroundId || 0,
2968
+ outfitIds: item.metadata.outfitIds || [],
2969
+ owner
2970
+ });
2971
+ } else if (category === 1) {
2972
+ // Background
2973
+ backgrounds.push({
2974
+ tokenId,
2975
+ upc,
2976
+ owner
2977
+ });
2978
+ } else {
2979
+ // Outfit
2980
+ outfits.push({
2981
+ tokenId,
2982
+ upc,
2983
+ category,
2984
+ owner
2985
+ });
2986
+ }
2987
+ });
2988
+
2989
+ // Collect all outfitIds and backgroundIds that are being used
2990
+ // IMPORTANT: We determine which assets are worn/used by looking at outfitIds and backgroundId
2991
+ // on Banny body entries (category === 0). We do NOT use wornByBannyBodyId from outfit/background
2992
+ // entries as it is not reliable in raw.json.
2993
+ const usedOutfitIds = new Set();
2994
+ const usedBackgroundIds = new Set();
2995
+
2996
+ bannys.forEach(banny => {
2997
+ if (banny.backgroundId && banny.backgroundId !== 0) {
2998
+ // Ensure consistent type (number) for Set comparison
2999
+ usedBackgroundIds.add(Number(banny.backgroundId));
3000
+ }
3001
+ banny.outfitIds.forEach(outfitId => {
3002
+ // Ensure consistent type (number) for Set comparison
3003
+ usedOutfitIds.add(Number(outfitId));
3004
+ });
3005
+ });
3006
+
3007
+ // V4 resolver addresses - items owned by these are being worn/used, so exclude them
3008
+ const v4ResolverAddress = '0xa5F8911d4CFd60a6697479f078409434424fe666';
3009
+ const v4ResolverFallback = '0xfF80c37a57016EFf3d19fb286e9C740eC4537Dd3';
3010
+
3011
+ // Find unused outfits and backgrounds
3012
+ // Also filter out items owned by resolver (these are being worn/used)
3013
+ // AND filter out items already processed in chunks 1-3 (or 1-2 for Base)
3014
+ // Track filtered items for logging
3015
+ const filteredItems = [];
3016
+
3017
+ const unusedOutfits = outfits.filter(outfit => {
3018
+ const tokenId = Number(outfit.tokenId);
3019
+ const owner = outfit.owner.toLowerCase();
3020
+ const reasons = [];
3021
+
3022
+ if (usedOutfitIds.has(tokenId)) {
3023
+ reasons.push('being-worn');
3024
+ }
3025
+ if (processedTokenIds.has(tokenId)) {
3026
+ reasons.push('already-processed');
3027
+ }
3028
+ if (owner === '0x0000000000000000000000000000000000000000') {
3029
+ reasons.push('zero-address');
3030
+ }
3031
+ if (owner === v4ResolverAddress.toLowerCase()) {
3032
+ reasons.push('resolver-owned');
3033
+ }
3034
+ if (owner === v4ResolverFallback.toLowerCase()) {
3035
+ reasons.push('resolver-owned');
3036
+ }
3037
+
3038
+ if (reasons.length > 0) {
3039
+ filteredItems.push({
3040
+ tokenId: outfit.tokenId,
3041
+ upc: outfit.upc,
3042
+ owner: outfit.owner,
3043
+ category: 'outfit',
3044
+ reasons: reasons
3045
+ });
3046
+ return false;
3047
+ }
3048
+ return true;
3049
+ });
3050
+
3051
+ const unusedBackgrounds = backgrounds.filter(bg => {
3052
+ const tokenId = Number(bg.tokenId);
3053
+ const owner = bg.owner.toLowerCase();
3054
+ const reasons = [];
3055
+
3056
+ if (usedBackgroundIds.has(tokenId)) {
3057
+ reasons.push('being-worn');
3058
+ }
3059
+ if (processedTokenIds.has(tokenId)) {
3060
+ reasons.push('already-processed');
3061
+ }
3062
+ if (owner === '0x0000000000000000000000000000000000000000') {
3063
+ reasons.push('zero-address');
3064
+ }
3065
+ if (owner === v4ResolverAddress.toLowerCase()) {
3066
+ reasons.push('resolver-owned');
3067
+ }
3068
+ if (owner === v4ResolverFallback.toLowerCase()) {
3069
+ reasons.push('resolver-owned');
3070
+ }
3071
+
3072
+ if (reasons.length > 0) {
3073
+ filteredItems.push({
3074
+ tokenId: bg.tokenId,
3075
+ upc: bg.upc,
3076
+ owner: bg.owner,
3077
+ category: 'background',
3078
+ reasons: reasons
3079
+ });
3080
+ return false;
3081
+ }
3082
+ return true;
3083
+ });
3084
+
3085
+ const unusedItems = [...unusedOutfits, ...unusedBackgrounds];
3086
+
3087
+ // Filter out items with zero address owners (already filtered above, but keeping for clarity)
3088
+ const unusedItemsWithOwners = unusedItems.filter(item =>
3089
+ item.owner !== '0x0000000000000000000000000000000000000000'
3090
+ );
3091
+
3092
+ // Calculate how many items were filtered out
3093
+ // Total items in raw.json for this chain = chainItems.length
3094
+ // Processed in chunks = totalProcessedInChunks (passed as parameter)
3095
+ // Unused items = unusedItemsWithOwners.length
3096
+ // Filtered = total - processed in chunks - unused items
3097
+ const filteredCount = chainItems.length - totalProcessedInChunks - unusedItemsWithOwners.length;
3098
+
3099
+ // Validate that all items have valid token IDs
3100
+ // If a token ID is in raw.json but doesn't exist in V4, this indicates a data issue
3101
+ const invalidItems = unusedItemsWithOwners.filter(item => {
3102
+ // Token IDs should be positive numbers
3103
+ const tokenId = Number(item.tokenId);
3104
+ return !tokenId || tokenId <= 0 || isNaN(tokenId);
3105
+ });
3106
+
3107
+ if (invalidItems.length > 0) {
3108
+ console.error(`ERROR: Found ${invalidItems.length} items with invalid token IDs in unused assets:`);
3109
+ invalidItems.forEach(item => {
3110
+ console.error(` - Token ID: ${item.tokenId}, UPC: ${item.upc}, Owner: ${item.owner}`);
3111
+ });
3112
+ throw new Error(`Invalid token IDs found in unused assets. This indicates a data issue in raw.json.`);
3113
+ }
3114
+
3115
+ if (unusedItemsWithOwners.length === 0) {
3116
+ return null; // No unused assets to migrate
3117
+ }
3118
+
3119
+ // Sort unused items by UPC to match mint order (same as in generateTokenIdArrayForUnused)
3120
+ // This ensures transferOwners and token IDs are in the same order
3121
+ const sortedUnusedItems = [...unusedItemsWithOwners].sort((a, b) => {
3122
+ if (a.upc !== b.upc) {
3123
+ return a.upc - b.upc;
3124
+ }
3125
+ // Within same UPC, maintain original order
3126
+ return unusedItemsWithOwners.indexOf(a) - unusedItemsWithOwners.indexOf(b);
3127
+ });
3128
+
3129
+ // Calculate tier ID quantities for unused items
3130
+ const tierIdQuantities = new Map();
3131
+ sortedUnusedItems.forEach(item => {
3132
+ const upc = item.upc;
3133
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
3134
+ });
3135
+
3136
+ // Build transfer data array (in sorted order to match token ID order)
3137
+ const transferData = sortedUnusedItems.map((item, index) => ({
3138
+ tokenIndex: index,
3139
+ owner: item.owner,
3140
+ tokenId: item.tokenId,
3141
+ upc: item.upc
3142
+ }));
3143
+
3144
+ let contract = `// SPDX-License-Identifier: MIT
3145
+ pragma solidity 0.8.23;
3146
+
3147
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
3148
+ import {JB721TiersHook} from "@bananapus/721-hook-v5/src/JB721TiersHook.sol";
3149
+ import {Banny721TokenUriResolver} from "../src/Banny721TokenUriResolver.sol";
3150
+ import {MigrationHelper} from "./helpers/MigrationHelper.sol";
3151
+
3152
+ /// @notice Migration contract for ${chain.name} to handle standalone outfits and backgrounds
3153
+ /// that are not worn/used by any banny. These assets are minted to this contract
3154
+ /// and then transferred directly to their owners.
3155
+ contract MigrationContract${chain.name}${chain.numChunks + 1} {
3156
+ address[] private transferOwners;
3157
+
3158
+ constructor(address[] memory _transferOwners) {
3159
+ transferOwners = _transferOwners;
3160
+ }
3161
+
3162
+ function executeMigration(
3163
+ address hookAddress,
3164
+ address resolverAddress,
3165
+ address v4HookAddress,
3166
+ address v4ResolverAddress,
3167
+ address fallbackV4ResolverAddress
3168
+ ) external {
3169
+
3170
+ // Validate addresses
3171
+ require(hookAddress != address(0), "Hook address not set");
3172
+ require(resolverAddress != address(0), "Resolver address not set");
3173
+ require(v4HookAddress != address(0), "V4 Hook address not set");
3174
+ require(v4ResolverAddress != address(0), "V4 Resolver address not set");
3175
+ require(fallbackV4ResolverAddress != address(0), "V4 fallback resolver address not set");
3176
+
3177
+ JB721TiersHook hook = JB721TiersHook(hookAddress);
3178
+ IERC721 v4Hook = IERC721(v4HookAddress);
3179
+
3180
+ // ${chain.name} migration - Standalone outfits and backgrounds (${unusedItemsWithOwners.length} items)
3181
+ // These are assets that are NOT being worn/used by any banny
3182
+
3183
+ // Assets are already minted to this contract by the deployer
3184
+ // V5 token IDs are calculated based on mint order (continuing from previous chunks)
3185
+ // V4 token IDs are the original token IDs from V4
3186
+
3187
+ // Generate token IDs - store both V5 minted token IDs and original V4 token IDs
3188
+ uint256[] memory v5TokenIds = new uint256[](transferOwners.length);
3189
+ uint256[] memory v4TokenIds = new uint256[](transferOwners.length);
3190
+ ${generateTokenIdArrayForUnused(sortedUnusedItems, tierIdQuantities, upcStartingUnitNumbers)}
3191
+
3192
+ uint256 successfulTransfers = 0;
3193
+
3194
+ for (uint256 i = 0; i < transferOwners.length; i++) {
3195
+ uint256 v5TokenId = v5TokenIds[i];
3196
+ uint256 v4TokenId = v4TokenIds[i];
3197
+
3198
+ // Verify V4 ownership using the original V4 token ID
3199
+ // This will revert if the token doesn't exist, which indicates a data issue
3200
+ address v4Owner = v4Hook.ownerOf(v4TokenId);
3201
+ address expectedOwner = transferOwners[i];
3202
+
3203
+ // If V4 owner is the main resolver, this token is being worn/used and shouldn't be in unused assets contract
3204
+ require(
3205
+ v4Owner != address(v4ResolverAddress),
3206
+ "Token owned by main resolver in V4 - should not be in unused assets contract"
3207
+ );
3208
+
3209
+ // Special case: If V4 owner is the fallback resolver BUT expected owner is NOT a resolver,
3210
+ // this is valid - the asset is being worn in V4 but we're minting directly to the actual owner in V5
3211
+ // raw.json already accounts for this and has the correct owner
3212
+ if (v4Owner == address(fallbackV4ResolverAddress)) {
3213
+ // Allow if expected owner is not a resolver (we're minting directly to owner in V5)
3214
+ require(
3215
+ expectedOwner != address(v4ResolverAddress) && expectedOwner != address(fallbackV4ResolverAddress),
3216
+ "Token owned by fallback resolver in V4 but expected owner is also a resolver - should not be in unused assets contract"
3217
+ );
3218
+ // Skip ownership verification in this case - we trust raw.json
3219
+ } else {
3220
+ // For all other cases, verify V4 owner matches expected owner
3221
+ require(v4Owner == expectedOwner, "V4/V5 ownership mismatch for token");
3222
+ }
3223
+
3224
+ // Verify this contract owns the V5 token before transferring
3225
+ require(hook.ownerOf(v5TokenId) == address(this), "Contract does not own token");
3226
+
3227
+ // Transfer using the minted V5 token ID
3228
+ IERC721(address(hook)).safeTransferFrom(
3229
+ address(this),
3230
+ transferOwners[i],
3231
+ v5TokenId
3232
+ );
3233
+ successfulTransfers++;
3234
+ }
3235
+
3236
+ // Verify all expected items were transferred
3237
+ require(
3238
+ successfulTransfers == transferOwners.length,
3239
+ "Not all items were transferred"
3240
+ );
3241
+
3242
+ // Final verification: Ensure this contract no longer owns any tokens
3243
+ // This ensures all transfers completed successfully and no tokens were left behind
3244
+ require(hook.balanceOf(address(this)) == 0, "Contract still owns tokens after migration");
3245
+
3246
+ // Verify tier balances: V5 should never exceed V4 (except for tiers owned by fallback resolver in V4)
3247
+ ${generateTierBalanceVerification(transferData, tierIdQuantities)}
3248
+ }
3249
+ }`;
3250
+
3251
+ return {
3252
+ contract,
3253
+ transferData,
3254
+ tierIdQuantities,
3255
+ unusedItems: sortedUnusedItems,
3256
+ filteredCount: filteredCount,
3257
+ filteredItems: filteredItems
3258
+ };
3259
+ }
3260
+
3261
+ function generateUnusedAssetsContractFromItems(chain, unusedItems, upcStartingUnitNumbers, contractNumber) {
3262
+ if (unusedItems.length === 0) {
3263
+ return null;
3264
+ }
3265
+
3266
+ // Sort unused items by UPC to match mint order
3267
+ const sortedUnusedItems = [...unusedItems].sort((a, b) => {
3268
+ if (a.upc !== b.upc) {
3269
+ return a.upc - b.upc;
3270
+ }
3271
+ return unusedItems.indexOf(a) - unusedItems.indexOf(b);
3272
+ });
3273
+
3274
+ // Calculate tier ID quantities for unused items
3275
+ const tierIdQuantities = new Map();
3276
+ sortedUnusedItems.forEach(item => {
3277
+ const upc = item.upc;
3278
+ tierIdQuantities.set(upc, (tierIdQuantities.get(upc) || 0) + 1);
3279
+ });
3280
+
3281
+ // Build transfer data array
3282
+ const transferData = sortedUnusedItems.map((item, index) => ({
3283
+ tokenIndex: index,
3284
+ owner: item.owner,
3285
+ tokenId: item.tokenId,
3286
+ upc: item.upc
3287
+ }));
3288
+
3289
+ let contract = `// SPDX-License-Identifier: MIT
3290
+ pragma solidity 0.8.23;
3291
+
3292
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
3293
+ import {JB721TiersHook} from "@bananapus/721-hook-v5/src/JB721TiersHook.sol";
3294
+ import {Banny721TokenUriResolver} from "../src/Banny721TokenUriResolver.sol";
3295
+ import {MigrationHelper} from "./helpers/MigrationHelper.sol";
3296
+
3297
+ /// @notice Migration contract for ${chain.name} to handle standalone outfits and backgrounds
3298
+ /// that are not worn/used by any banny. These assets are minted to this contract
3299
+ /// and then transferred directly to their owners.
3300
+ contract MigrationContract${chain.name}${contractNumber} {
3301
+ address[] private transferOwners;
3302
+
3303
+ constructor(address[] memory _transferOwners) {
3304
+ transferOwners = _transferOwners;
3305
+ }
3306
+
3307
+ function executeMigration(
3308
+ address hookAddress,
3309
+ address resolverAddress,
3310
+ address v4HookAddress,
3311
+ address v4ResolverAddress,
3312
+ address fallbackV4ResolverAddress
3313
+ ) external {
3314
+
3315
+ // Validate addresses
3316
+ require(hookAddress != address(0), "Hook address not set");
3317
+ require(resolverAddress != address(0), "Resolver address not set");
3318
+ require(v4HookAddress != address(0), "V4 Hook address not set");
3319
+ require(v4ResolverAddress != address(0), "V4 Resolver address not set");
3320
+ require(fallbackV4ResolverAddress != address(0), "V4 fallback resolver address not set");
3321
+
3322
+ JB721TiersHook hook = JB721TiersHook(hookAddress);
3323
+ IERC721 v4Hook = IERC721(v4HookAddress);
3324
+
3325
+ // ${chain.name} migration - Standalone outfits and backgrounds (${sortedUnusedItems.length} items)
3326
+ // These are assets that are NOT being worn/used by any banny
3327
+
3328
+ // Assets are already minted to this contract by the deployer
3329
+ // V5 token IDs are calculated based on mint order (continuing from previous chunks)
3330
+ // V4 token IDs are the original token IDs from V4
3331
+
3332
+ // Generate token IDs - store both V5 minted token IDs and original V4 token IDs
3333
+ uint256[] memory v5TokenIds = new uint256[](transferOwners.length);
3334
+ uint256[] memory v4TokenIds = new uint256[](transferOwners.length);
3335
+ ${generateTokenIdArrayForUnused(sortedUnusedItems, tierIdQuantities, upcStartingUnitNumbers)}
3336
+
3337
+ uint256 successfulTransfers = 0;
3338
+
3339
+ for (uint256 i = 0; i < transferOwners.length; i++) {
3340
+ uint256 v5TokenId = v5TokenIds[i];
3341
+ uint256 v4TokenId = v4TokenIds[i];
3342
+
3343
+ // Verify V4 ownership using the original V4 token ID
3344
+ address v4Owner = v4Hook.ownerOf(v4TokenId);
3345
+ address expectedOwner = transferOwners[i];
3346
+
3347
+ require(
3348
+ v4Owner != address(v4ResolverAddress),
3349
+ "Token owned by main resolver in V4 - should not be in unused assets contract"
3350
+ );
3351
+
3352
+ if (v4Owner == address(fallbackV4ResolverAddress)) {
3353
+ require(
3354
+ expectedOwner != address(v4ResolverAddress) && expectedOwner != address(fallbackV4ResolverAddress),
3355
+ "Token owned by fallback resolver in V4 but expected owner is also a resolver - should not be in unused assets contract"
3356
+ );
3357
+ } else {
3358
+ require(v4Owner == expectedOwner, "V4/V5 ownership mismatch for token");
3359
+ }
3360
+
3361
+ require(hook.ownerOf(v5TokenId) == address(this), "Contract does not own token");
3362
+
3363
+ IERC721(address(hook)).safeTransferFrom(
3364
+ address(this),
3365
+ transferOwners[i],
3366
+ v5TokenId
3367
+ );
3368
+ successfulTransfers++;
3369
+ }
3370
+
3371
+ require(
3372
+ successfulTransfers == transferOwners.length,
3373
+ "Not all items were transferred"
3374
+ );
3375
+
3376
+ require(hook.balanceOf(address(this)) == 0, "Contract still owns tokens after migration");
3377
+
3378
+ // Verify tier balances: V5 should never exceed V4 (except for tiers owned by fallback resolver in V4)
3379
+ ${generateTierBalanceVerification(transferData, tierIdQuantities)}
3380
+ }
3381
+ }`;
3382
+
3383
+ return contract;
3384
+ }
3385
+
3386
+ function generateTokenIdArrayForUnused(unusedItems, tierIdQuantities, upcStartingUnitNumbers) {
3387
+ // Build mapping from unused items to their minted token IDs
3388
+ // Tokens are minted in UPC-sorted order, and token IDs continue from previous chunks
3389
+ // Formula: upc * 1000000000 + unitNumber (where unitNumber continues from previous chunks)
3390
+ const upcCounters = new Map();
3391
+ const itemToMintedTokenId = new Map();
3392
+
3393
+ // Sort unused items by UPC to match mint order
3394
+ // Note: unusedItems should already be sorted when passed in, but we sort again to be safe
3395
+ const sortedByUpc = [...unusedItems].sort((a, b) => {
3396
+ if (a.upc !== b.upc) {
3397
+ return a.upc - b.upc;
3398
+ }
3399
+ // Within same UPC, maintain original order from the passed array
3400
+ return unusedItems.indexOf(a) - unusedItems.indexOf(b);
3401
+ });
3402
+
3403
+ // Calculate minted token IDs based on mint order
3404
+ sortedByUpc.forEach(item => {
3405
+ const upc = item.upc;
3406
+ const counter = (upcCounters.get(upc) || 0) + 1;
3407
+ upcCounters.set(upc, counter);
3408
+
3409
+ // Calculate the actual minted token ID
3410
+ // unitNumber = startingUnitNumber (from previous chunks) + counter - 1
3411
+ const startingUnitNumber = upcStartingUnitNumbers.get(upc) || 1;
3412
+ const unitNumber = startingUnitNumber + counter - 1;
3413
+ const mintedTokenId = upc * 1000000000 + unitNumber;
3414
+
3415
+ // Map the original item to its minted token ID
3416
+ itemToMintedTokenId.set(item, mintedTokenId);
3417
+ });
3418
+
3419
+ // Generate code in the order of sortedByUpc (mint order) to match token ID calculation
3420
+ // Store both V5 minted token IDs and original V4 token IDs
3421
+ let code = '';
3422
+ sortedByUpc.forEach((item, index) => {
3423
+ const mintedTokenId = itemToMintedTokenId.get(item);
3424
+ const v4TokenId = item.tokenId;
3425
+
3426
+ if (mintedTokenId) {
3427
+ // Store both V5 minted token ID and original V4 token ID
3428
+ code += ` v5TokenIds[${index}] = ${mintedTokenId}; // Minted V5 Token ID\n`;
3429
+ code += ` v4TokenIds[${index}] = ${v4TokenId}; // Original V4 Token ID\n`;
3430
+ } else {
3431
+ // Fallback: use V4 token ID for both (shouldn't happen)
3432
+ code += ` v5TokenIds[${index}] = ${v4TokenId}; // Fallback: V4 token ID\n`;
3433
+ code += ` v4TokenIds[${index}] = ${v4TokenId}; // Fallback: V4 token ID\n`;
3434
+ }
3435
+ });
3436
+
3437
+ return code;
3438
+ }
3439
+
3440
+ // Run the script generation
3441
+ generateMigrationScript();