diff --git a/contracts/package.json b/contracts/package.json index ef010325..f23fccb1 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -43,6 +43,8 @@ "deploy:post:scroll-sepolia": "npx hardhat run --network scroll-sepolia script/deploy/postDeployment.ts", "encode": "npx hardhat run script/encode.ts", "lint": "pnpm solhint \"{script,src,test}/**/*.sol\"", + "massImport": "npx hardhat run --network linea script/massImport/massImport.ts", + "massImport:goerli": "npx hardhat run --network linea-goerli script/massImport/massImport.ts", "reimport": "npx hardhat run --network linea script/recreateNetworkFile.ts", "reimport:goerli": "npx hardhat run --network linea-goerli script/recreateNetworkFile.ts", "reimport:scroll-sepolia": "npx hardhat run --network scroll-sepolia script/recreateNetworkFile.ts", diff --git a/contracts/script/massImport/massImport.ts b/contracts/script/massImport/massImport.ts new file mode 100644 index 00000000..44198d62 --- /dev/null +++ b/contracts/script/massImport/massImport.ts @@ -0,0 +1,167 @@ +import { AbiCoder, Contract, formatEther, formatUnits, parseUnits } from "ethers"; +import { ethers } from "hardhat"; +import source from "./source.json"; + +const MAX_GAS_PRICE = parseUnits("0.7", "gwei"); // Set your maximum value here +const BLOCK_LIMIT_FACTOR = 80; +const BLOCK_TIME = 12; // Average block generation time in Ethereum, in seconds +const PORTAL_ADDRESS = "0xb3c0e57d560f36697f5d727c2c6db4e0c8f87bd8"; +const BATCH_LENGTH = 100; + +let lastBlockNumber: number | null = null; + +interface AttestationPayload { + schemaId?: string; + expirationDate?: number; + subject: string; + attestationData: string; +} + +async function callMassImport(batches: AttestationPayload[][], attestationRegistry: Contract) { + if (batches.length === 0) { + return; + } + + // TODO: check batches length is decreasing regularly + + const batch = batches.pop(); + + if (!batch) { + return; + } + + // Retrieve information about the last block + const lastBlock = await ethers.provider.getBlock("latest"); + + try { + if (!lastBlock) { + batches.unshift(batch); // Put the batch back in the list + setTimeout(callMassImport, BLOCK_TIME * 1000, batches, attestationRegistry); // Wait for a block generation time before retrying + return; + } + + // Check if a transaction has already been sent in this block, or the previous block + if ( + lastBlockNumber !== null && + (lastBlockNumber === lastBlock.number || lastBlockNumber === lastBlock.number - 1) + ) { + console.log(`Waiting for a new block to send the transaction.`); + batches.unshift(batch); // Put the batch back in the list + setTimeout(callMassImport, BLOCK_TIME * 1000, batches, attestationRegistry); // Wait for a block generation time before retrying + return; + } + + // Retrieve the current gas price + const gasPrice = (await ethers.provider.getFeeData()).gasPrice; + + // Check if the gas price is defined + if (!gasPrice) { + console.log(`Gas price is unknown. Aborting transaction.`); + batches.unshift(batch); // Put the batch back in the list + setTimeout(callMassImport, BLOCK_TIME * 1000, batches, attestationRegistry); // Wait for a block generation time before retrying + return; + } + + // Check if the gas price is acceptable + if (gasPrice > MAX_GAS_PRICE) { + console.log(`Gas price of ${formatUnits(gasPrice, "gwei")} gwei is too high. Aborting transaction.`); + batches.unshift(batch); // Put the batch back in the list + setTimeout(callMassImport, BLOCK_TIME * 1000, batches, attestationRegistry); // Wait for a block generation time before retrying + return; + } + + // Calculate the gas limit based on the last block + const maxGas = (lastBlock.gasLimit * BigInt(BLOCK_LIMIT_FACTOR)) / BigInt(100); + + const gasEstimated = await attestationRegistry.massImport.estimateGas(batch, PORTAL_ADDRESS); + + if (gasEstimated > maxGas) { + console.log( + `Transaction estimated gas is ${formatUnits(gasEstimated, "wei")}, higher than the max (${formatUnits( + maxGas, + "wei", + )}). Aborting transaction.`, + ); + batches.unshift(batch); // Put the batch back in the list + setTimeout(callMassImport, BLOCK_TIME * 1000, batches, attestationRegistry); // Wait for a block generation time before retrying + return; + } + + console.log( + `Sending a transaction with a gas price of ${formatUnits( + gasPrice.toString(), + "gwei", + )} gwei and an estimated gas of ${formatUnits(gasEstimated, "gwei")} for an estimated total of ${formatEther( + gasEstimated * gasPrice, + )} ETH`, + ); + + // Call the contract method + const txResponse = await attestationRegistry.massImport(batch, PORTAL_ADDRESS, { + gasPrice: gasPrice, + }); + + console.log(`Transaction sent with hash: ${txResponse.hash}`); + + // Wait for the transaction receipt + const receipt = await txResponse.wait(); + + console.log(`Transaction successfully confirmed in block ${receipt.blockNumber}`); + + // Update the number of the last block + lastBlockNumber = lastBlock.number; + + console.log(`There are ${batches.length} batches left`); + + // Recursively call the function for the next transaction + callMassImport(batches, attestationRegistry); + } catch (error: unknown) { + assertIsError(error); + console.log(`Transaction failed with error: ${error.message}. Retrying...`); + batches.unshift(batch); // Put the batch back in the list + setTimeout(callMassImport, BLOCK_TIME * 1000, batches, attestationRegistry); // Wait for a block generation time before retrying + } +} + +async function main() { + const proxyAddress = process.env.ATTESTATION_REGISTRY_ADDRESS ?? ""; + const attestationRegistry = await ethers.getContractAt("AttestationRegistry", proxyAddress); + + const abiCoder = new AbiCoder(); + + const rawPayloads: AttestationPayload[] = source.map((item) => ({ + ...item, + subject: abiCoder.encode(["address"], [item.subject]), + attestationData: abiCoder.encode(["uint8"], [item.attestationData]), + })); + + const attestationPayloads: AttestationPayload[] = []; + + for (let i = 0; i < 1000; i++) { + attestationPayloads.push(...rawPayloads); + } + + console.log(`${attestationPayloads.length} total payloads to attest`); + + const batches: AttestationPayload[][] = Array.from( + { length: Math.ceil(attestationPayloads.length / BATCH_LENGTH) }, + (v, i) => attestationPayloads.slice(i * BATCH_LENGTH, i * BATCH_LENGTH + BATCH_LENGTH), + ).reverse(); + + console.log(`${batches.length} batches of payloads to attest`); + + await callMassImport(batches, attestationRegistry); +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); + +function assertIsError(error: unknown): asserts error is Error { + if (!(error instanceof Error)) { + throw error; + } +} diff --git a/script/massImport/source.json b/script/massImport/source.json new file mode 100644 index 00000000..7248e1fd --- /dev/null +++ b/script/massImport/source.json @@ -0,0 +1,68 @@ +[ + { + "schemaId": "0xd1664d97bd195df77e3d5fe78c1737ab3adaa38bbe52a680d1aa30fa51f186ba", + "expirationDate": 1793835110, + "subject": "0xf1f5881ebc8b1bcb8df89faae642cbaba83f4940", + "attestationData": 1 + }, + { + "schemaId": "0xd1664d97bd195df77e3d5fe78c1737ab3adaa38bbe52a680d1aa30fa51f186ba", + "expirationDate": 1793835110, + "subject": "0x7f60b39986383551002e7bb54b6bc7a73c4b4ee8", + "attestationData": 2 + }, + { + "schemaId": "0xd1664d97bd195df77e3d5fe78c1737ab3adaa38bbe52a680d1aa30fa51f186ba", + "expirationDate": 1793835110, + "subject": "0x547b324b3f9e1f9f436fede6e88ae1ca816db6f3", + "attestationData": 3 + }, + { + "schemaId": "0xd1664d97bd195df77e3d5fe78c1737ab3adaa38bbe52a680d1aa30fa51f186ba", + "expirationDate": 1793835110, + "subject": "0xcb859f99f84ab770a50380680be94ad9331bcec5", + "attestationData": 4 + }, + { + "schemaId": "0xd1664d97bd195df77e3d5fe78c1737ab3adaa38bbe52a680d1aa30fa51f186ba", + "expirationDate": 1793835110, + "subject": "0x59cf6818b9e90cd73b2120ac621c0b54a99c8340", + "attestationData": 5 + }, + { + "schemaId": "0xd1664d97bd195df77e3d5fe78c1737ab3adaa38bbe52a680d1aa30fa51f186ba", + "expirationDate": 1793835110, + "subject": "0x591339da9cebef23f161710b1385d53cae1f3c6a", + "attestationData": 1 + }, + { + "schemaId": "0xd1664d97bd195df77e3d5fe78c1737ab3adaa38bbe52a680d1aa30fa51f186ba", + "expirationDate": 1793835110, + "subject": "0x1ead1b980c754b69b2ab59f1abb6bca900c2073a", + "attestationData": 2 + }, + { + "schemaId": "0xd1664d97bd195df77e3d5fe78c1737ab3adaa38bbe52a680d1aa30fa51f186ba", + "expirationDate": 1793835110, + "subject": "0xed6c7974d8a9ec60644c4f49861dc3bb752ee123", + "attestationData": 3 + }, + { + "schemaId": "0xd1664d97bd195df77e3d5fe78c1737ab3adaa38bbe52a680d1aa30fa51f186ba", + "expirationDate": 1793835110, + "subject": "0xd1b59274c6682e8d6201976e95e971b8affaccd0", + "attestationData": 4 + }, + { + "schemaId": "0xd1664d97bd195df77e3d5fe78c1737ab3adaa38bbe52a680d1aa30fa51f186ba", + "expirationDate": 1793835110, + "subject": "0x19383854824e6ed0270eedf7c3d56896e8c6e32e", + "attestationData": 5 + }, + { + "schemaId": "0xd1664d97bd195df77e3d5fe78c1737ab3adaa38bbe52a680d1aa30fa51f186ba", + "expirationDate": 1793835110, + "subject": "0x5cf6bb1765f5c1a3a12953deb26ff82ea4043acc", + "attestationData": 1 + } +]