Skip to content

Commit

Permalink
Add a script for create contract metadata migration (#55)
Browse files Browse the repository at this point in the history
Co-authored-by: crystalin <[email protected]>
  • Loading branch information
ahmadkaouk and crystalin authored Nov 29, 2024
1 parent bef7dd8 commit 8720c85
Show file tree
Hide file tree
Showing 2 changed files with 333 additions and 0 deletions.
201 changes: 201 additions & 0 deletions src/lazy-migrations/004-create-contract-metadata.ts
Original file line number Diff line number Diff line change
@@ -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 <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.ts";
import {
monitorSubmittedExtrinsic,
waitForAllMonitoredExtrinsics,
} from "../utils/monitoring.ts";
import { ALITH_PRIVATE_KEY } from "../utils/constants.ts";
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<string, string>;
}

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);
});
132 changes: 132 additions & 0 deletions src/lazy-migrations/get-contracts-without-metadata.ts
Original file line number Diff line number Diff line change
@@ -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.ts";
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));

0 comments on commit 8720c85

Please sign in to comment.