From 4891a922efb89e93d29d19c1d3ce73d066d12013 Mon Sep 17 00:00:00 2001 From: dtfiedler Date: Wed, 2 Oct 2024 12:09:17 -0500 Subject: [PATCH] chore(migration): final migration scripts for oct-2 ant fix --- tools/aos-bundled.lua | 12 +- tools/create-csv-from-arns-records.mjs | 36 +-- tools/deduplicate-ant-new-process-ids.mjs | 85 +++++++ tools/initialize-state-for-new-ants.mjs | 0 tools/migrate-ant-states.mjs | 262 +++++++++++++++------- tools/package.json | 3 +- tools/swap-ant-process-on-registry.mjs | 219 ++++++++++++++++++ tools/verify-sdk-integration.mjs | 236 ++++++++++++++----- tools/yarn.lock | 12 + 9 files changed, 706 insertions(+), 159 deletions(-) create mode 100644 tools/deduplicate-ant-new-process-ids.mjs delete mode 100644 tools/initialize-state-for-new-ants.mjs create mode 100644 tools/swap-ant-process-on-registry.mjs diff --git a/tools/aos-bundled.lua b/tools/aos-bundled.lua index 72c6ccec..903fea27 100644 --- a/tools/aos-bundled.lua +++ b/tools/aos-bundled.lua @@ -1148,7 +1148,7 @@ function ant.init() Logo = Logo, Denomination = tostring(Denomination), Owner = Owner, - HandlerNames = utils.getHandlerNames(Handlers), + Handlers = utils.getHandlerNames(Handlers), ["Source-Code-TX-ID"] = SourceCodeTxId, } ao.send({ @@ -1235,7 +1235,7 @@ function ant.init() end local tags = msg.Tags local name, transactionId, ttlSeconds = - tags["Sub-Domain"], tags["Transaction-Id"], tonumber(tags["TTL-Seconds"]) + string.lower(tags["Sub-Domain"]), tags["Transaction-Id"], tonumber(tags["TTL-Seconds"]) local setRecordStatus, setRecordResult = pcall(records.setRecord, name, transactionId, ttlSeconds) if not setRecordStatus then @@ -1257,7 +1257,8 @@ function ant.init() if assertHasPermission == false then return ao.send({ Target = msg.From, Action = "Invalid-Remove-Record-Notice", Data = permissionErr }) end - local removeRecordStatus, removeRecordResult = pcall(records.removeRecord, msg.Tags["Sub-Domain"]) + local name = string.lower(msg.Tags["Sub-Domain"]) + local removeRecordStatus, removeRecordResult = pcall(records.removeRecord, name) if not removeRecordStatus then ao.send({ Target = msg.From, @@ -1272,7 +1273,8 @@ function ant.init() end) Handlers.add(camel(ActionMap.Record), utils.hasMatchingTag("Action", ActionMap.Record), function(msg) - local nameStatus, nameRes = pcall(records.getRecord, msg.Tags["Sub-Domain"]) + local name = string.lower(msg.Tags["Sub-Domain"]) + local nameStatus, nameRes = pcall(records.getRecord, name) if not nameStatus then ao.send({ Target = msg.From, @@ -1287,7 +1289,7 @@ function ant.init() local recordNotice = { Target = msg.From, Action = "Record-Notice", - Name = msg.Tags["Sub-Domain"], + Name = name, Data = nameRes, } diff --git a/tools/create-csv-from-arns-records.mjs b/tools/create-csv-from-arns-records.mjs index 88136298..0a0794db 100644 --- a/tools/create-csv-from-arns-records.mjs +++ b/tools/create-csv-from-arns-records.mjs @@ -1,38 +1,48 @@ -import { AOProcess, IO, IO_DEVNET_PROCESS_ID, IO_TESTNET_PROCESS_ID } from '@ar.io/sdk'; -import { connect } from '@permaweb/aoconnect'; -import path from 'path'; -import fs from 'fs'; +import { + AOProcess, + IO, + IO_DEVNET_PROCESS_ID, + IO_TESTNET_PROCESS_ID, +} from "@ar.io/sdk"; +import { connect } from "@permaweb/aoconnect"; +import path from "path"; +import fs from "fs"; const __dirname = path.dirname(new URL(import.meta.url).pathname); -const restart = process.argv.includes('--restart'); -const testnet = process.argv.includes('--testnet'); +const restart = process.argv.includes("--restart"); +const testnet = process.argv.includes("--testnet"); async function main() { const io = IO.init({ process: new AOProcess({ processId: testnet ? IO_TESTNET_PROCESS_ID : IO_DEVNET_PROCESS_ID, ao: connect({ - CU_URL: 'https://cu.ar-io.dev', + CU_URL: "https://cu.ar-io.dev", }), }), }); - const outputFilePath = path.join(__dirname, `arns-processid-mapping-${testnet ? 'testnet' : 'devnet'}.csv`); + const outputFilePath = path.join( + __dirname, + `arns-processid-mapping-${testnet ? "testnet" : "devnet"}.csv`, + ); const arnsRecords = await io.getArNSRecords({ limit: 100000, - sortBy: 'startTimestamp', - sortOrder: 'asc', + sortBy: "startTimestamp", + sortOrder: "asc", }); // recreate the file if restart is true if (!fs.existsSync(outputFilePath) || restart) { - fs.writeFileSync(outputFilePath, 'domain,oldProcessId\n', { flag: 'w' }); + fs.writeFileSync(outputFilePath, "domain,oldProcessId\n", { flag: "w" }); } - console.log(`Found ${arnsRecords.items.length} ARNS records for process, mapping to CSV for processing`); + console.log( + `Found ${arnsRecords.items.length} ARNS records for process, mapping to CSV for processing`, + ); arnsRecords.items.forEach((record) => { fs.writeFileSync(outputFilePath, `${record.name},${record.processId}\n`, { - flag: 'a', + flag: "a", }); }); console.log(`Wrote ${arnsRecords.items.length} ARNS records to CSV`); diff --git a/tools/deduplicate-ant-new-process-ids.mjs b/tools/deduplicate-ant-new-process-ids.mjs new file mode 100644 index 00000000..65888a02 --- /dev/null +++ b/tools/deduplicate-ant-new-process-ids.mjs @@ -0,0 +1,85 @@ +import Arweave from "arweave"; +import path from "path"; +import fs from "fs"; + +const __dirname = path.dirname(new URL(import.meta.url).pathname); +const restart = process.argv.includes("--restart"); +const inputFilePath = process.argv.includes("--file") + ? process.argv[process.argv.indexOf("--file") + 1] + : null; +const wallet = JSON.parse( + fs.readFileSync(path.join(__dirname, "key.json"), "utf8"), +); +const testnet = process.argv.includes("--testnet"); +const arweave = Arweave.init({ + host: "arweave.net", + port: 443, + protocol: "https", +}); +const index = process.argv.includes("--index") + ? process.argv[process.argv.indexOf("--index") + 1] + : null; + +async function main() { + const csv = fs.readFileSync(path.join(__dirname, inputFilePath), "utf8"); + + const postfix = index ? `-${index}` : ""; + + const outputFilePath = path.join( + __dirname, + `deduplicated-processids-${testnet ? "testnet" : "devnet"}${postfix}.csv`, + ); + + // print out address of wallet being used + const address = await arweave.wallets.jwkToAddress(wallet); + console.log(`Using wallet ${address} to evaluate ants`); + + const alreadyCreatedProcessIds = csv + .split("\n") + .slice(1) // skip header + .map((line) => line.split(",")) + .filter( + ([domain, oldProcessId, newProcessId]) => + domain && oldProcessId && newProcessId && oldProcessId !== newProcessId, + ); + + // create output csv if not exists including eval result + if (!fs.existsSync(outputFilePath) || restart) { + fs.writeFileSync(outputFilePath, "domain,oldProcessId,newProcessId\n", { + flag: "w", + }); + } + + const processMap = new Map(); + + // process map - don't re-evaluate ants that have already been evaluated + + for (const [domain, oldProcessId, newProcessId] of alreadyCreatedProcessIds) { + console.log(`Evaluating ant ${newProcessId}`); + + // don't eval if we already have on the process map + if (processMap.has(oldProcessId)) { + console.log(`Skipping ${oldProcessId} as it has already been created`); + fs.writeFileSync( + outputFilePath, + `${domain},${oldProcessId},${processMap.get(oldProcessId)}\n`, + { + flag: "a", + }, + ); + continue; + } + + fs.writeFileSync( + outputFilePath, + `${domain},${oldProcessId},${newProcessId}\n`, + { + flag: "a", + }, + ); + + processMap.set(oldProcessId, newProcessId); + } +} + +main(); diff --git a/tools/initialize-state-for-new-ants.mjs b/tools/initialize-state-for-new-ants.mjs deleted file mode 100644 index e69de29b..00000000 diff --git a/tools/migrate-ant-states.mjs b/tools/migrate-ant-states.mjs index dc78ef3a..45338c4d 100644 --- a/tools/migrate-ant-states.mjs +++ b/tools/migrate-ant-states.mjs @@ -3,10 +3,11 @@ import Arweave from "arweave"; import { connect } from "@permaweb/aoconnect"; import path from "path"; import fs from "fs"; - +import pLimit from "p-limit"; const __dirname = path.dirname(new URL(import.meta.url).pathname); const restart = process.argv.includes("--restart"); const dryRun = process.argv.includes("--dry-run"); +const testnet = process.argv.includes("--testnet"); const inputFilePath = process.argv.includes("--file") ? process.argv[process.argv.indexOf("--file") + 1] : null; @@ -14,12 +15,7 @@ const wallet = JSON.parse( fs.readFileSync(path.join(__dirname, "key.json"), "utf8"), ); const signer = new ArweaveSigner(wallet); -const testnet = process.argv.includes("--testnet"); -const arweave = Arweave.init({ - host: "arweave.net", - port: 443, - protocol: "https", -}); +const arweave = Arweave.init({}); const { message, result } = connect(); async function main() { @@ -75,103 +71,197 @@ async function main() { sourceCodeUpdated, sdkVerified, stateMigrated, - initializeStateMessageId + initializeStateMessageId, ] of existingRecords) { processMap.set(newProcessId, initializeStateMessageId); } - console.log(`Skipping ${Object.keys(processMap).length} ants that have already been migrated.`); + console.log( + `Skipping ${Object.keys(processMap).length} ants that have already been migrated.`, + ); } // filter out messages in the process map and remove duplicates based on newProcessId - const processIdsToMigrate = antsToMigrateWithProcessIds.reduce((acc, [domain, oldProcessId, newProcessId, sourceCodeUpdated, sdkVerified, stateMigrated]) => { - if (!processMap.has(newProcessId) && !acc.some(item => item[2] === newProcessId)) { - acc.push([domain, oldProcessId, newProcessId, sourceCodeUpdated, sdkVerified, stateMigrated]); - } - return acc; - }, []); + const processIdsToMigrate = antsToMigrateWithProcessIds.reduce( + ( + acc, + [ + domain, + oldProcessId, + newProcessId, + sourceCodeUpdated, + sdkVerified, + stateMigrated, + ], + ) => { + if ( + !processMap.has(newProcessId) && + !acc.some((item) => item[2] === newProcessId) + ) { + acc.push([ + domain, + oldProcessId, + newProcessId, + sourceCodeUpdated, + sdkVerified, + stateMigrated, + ]); + } + return acc; + }, + [], + ); console.log(`Migrating ${processIdsToMigrate.length} unique ants`); // process map - don't re-evaluate ants that have already been evaluated + const limit = pLimit(10); - await Promise.all(processIdsToMigrate.map(async ([domain, oldProcessId, newProcessId, sourceCodeUpdated, sdkVerified,stateMigrated]) => { - console.log(`Migrating state for ant ${oldProcessId} to ${newProcessId}`); - - // don't eval if we already have on the process map - if (processMap.has(newProcessId)) { - console.log(`Skipping ${newProcessId} as it has already been migrated`); - fs.promises.writeFile( - outputFilePath, - `${domain},${oldProcessId},${newProcessId},${sourceCodeUpdated},${sdkVerified},${stateMigrated},${processMap.get(newProcessId)}\n`, - { - flag: "a", - }, - ); - return; - } + await Promise.all( + antsToMigrateWithProcessIds.map( + async ([ + domain, + oldProcessId, + newProcessId, + sourceCodeUpdated, + sdkVerified, + stateMigrated, + ]) => { + return limit(async () => { + if (domain === undefined) { + console.error(`Skipping ${newProcessId} as it has no domain`); + return; + } + // don't eval if we already have on the process map + if (processMap.has(newProcessId)) { + console.log( + `Skipping ${newProcessId} as it has already been migrated`, + ); + fs.promises.writeFile( + outputFilePath, + `${domain},${oldProcessId},${newProcessId},${sourceCodeUpdated},${sdkVerified},${stateMigrated},${processMap.get(newProcessId)}\n`, + { + flag: "a", + }, + ); + return; + } - // get the current AO state of the old process - const oldProcessANT = ANT.init({ - processId: oldProcessId - }); - - const oldProcessState = await oldProcessANT.getState(); - - // required by Initialize-State in this format. Everything else will be defaulted - const migratedState = { - owner: oldProcessState.Owner, - controllers: oldProcessState.Controllers, - name: oldProcessState.Name, - ticker: oldProcessState.Ticker, - records: oldProcessState.Records, - balances: oldProcessState.Balances, - } + // get the current AO state of the old process + const oldProcessANT = ANT.init({ + processId: oldProcessId, + }); - if (dryRun) { - console.log(`Dry run, skipping actual evaluation of ant ${newProcessId}. Migrated state: ${JSON.stringify(migratedState)}`); - processMap.set(newProcessId, 'fake-message-id-of-init-state'); - fs.promises.writeFile( - outputFilePath, - `${domain},${oldProcessId},${newProcessId},${sourceCodeUpdated},${sdkVerified},${sourceCodeUpdated},${processMap.get(newProcessId)}\n`, - { - flag: "a", - }, - ); - return; - } + const oldProcessState = await Promise.race([ + oldProcessANT.getState(), + new Promise((_, reject) => + setTimeout( + () => + reject( + new Error("Timeout getting state for " + oldProcessId), + ), + 15_000, + ), + ), + ]) + .catch((error) => { + console.error( + `Error getting state for ${oldProcessId}: ${error}`, + ); + return null; + }) + .then((state) => { + return state; + }); - const migrateStateMessageId = await message({ - signer: createAoSigner(signer), - tags: [ - { - name: "Action", - value: "Initialize-State", - }, - { - name: "Old-Process-Id", - value: oldProcessId, - }, - ], - data: JSON.stringify(migratedState), - }); + if (!oldProcessState) { + console.error( + `Failed to get state for ${oldProcessId}. Was not available on original ant, marking as migrated`, + ); + processMap.set(newProcessId, "not-migrated"); + fs.promises.writeFile( + outputFilePath, + `${domain},${oldProcessId},${newProcessId},${sourceCodeUpdated},${sdkVerified},${processMap.get(newProcessId)}\n`, + { + flag: "a", + }, + ); + return; + } - // crank the MU to ensure eval is processed - await result({ - message: evalMessageId, - process: newProcessId, - }); + // required by Initialize-State in this format. Everything else will be defaulted + const migratedState = { + owner: oldProcessState.Owner, + controllers: oldProcessState.Controllers, + name: oldProcessState.Name, + ticker: oldProcessState.Ticker, + records: oldProcessState.Records, + balances: oldProcessState.Balances, + }; - fs.promises.writeFile( - outputFilePath, - `${domain},${oldProcessId},${newProcessId},${sourceCodeUpdated},${sdkVerified},${sdkVerified},${migrateStateMessageId}\n`, - { - flag: "a", - }, - ); + if (dryRun) { + console.log( + `Dry run, skipping actual evaluation of ant ${newProcessId}. Migrated state: ${JSON.stringify(migratedState)}`, + ); + processMap.set(newProcessId, "fake-message-id-of-init-state"); + fs.promises.writeFile( + outputFilePath, + `${domain},${oldProcessId},${newProcessId},${sourceCodeUpdated},${sdkVerified},${processMap.get(newProcessId)}\n`, + { + flag: "a", + }, + ); + return; + } + + const migrateStateMessageId = await message({ + signer: createAoSigner(signer), + process: newProcessId, + tags: [ + { + name: "Action", + value: "Initialize-State", + }, + { + name: "Old-Process-Id", + value: oldProcessId, + }, + ], + data: JSON.stringify(migratedState), + }).catch((error) => { + console.error( + `Error sending message for ${newProcessId}: ${error}`, + ); + return null; + }); - processMap.set(newProcessId, migrateStateMessageId); - }), + if (!migrateStateMessageId) { + console.error(`Failed to send message for ${newProcessId}`); + return; + } + + // crank the MU to ensure eval is processed + await result({ + message: migrateStateMessageId, + process: newProcessId, + }).catch((error) => { + console.error(`Error getting result for ${newProcessId}: ${error}`); + return; + }); + + fs.promises.writeFile( + outputFilePath, + `${domain},${oldProcessId},${newProcessId},${sourceCodeUpdated},${sdkVerified},${migrateStateMessageId}\n`, + { + flag: "a", + }, + ); + + processMap.set(newProcessId, migrateStateMessageId); + return; + }); + }, + ), ); } diff --git a/tools/package.json b/tools/package.json index a90683b3..b168eff2 100644 --- a/tools/package.json +++ b/tools/package.json @@ -6,6 +6,7 @@ "dependencies": { "@ar.io/sdk": "^2.2.5", "@permaweb/aoconnect": "^0.0.59", - "arweave": "^1.15.5" + "arweave": "^1.15.5", + "p-limit": "^6.1.0" } } diff --git a/tools/swap-ant-process-on-registry.mjs b/tools/swap-ant-process-on-registry.mjs new file mode 100644 index 00000000..5ecf71ea --- /dev/null +++ b/tools/swap-ant-process-on-registry.mjs @@ -0,0 +1,219 @@ +import { + ANT, + IO_TESTNET_PROCESS_ID, + IO, + AOProcess, + createAoSigner, + ArweaveSigner, +} from "@ar.io/sdk"; +import { connect } from "@permaweb/aoconnect"; +import path from "path"; +import fs from "fs"; +import { strict as assert } from "node:assert"; +import pLimit from "p-limit"; +import Arweave from "arweave"; + +const __dirname = path.dirname(new URL(import.meta.url).pathname); +const inputFilePath = process.argv.includes("--file") + ? process.argv[process.argv.indexOf("--file") + 1] + : null; +const dryRun = process.argv.includes("--dry-run") ? true : false; +const arweave = Arweave.init({ + host: "arweave.net", + port: 443, + protocol: "https", +}); +const wallet = JSON.parse( + fs.readFileSync(path.join(__dirname, "key.json"), "utf8"), +); +const signer = new ArweaveSigner(wallet); +const ao = connect({ + CU_URL: "https://cu.ar-io.dev", +}); +async function main() { + const csv = fs.readFileSync(path.join(__dirname, inputFilePath), "utf8"); + + // print out address of wallet being used + const address = await arweave.wallets.jwkToAddress(wallet); + console.log(`Using wallet ${address} to migrate process ids on IO registry`); + + const migratedProcessIds = csv + .split("\n") + .slice(1) // skip header + .map((line) => line.split(",")) + .filter( + ([ + domain, + oldProcessId, + newProcessId, + sourceCodeUpdated, + sdkVerified, + initializeStateMessageId, + stateMatchVerified, + ]) => + domain && + oldProcessId && + newProcessId && + sourceCodeUpdated && + sdkVerified && + initializeStateMessageId && + stateMatchVerified, + ); + + const adminProcess = new AOProcess({ + signer: createAoSigner(wallet), + processId: IO_TESTNET_PROCESS_ID, + ao: ao, + }); + const io = IO.init({ + process: adminProcess, + }); + + const limit = pLimit(10); + + // output file + const outputFilePath = path.join(__dirname, "final-migration-results.csv"); + fs.writeFileSync(outputFilePath, "domain,oldProcessId,newProcessId,sourceCodeUpdated,sdkVerified,initializeStateMessageId,stateMatchVerified,migratedOnRegistry,updateRecordMessageId\n"); + + await Promise.all( + migratedProcessIds.map( + async ([ + domain, + oldProcessId, + newProcessId, + sourceCodeUpdated, + sdkVerified, + initializeStateMessageId, + stateMatchVerified, + ]) => { + return limit(async () => { + // check that io has the arns record we are about to update + const arnsRecord = await io.getArNSRecord({ name: domain }); + if (!arnsRecord) { + console.error(`ARNs record not found for ${domain}`); + return; + } + + // get the process id currently used by the arns record, if it does not match new process id, it will be migrated + const currentProcessId = arnsRecord.processId; + if (currentProcessId === newProcessId) { + // get AddRecord data item with name from graphql + const messageId = await arweave.api.post('/graphql', { + query: ` + { + transactions(tags: [{ name: "Action", values: ["AddRecord"] }, { name: "Name", values: ["${domain}"]}] sort:HEIGHT_DESC, first:1) { + edges { + node { + id + } + } + } + } + `, + }).then(({ data }) => data?.data?.transactions?.edges[0]?.node?.id); + console.log( + `Process id ${currentProcessId} already matches new process id ${newProcessId} for ${domain}. Skipping.`, + ); + fs.appendFileSync(outputFilePath, `${domain},${oldProcessId},${newProcessId},${sourceCodeUpdated},${sdkVerified},${initializeStateMessageId},${stateMatchVerified},true,${messageId}\n`); + // wait 3 seconds for the record to be updated + await new Promise((resolve) => setTimeout(resolve, 3000)); + return; + } + + console.log( + `Process id ${currentProcessId} does not match new process id ${newProcessId} for ${domain}. Eligible for migration.`, + ); + + const updatedRecordData = { + type: arnsRecord.type, + startTimestamp: arnsRecord.startTimestamp, + processId: newProcessId, + undernameLimit: arnsRecord.undernameLimit, + purchasePrice: arnsRecord.purchasePrice, + ...(arnsRecord.type === "lease" + ? { endTimestamp: arnsRecord.endTimestamp } + : {}), + }; + + const { + processId: _oldProcessId, + ...previousRecordWithoutProcessId + } = arnsRecord; + const { processId: _newProcessId, ...newRecordWithoutProcessId } = + updatedRecordData; + + // assert the only thing that is different is the process id + assert.deepEqual( + previousRecordWithoutProcessId, + newRecordWithoutProcessId, + "Updated record data should only differ in process id", + ); + + assert.equal( + oldProcessId, + _oldProcessId, + "Old process id should be the same as the one in the record", + ); + + assert.equal( + newProcessId, + _newProcessId, + "New process id should be the same as the one in the record", + ); + + // assert if it is a permabuy endTimestamp is empty and if a least timestamp is not empty + if (arnsRecord.type === "permabuy") { + assert.equal( + arnsRecord.endTimestamp, + undefined, + "End timestamp should be empty for permabuy", + ); + } + + if (arnsRecord.type === "lease") { + assert.notEqual( + arnsRecord.startTimestamp, + undefined, + "Start timestamp should not be empty for lease", + ); + } + + console.log(`Updating record for ${domain} with process id ${newProcessId}`); + + if (dryRun) { + console.log( + `Dry run: would update record ${JSON.stringify(updatedRecordData)} for ${domain}`, + ); + fs.appendFileSync(outputFilePath, `${domain},${oldProcessId},${newProcessId},${sourceCodeUpdated},${sdkVerified},${initializeStateMessageId},${stateMatchVerified},false,null\n`); + return; + } + + + const updateRecordMessage = await adminProcess.send({ + signer: createAoSigner(signer), + tags: [ + { name: "Action", value: "AddRecord" }, + { name: 'Name', value: domain}, + ], + data: JSON.stringify(updatedRecordData), + }) + + console.log(`Update record message id: ${updateRecordMessage.id}`); + // wait 3 seconds for the record to be updated + await new Promise((resolve) => setTimeout(resolve, 3000)); + // assert that fetching the record after the update returns the updated record + const updatedRecord = await io.getArNSRecord({ name: domain }); + assert.deepEqual( + updatedRecord, + updatedRecordData, + "Updated record should be the same as the one in the registry", + ); + + fs.appendFileSync(outputFilePath, `${domain},${oldProcessId},${newProcessId},${sourceCodeUpdated},${sdkVerified},${initializeStateMessageId},${stateMatchVerified},true,${updateRecordMessage.id}\n`); + }); + }, + ), + ); +} + +main(); diff --git a/tools/verify-sdk-integration.mjs b/tools/verify-sdk-integration.mjs index 09f1c7b5..90e72ee7 100644 --- a/tools/verify-sdk-integration.mjs +++ b/tools/verify-sdk-integration.mjs @@ -4,12 +4,15 @@ import fs from "fs"; import path from "path"; import { strict as assert } from "node:assert"; import Arweave from "arweave"; - +import pLimit from "p-limit"; const __dirname = path.dirname(new URL(import.meta.url).pathname); const inputFilePath = process.argv.includes("--file") ? process.argv[process.argv.indexOf("--file") + 1] : null; const testnet = process.argv.includes("--testnet") ? true : false; +const verifyStateMigration = process.argv.includes("--verify-state-migration") + ? true + : false; const aoClient = connect(); const wallet = JSON.parse( fs.readFileSync(path.join(__dirname, "key.json"), "utf8"), @@ -25,7 +28,7 @@ async function main() { const outputFilePath = path.join( __dirname, - `e2e-eval-verified-ants-${testnet ? "testnet" : "devnet"}.csv`, + `verified-ant-state-and-sdk-integration-${testnet ? "testnet" : "devnet"}.csv`, ); // print out address of wallet being used @@ -37,65 +40,190 @@ async function main() { .slice(1) // skip header .map((line) => line.split(",")) .filter( - ([domain, oldProcessId, newProcessId, evaluated]) => - domain && - oldProcessId && - newProcessId && - oldProcessId !== newProcessId + ([domain, oldProcessId, newProcessId, sourceCodeUpdated]) => + domain && oldProcessId && newProcessId && oldProcessId !== newProcessId, ); fs.writeFileSync( outputFilePath, - "domain,oldProcessId,newProcessId,evaluated,sdkIntegrationVerified\n", + "domain,oldProcessId,newProcessId,sourceCodeUpdated,sdkVerified,initializeStateMessageId,stateMatchVerified\n", { flag: "w" }, ); - await Promise.all(inputProcessIds.map(async ([domain, oldProcessId, newProcessId]) => { - console.log( - `Verifying sdk integration for ${domain} with new process id ${newProcessId}`, - ); - const ant = ANT.init({ - process: new AOProcess({ - processId: newProcessId, - ao: aoClient, - }), - }); - const info = await ant.getInfo(); - assert(info.Name); - assert(info.Ticker); - assert(info["Total-Supply"]); - assert(info.Denomination !== undefined); - assert(info.Logo); - assert(info.Owner === address); - assert(info.Handlers); - assert.deepStrictEqual(info.Handlers, [ - "evolve", - "_eval", - "_default", - "transfer", - "balance", - "balances", - "totalSupply", - "info", - "addController", - "removeController", - "controllers", - "setRecord", - "removeRecord", - "record", - "records", - "setName", - "setTicker", - "initializeState", - "state", - ]); - - // write the existing columns and code verified column - await fs.promises.appendFile( - outputFilePath, - `${domain},${oldProcessId},${newProcessId},true,true\n`, - ); - }), + const limit = pLimit(50); + + await Promise.all( + inputProcessIds.map( + async ([ + domain, + oldProcessId, + newProcessId, + sourceCodeUpdated, + sdkVerified, + initializeStateMessageId, + ]) => { + return limit(async () => { + console.log( + `Verifying state and SDK integrations for ${newProcessId}`, + ); + const ant = ANT.init({ + process: new AOProcess({ + processId: newProcessId, + ao: aoClient, + }), + }); + const info = await ant.getInfo().catch((error) => { + console.error(`Error getting info for ${newProcessId}: ${error}`); + return null; + }); + assert( + info, + `Info not found for ${domain} with process id ${newProcessId}`, + ); + assert(info.Name !== undefined, `Name not found for ${newProcessId}`); + assert( + info.Ticker !== undefined, + `Ticker not found for ${newProcessId}`, + ); + assert( + info["Total-Supply"] !== 1, + `Total supply not found for ${newProcessId}`, + ); + assert( + info.Denomination !== 0, + `Denomination not found for ${newProcessId}`, + ); + assert(info.Logo !== undefined, `Logo not found for ${newProcessId}`); + assert( + info.Owner !== undefined, + `Owner not found for ${newProcessId}`, + ); + assert( + info.Handlers !== undefined, + `Handlers not found for ${newProcessId}`, + ); + assert.deepStrictEqual(info.Handlers, [ + "evolve", + "_eval", + "_default", + "transfer", + "balance", + "balances", + "totalSupply", + "info", + "addController", + "removeController", + "controllers", + "setRecord", + "removeRecord", + "record", + "records", + "setName", + "setTicker", + "initializeState", + "state", + ]); + + if (verifyStateMigration) { + // verify old state and new state are the same for every name + const oldProcessANT = ANT.init({ + processId: oldProcessId, + }); + + // timeout after 15 seconds + const oldProcessState = await Promise.race([ + oldProcessANT.getState(), + new Promise((_, reject) => + setTimeout( + () => reject(new Error("Timeout getting state")), + 15_000, + ), + ), + ]) + .catch((error) => { + console.error( + `Error getting state for ${oldProcessId}: ${error}`, + ); + return null; + }) + .then((state) => { + return state; + }); + + const newProcessANT = ANT.init({ + processId: newProcessId, + }); + + const newProcessState = await newProcessANT + .getState() + .catch((error) => { + console.error( + `Error getting state for ${newProcessId}: ${error}`, + ); + return null; + }); + + assert(newProcessState, `State not found for ${newProcessId}`); + + // confirm we have the updated source code + assert.deepStrictEqual( + newProcessState["Source-Code-TX-ID"], + "pOh2yupSaQCrLI_-ah8tVTiusUdVNTxxeWTQQHNdf30", + `Source code not updated for ${newProcessId}`, + ); + + if (!oldProcessState) { + assert.deepStrictEqual( + oldProcessState.Records, + newProcessState.Records, + `Records not migrated for ${newProcessId}`, + ); + assert.deepStrictEqual( + oldProcessState.Balances, + newProcessState.Balances, + `Balances not migrated for ${newProcessId}`, + ); + assert.deepStrictEqual( + oldProcessState.Name, + newProcessState.Name, + `Name not migrated for ${newProcessId}`, + ); + assert.deepStrictEqual( + oldProcessState.Ticker, + newProcessState.Ticker, + ); + assert.deepStrictEqual( + oldProcessState.Denomination, + newProcessState.Denomination, + `Denomination not migrated for ${newProcessId}`, + ); + assert.deepStrictEqual( + oldProcessState.Logo, + newProcessState.Logo, + `Logo not migrated for ${newProcessId}`, + ); + assert.deepStrictEqual( + oldProcessState.Owner, + newProcessState.Owner, + `Owner not migrated for ${newProcessId}`, + ); + assert.deepStrictEqual( + oldProcessState.Controllers, + newProcessState.Controllers, + `Controllers not migrated for ${newProcessId}`, + ); + } + } + + // write the existing columns and code verified column + await fs.promises.appendFile( + outputFilePath, + // domain,oldProcessId,newProcessId,sourceCodeUpdated,sdkVerified,stateMigrated,initializeStateMessageId + `${domain},${oldProcessId},${newProcessId},${sourceCodeUpdated},${sdkVerified},${initializeStateMessageId},true\n`, + ); + }); + }, + ), ); } diff --git a/tools/yarn.lock b/tools/yarn.lock index 766ca2cc..e5f08061 100644 --- a/tools/yarn.lock +++ b/tools/yarn.lock @@ -899,6 +899,13 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" +p-limit@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-6.1.0.tgz#d91f9364d3fdff89b0a45c70d04ad4e0df30a0e8" + integrity sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg== + dependencies: + yocto-queue "^1.1.1" + plimit-lit@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/plimit-lit/-/plimit-lit-3.0.1.tgz#45a2aee1a7249aa9c2eafc67b6a27bc927e3aa39" @@ -1110,6 +1117,11 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +yocto-queue@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" + integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== + zod@^3.23.5, zod@^3.23.8: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"