@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.
- package/README.md +53 -0
- package/SKILLS.md +94 -0
- package/deployments/banny-core-v5/arbitrum/Banny721TokenUriResolver.json +1809 -0
- package/deployments/banny-core-v5/arbitrum_sepolia/Banny721TokenUriResolver.json +1795 -0
- package/deployments/banny-core-v5/base/Banny721TokenUriResolver.json +1810 -0
- package/deployments/banny-core-v5/base_sepolia/Banny721TokenUriResolver.json +1796 -0
- package/deployments/banny-core-v5/ethereum/Banny721TokenUriResolver.json +1795 -0
- package/deployments/banny-core-v5/optimism/Banny721TokenUriResolver.json +1810 -0
- package/deployments/banny-core-v5/optimism_sepolia/Banny721TokenUriResolver.json +1796 -0
- package/deployments/banny-core-v5/sepolia/Banny721TokenUriResolver.json +1795 -0
- package/foundry.toml +22 -0
- package/package.json +53 -0
- package/remappings.txt +1 -0
- package/script/1.Fix.s.sol +290 -0
- package/script/Add.Denver.s.sol +75 -0
- package/script/AirdropOutfits.s.sol +2302 -0
- package/script/Deploy.s.sol +440 -0
- package/script/Drop1.s.sol +979 -0
- package/script/MigrationContractArbitrum.sol +494 -0
- package/script/MigrationContractArbitrum1.sol +170 -0
- package/script/MigrationContractArbitrum2.sol +204 -0
- package/script/MigrationContractArbitrum3.sol +174 -0
- package/script/MigrationContractArbitrum4.sol +478 -0
- package/script/MigrationContractBase1.sol +444 -0
- package/script/MigrationContractBase2.sol +175 -0
- package/script/MigrationContractBase3.sol +309 -0
- package/script/MigrationContractBase4.sol +350 -0
- package/script/MigrationContractBase5.sol +259 -0
- package/script/MigrationContractEthereum1.sol +468 -0
- package/script/MigrationContractEthereum2.sol +306 -0
- package/script/MigrationContractEthereum3.sol +349 -0
- package/script/MigrationContractEthereum4.sol +352 -0
- package/script/MigrationContractEthereum5.sol +354 -0
- package/script/MigrationContractEthereum6.sol +270 -0
- package/script/MigrationContractEthereum7.sol +439 -0
- package/script/MigrationContractEthereum8.sol +385 -0
- package/script/MigrationContractOptimism.sol +196 -0
- package/script/helpers/BannyverseDeploymentLib.sol +73 -0
- package/script/helpers/MigrationHelper.sol +155 -0
- package/script/outfit_drop/generate-migration.js +3441 -0
- package/script/outfit_drop/raw.json +43276 -0
- package/slither-ci.config.json +10 -0
- package/sphinx.lock +521 -0
- package/src/Banny721TokenUriResolver.sol +1288 -0
- package/src/interfaces/IBanny721TokenUriResolver.sol +137 -0
- package/test/Banny721TokenUriResolver.t.sol +669 -0
- package/test/BannyAttacks.t.sol +322 -0
- 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();
|