diff --git a/src/lazy-migrations/004-create-contract-metadata.ts b/src/lazy-migrations/004-create-contract-metadata.ts new file mode 100644 index 0000000..3a9aed5 --- /dev/null +++ b/src/lazy-migrations/004-create-contract-metadata.ts @@ -0,0 +1,201 @@ +/* + Create contract metadata for a given contract address + +Ex: ./node_modules/.bin/ts-node src/lazy-migrations/004-create-contract-metadata.ts \ + --url ws://localhost:9944 \ + --account-priv-key \ + --limit 1000 +*/ +import yargs from "yargs"; +import "@polkadot/api-augment"; +import "@moonbeam-network/api-augment"; +import { Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { getApiFor, NETWORK_YARGS_OPTIONS } from "../utils/networks"; +import { + monitorSubmittedExtrinsic, + waitForAllMonitoredExtrinsics, +} from "../utils/monitoring"; +import { ALITH_PRIVATE_KEY } from "../utils/constants"; +import fs from "fs"; +import path from "path"; + +const argv = yargs(process.argv.slice(2)) + .usage("Usage: $0") + .version("1.0.0") + .options({ + ...NETWORK_YARGS_OPTIONS, + "account-priv-key": { + type: "string", + demandOption: false, + alias: "account", + }, + limit: { + type: "number", + default: 100, + describe: + "The maximum number of storage entries to be removed by this call", + }, + alith: { + type: "boolean", + demandOption: false, + conflicts: ["account-priv-key"], + }, + }) + .check((argv) => { + if (!(argv["account-priv-key"] || argv["alith"])) { + throw new Error("Missing --account-priv-key or --alith"); + } + return true; + }).argv; + +interface MigrationDB { + pending_contracts: string[]; + migrated_contracts: string[]; + failed_contracts: Record; +} + +async function main() { + const api = await getApiFor(argv); + const keyring = new Keyring({ type: "ethereum" }); + + const chain = (await api.rpc.system.chain()) + .toString() + .toLowerCase() + .replace(/\s/g, "-"); + const INPUT_FILE = path.resolve( + __dirname, + `contracts-without-metadata-addresses-${chain}-db.json`, + ); + const PROGRESS_FILE = path.resolve( + __dirname, + `contract-without-metadata-migration-progress--${chain}.json`, + ); + + // Initialize or load progress DB + let db: MigrationDB = { + pending_contracts: [], + migrated_contracts: [], + failed_contracts: {}, + }; + + try { + // Load addresses to migrate + if (!fs.existsSync(INPUT_FILE)) { + throw new Error(`Input file ${INPUT_FILE} not found`); + } + const addresses = JSON.parse(fs.readFileSync(INPUT_FILE, "utf8")); + + // Load existing progress + if (fs.existsSync(PROGRESS_FILE)) { + db = JSON.parse(fs.readFileSync(PROGRESS_FILE, "utf8")); + } else { + db.pending_contracts = addresses; + } + + const limit = argv["limit"]; + let account: KeyringPair; + let nonce; + + // Setup account + const privKey = argv["alith"] + ? ALITH_PRIVATE_KEY + : argv["account-priv-key"]; + if (privKey) { + account = keyring.addFromUri(privKey, null, "ethereum"); + const { nonce: rawNonce } = await api.query.system.account( + account.address, + ); + nonce = BigInt(rawNonce.toString()); + } + + // Get contracts to process in this run + const contractsToProcess = db.pending_contracts.slice(0, limit); + console.log( + `Submitting transactions for ${contractsToProcess.length} contracts...`, + ); + + // Submit all transactions first + for (const contract of contractsToProcess) { + // Check if already have metadata + const has_metadata = await api.query.evm.accountCodesMetadata(contract); + if (!has_metadata.isEmpty) { + db.migrated_contracts.push(contract); + db.pending_contracts = db.pending_contracts.filter( + (addr) => addr !== contract, + ); + continue; + } + + try { + const tx = + api.tx["moonbeamLazyMigrations"].createContractMetadata(contract); + await tx.signAndSend( + account, + { nonce: nonce++ }, + monitorSubmittedExtrinsic(api, { id: `migration-${contract}` }), + ); + console.log(`Submitted transaction for ${contract}`); + } catch (error) { + console.error(`Failed to submit transaction for ${contract}:`, error); + db.failed_contracts[contract] = + error.message || "Transaction submission failed"; + db.pending_contracts = db.pending_contracts.filter( + (addr) => addr !== contract, + ); + } + } + + // Wait for all transactions to complete + console.log("\nWaiting for all transactions to complete..."); + await waitForAllMonitoredExtrinsics(); + console.log("All transactions completed. Starting verification..."); + + // Verify metadata creation for all contracts + for (const contract of contractsToProcess) { + // Skip contracts that failed during submission + if (db.failed_contracts[contract]) { + continue; + } + + const has_metadata = await api.query.evm.accountCodesMetadata(contract); + if (!has_metadata.isEmpty) { + db.migrated_contracts.push(contract); + db.pending_contracts = db.pending_contracts.filter( + (addr) => addr !== contract, + ); + console.log(`✅ Verified metadata for ${contract}`); + } else { + console.log(`❌ Metadata verification failed for ${contract}`); + db.failed_contracts[contract] = "Metadata verification failed"; + db.pending_contracts = db.pending_contracts.filter( + (addr) => addr !== contract, + ); + } + } + + // Save final progress + fs.writeFileSync(PROGRESS_FILE, JSON.stringify(db, null, 2)); + + // Print summary + console.log("\nMigration Summary:"); + console.log( + `✅ Successfully processed: ${db.migrated_contracts.length} contracts`, + ); + console.log( + `❌ Failed: ${Object.keys(db.failed_contracts).length} contracts`, + ); + console.log(`⏳ Remaining: ${db.pending_contracts.length} contracts`); + } catch (error) { + console.error("Migration error:", error); + throw error; + } finally { + await waitForAllMonitoredExtrinsics(); + await api.disconnect(); + } +} + +main().catch((err) => { + console.error("ERR!", err); + process.exit(1); +}); diff --git a/src/lazy-migrations/get-contracts-without-metadata.ts b/src/lazy-migrations/get-contracts-without-metadata.ts new file mode 100644 index 0000000..138e77f --- /dev/null +++ b/src/lazy-migrations/get-contracts-without-metadata.ts @@ -0,0 +1,132 @@ +import yargs from "yargs"; +import fs from "fs"; +import path from "path"; +import "@polkadot/api-augment"; +import "@moonbeam-network/api-augment"; +import { blake2AsHex, xxhashAsHex } from "@polkadot/util-crypto"; +import { getApiFor, NETWORK_YARGS_OPTIONS } from "../utils/networks"; +import { Raw } from "@polkadot/types"; + +const argv = yargs(process.argv.slice(2)) + .usage("Usage: $0") + .version("1.0.0") + .options({ + ...NETWORK_YARGS_OPTIONS, + limit: { + type: "number", + default: 1000, + describe: "The maximum number of storage entries to process per batch", + }, + request_delay: { + type: "number", + default: 500, + describe: "Delay between requests in ms", + }, + }).argv; + +async function main() { + const api = await getApiFor(argv); + + try { + const chain = (await api.rpc.system.chain()) + .toString() + .toLowerCase() + .replace(/\s/g, "-"); + + const TEMPORARY_DB_FILE = path.resolve( + __dirname, + `contracts-without-metadata-addresses-${chain}-db.json`, + ); + + let db = { + contract_processed: 0, + contracts_without_metadata: {}, + fixed_contracts: {}, + cursor: "", + }; + + if (fs.existsSync(TEMPORARY_DB_FILE)) { + db = { + ...db, + ...JSON.parse( + fs.readFileSync(TEMPORARY_DB_FILE, { encoding: "utf-8" }), + ), + }; + } + + const evmAccountCodePrefix = + xxhashAsHex("EVM", 128) + xxhashAsHex("AccountCodes", 128).slice(2); + const evmAccountCodeMetadataPrefix = + xxhashAsHex("EVM", 128) + + xxhashAsHex("AccountCodesMetadata", 128).slice(2); + + const ITEMS_PER_PAGE = argv.limit; + + while (db.cursor !== undefined) { + const keys = await api.rpc.state.getKeysPaged( + evmAccountCodePrefix, + ITEMS_PER_PAGE, + db.cursor, + ); + + db.cursor = keys.length > 0 ? keys[keys.length - 1].toHex() : undefined; + console.log(`Cursor: ${db.cursor}, Keys fetched: ${keys.length}`); + + if (keys.length === 0) { + console.log("No more keys to process."); + break; + } + + const metadataKeys = []; + const addresses = []; + + for (const key of keys) { + const SKIP_BYTES = 16 + 16 + 16; + const address = + "0x" + + key + .toHex() + .slice(2) + .slice(SKIP_BYTES * 2); + addresses.push(address); + + const address_blake2_hash = blake2AsHex(address, 128).slice(2); + const contract_metadata_key = + evmAccountCodeMetadataPrefix + address_blake2_hash + address.slice(2); + metadataKeys.push(contract_metadata_key); + } + + // Batch query the storage for metadata keys + const storageValues = (await api.rpc.state.queryStorageAt( + metadataKeys, + )) as unknown as Raw[]; + + // Process the results + storageValues.forEach((storageValue, index) => { + if (storageValue.isEmpty) { + const address = addresses[index]; + db.contracts_without_metadata[address] = true; + } + db.contract_processed++; + }); + + // Save progress periodically + if (db.contract_processed % 1000 === 0) { + fs.writeFileSync(TEMPORARY_DB_FILE, JSON.stringify(db, null, 2)); + } + + // Optional delay to prevent overloading the API + await new Promise((resolve) => setTimeout(resolve, argv.request_delay)); + } + + // Final save + fs.writeFileSync(TEMPORARY_DB_FILE, JSON.stringify(db, null, 2)); + console.log("Processing completed."); + } catch (error) { + console.error(error); + } finally { + await api.disconnect(); + } +} + +main().catch((error) => console.error("Error:", error));