Skip to content

Commit

Permalink
fix: Add Linea import script
Browse files Browse the repository at this point in the history
  • Loading branch information
alainncls committed Nov 17, 2023
1 parent 9163ef8 commit 56fd2da
Show file tree
Hide file tree
Showing 3 changed files with 847 additions and 126 deletions.
3 changes: 3 additions & 0 deletions contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@
"hardhat": "^2.19.0",
"solhint": "^3.6.2",
"solhint-plugin-prettier": "^0.0.5"
},
"dependencies": {
"@consensys/linea-sdk": "^0.1.6"
}
}
386 changes: 386 additions & 0 deletions contracts/script/massImport/lineaImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,386 @@
import fs from "fs";
import { config } from "dotenv";
import path from "path";
import { EIP1559GasProvider } from "@consensys/linea-sdk";
import {
isAddress,
isHexString,
JsonRpcProvider,
parseUnits,
TransactionRequest,
TransactionResponse,
Wallet,
} from "ethers";
import { defaultAbiCoder } from "@ethersproject/abi";
import { hexConcat } from "@ethersproject/bytes";

config();

const processedBatchIds: number[] = [];

// *********************************************************************************
// ********************************* CONFIGURATION *********************************
// *********************************************************************************

const DEFAULT_MAX_FEE_PER_GAS = parseUnits("100", "gwei").toString();
const DEFAULT_GAS_ESTIMATION_PERCENTILE = "10";
const DEFAULT_GAS_PRICE_CAP = parseUnits("5", "gwei").toString();

type Config = {
inputFile: string;
destinationAddress: string;
providerUrl: string;
signerPrivateKey: string;
maxFeePerGas: number;
gasEstimationPercentile: number;
gasPriceCap: string;
};

type Batch = {
id: number;
recipients: string[];
amount: number;
};

enum BatchStatuses {
Failed = "Failed",
Success = "Success",
Pending = "Pending",
}

type TrackingData = {
recipients: string[];
tokenAmount: number;
status: BatchStatuses;
transactionHash?: string;
error?: unknown;
};

function isValidUrl(urlString: string): boolean {
try {
return Boolean(new URL(urlString));
} catch (e) {
return false;
}
}

function requireEnv(name: string): string {
const envVariable = process.env[name];
if (!envVariable) {
throw new Error(`Missing ${name} environment variable.`);
}
return envVariable;
}

function getConfig(): Config {
const inputFile = requireEnv("INPUT_FILE");
const destinationAddress = requireEnv("DESTINATION_ADDRESS");
const providerUrl = requireEnv("PROVIDER_URL");
const signerPrivateKey = requireEnv("SIGNER_PRIVATE_KEY");

if (!isAddress(destinationAddress)) {
throw new Error(`Destination address is not a valid Ethereum address.`);
}

if (!isValidUrl(providerUrl)) {
throw new Error(`Invalid provider URL.`);
}

if (!isHexString(signerPrivateKey, 64)) {
throw new Error(`Signer private key must be hexadecimal string of length 64`);
}

if (path.extname(inputFile) !== ".json") {
throw new Error(`File ${inputFile} is not a JSON file.`);
}

if (!fs.existsSync(inputFile)) {
throw new Error(`File ${inputFile} does not exist.`);
}

return {
inputFile,
destinationAddress,
providerUrl,
signerPrivateKey,
maxFeePerGas: parseInt(process.env.MAX_FEE_PER_GAS ?? DEFAULT_MAX_FEE_PER_GAS),
gasEstimationPercentile: parseInt(process.env.GAS_ESTIMATION_PERCENTILE ?? DEFAULT_GAS_ESTIMATION_PERCENTILE),
gasPriceCap: process.env.GAS_PRICE_CAP ?? DEFAULT_GAS_PRICE_CAP,
};
}

// *********************************************************************************
// ********************************* UTILS FUNCTIONS *******************************
// *********************************************************************************

export const wait = (timeout: number) => new Promise((resolve) => setTimeout(resolve, timeout));

async function estimateTransactionGas(signer: Wallet, transaction: TransactionRequest): Promise<BigNumber> {
try {
return signer.estimateGas(transaction);
} catch (error: unknown) {
throw new Error(`GasEstimationError: ${JSON.stringify(error)}`);
}
}

async function executeTransaction(
signer: Wallet,
transaction: TransactionRequest,
batch: Batch,
): Promise<{ transactionResponse: TransactionResponse; batch: Batch }> {
try {
return {
transactionResponse: await signer.sendTransaction(transaction),
batch,
};
} catch (error: unknown) {
throw new Error(`TransactionError: ${JSON.stringify(error)}`);
}
}

function createTrackingFile(path: string): Map<number, TrackingData> {
if (fs.existsSync(path)) {
const mapAsArray = fs.readFileSync(path, "utf-8");
return new Map(JSON.parse(mapAsArray));
}

fs.writeFileSync(path, JSON.stringify(Array.from(new Map<number, TrackingData>().entries())));
return new Map<number, TrackingData>();
}

function updateTrackingFile(trackingData: Map<number, TrackingData>) {
fs.writeFileSync("tracking.json", JSON.stringify(Array.from(trackingData.entries()), null, 2));
}

async function processPendingBatches(
provider: JsonRpcProvider,
batches: Batch[],
trackingData: Map<number, TrackingData>,
): Promise<(Batch & { transactionHash?: string })[]> {
const pendingBatches = batches
.filter((batch) => trackingData.get(batch.id)?.status === BatchStatuses.Pending)
.map((batch) => ({
...batch,
transactionHash: trackingData.get(batch.id)?.transactionHash,
}));

const remainingPendingBatches: (Batch & { transactionHash?: string })[] = [];

for (const { transactionHash, id, recipients, amount } of pendingBatches) {
if (!transactionHash) {
remainingPendingBatches.push({ id, recipients, amount });
continue;
}

const receipt = await provider.getTransactionReceipt(transactionHash);

if (!receipt) {
remainingPendingBatches.push({ id, recipients, amount, transactionHash });
continue;
}

if (receipt.status == 0) {
// track failing batches
trackingData.set(id, {
recipients,
tokenAmount: amount,
status: BatchStatuses.Failed,
transactionHash,
});

console.log(`Transaction reverted. Hash: ${transactionHash}, batchId: ${id}`);
updateTrackingFile(trackingData);

// continue the batch loop
continue;
}
// track succeded batches
trackingData.set(id, {
recipients,
tokenAmount: amount,
status: BatchStatuses.Success,
transactionHash: transactionHash,
});

updateTrackingFile(trackingData);
console.log(`Transaction succeed. Hash: ${transactionHash}, batchId: ${id}`);
}

return remainingPendingBatches;
}

// *********************************************************************************
// ********************************* MAIN FUNCTION *********************************
// *********************************************************************************

async function main() {
const {
inputFile,
destinationAddress,
providerUrl,
signerPrivateKey,
maxFeePerGas,
gasEstimationPercentile,
gasPriceCap,
} = getConfig();

const provider = new JsonRpcProvider(providerUrl);
const { chainId } = await provider.getNetwork();
const eip1559GasProvider = new EIP1559GasProvider(provider, maxFeePerGas, gasEstimationPercentile);
const signer = new Wallet(signerPrivateKey, provider);

const trackingData = createTrackingFile("tracking.json");

const readFile = fs.readFileSync(inputFile, "utf-8");
const batches: Batch[] = JSON.parse(readFile);

const filteredBatches = batches.filter(
(batch) => trackingData.get(batch.id)?.status === BatchStatuses.Failed || !trackingData.has(batch.id),
);

console.log("Processing pending batches...");
const remainingPendingBatches = await processPendingBatches(provider, batches, trackingData);

if (remainingPendingBatches.length !== 0) {
console.warn(`The following batches are still pending: ${JSON.stringify(remainingPendingBatches, null, 2)}`);
return;
}

let nonce = await provider.getTransactionCount(signer.address);

const pendingTransactions = [];

console.log(`Total number of batches to process: ${filteredBatches.length}.`);

for (const [index, batch] of filteredBatches.entries()) {
try {
const encodedBatchMintCall = hexConcat([
"0x83b74baa",
defaultAbiCoder.encode(["address[]", "uint256"], [batch.recipients, parseUnits(batch.amount.toString())]),
]);

const encodedExecuteTransactionWithRole = hexConcat([
"0x6928e74b",
defaultAbiCoder.encode(
["address", "uint256", "bytes", "uint8", "uint16", "bool"],
[destinationAddress, 0, encodedBatchMintCall, 0, 1, true],
),
]);

let fees = await eip1559GasProvider.get1559Fees();

while (fees.maxFeePerGas.gt(gasPriceCap)) {
console.warn(`Max fee per gas (${fees.maxFeePerGas.toString()}) exceeds gas price cap (${gasPriceCap})`);

const currentBlockNumber = await provider.getBlockNumber();
while ((await provider.getBlockNumber()) === currentBlockNumber) {
console.warn(`Waiting for next block: ${currentBlockNumber}`);
await wait(4_000);
}

fees = await eip1559GasProvider.get1559Fees();
}

const transactionRequest: TransactionRequest = {
to: zodiacRolesModifierAddress,
value: 0,
type: 2,
data: encodedExecuteTransactionWithRole,
chainId,
maxFeePerGas: fees.maxFeePerGas,
maxPriorityFeePerGas: fees.maxPriorityFeePerGas,
nonce,
};

const transactionGasLimit = await estimateTransactionGas(signer, transactionRequest);

const transaction: TransactionRequest = {
...transactionRequest,
gasLimit: transactionGasLimit,
};

const transactionInfo = await executeTransaction(signer, transaction, batch);
pendingTransactions.push(transactionInfo);

trackingData.set(batch.id, {
recipients: batch.recipients,
tokenAmount: batch.amount,
status: BatchStatuses.Pending,
transactionHash: transactionInfo.transactionResponse.hash,
});

updateTrackingFile(trackingData);

processedBatchIds.push(batch.id);

console.log(`Batch with ID=${batch.id} sent.\n ${JSON.stringify(batch)}\n`);
nonce = nonce + 1;
} catch (error) {
trackingData.set(batch.id, {
recipients: batch.recipients,
tokenAmount: batch.amount,
status: BatchStatuses.Failed,
error,
});
updateTrackingFile(trackingData);
console.error(`Batch with ID=${batch.id} failed.\n Stopping script execution.`);
return;
}

if (index + (1 % 15) === 0) {
console.log(`Pause the execution for 60 seconds...`);
await wait(60_000);
}
}

if (pendingTransactions.length !== 0) {
console.log(`Waiting for all receipts...`);
}

const transactionsInfos = await Promise.all(
pendingTransactions.map(async ({ transactionResponse, batch }) => {
return {
transactionReceipt: await transactionResponse.wait(),
batch,
};
}),
);

for (const { batch, transactionReceipt } of transactionsInfos) {
if (transactionReceipt.status == 0) {
trackingData.set(batch.id, {
recipients: batch.recipients,
tokenAmount: batch.amount,
status: BatchStatuses.Failed,
transactionHash: transactionReceipt.transactionHash,
});

console.log(`Transaction reverted. Hash: ${transactionReceipt.transactionHash}, batchId: ${batch.id}`);
updateTrackingFile(trackingData);
continue;
}

trackingData.set(batch.id, {
recipients: batch.recipients,
tokenAmount: batch.amount,
status: BatchStatuses.Success,
transactionHash: transactionReceipt.transactionHash,
});

updateTrackingFile(trackingData);
console.log(`Transaction succeed. Hash: ${transactionReceipt.transactionHash}, batchId: ${batch.id}`);
}
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

process.on("SIGINT", () => {
console.log(`Processed batches: ${JSON.stringify(processedBatchIds, null, 2)}`);
console.log("\nGracefully shutting down from SIGINT (Ctrl-C)");
process.exit(1);
});
Loading

0 comments on commit 56fd2da

Please sign in to comment.