diff --git a/.github/workflows/monitor.yaml b/.github/workflows/monitor.yaml new file mode 100644 index 0000000..8c37474 --- /dev/null +++ b/.github/workflows/monitor.yaml @@ -0,0 +1,115 @@ +# run on a cron every hour and call yarn monitor + +name: Montior IO Process + +on: + push: + workflow_dispatch: + schedule: + - cron: '0 * * * *' # Run every hour + +jobs: + monitor: + permissions: + contents: read + actions: read + strategy: + matrix: + network: [testnet, devnet] + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'yarn' + + - name: Setup + run: yarn + + - name: Monitor + run: yarn monitor | tee results.txt + continue-on-error: true + id: monitor + env: + IO_PROCESS_ID: ${{ matrix.network == 'testnet' && 'agYcCFJtrMG6cqMuZfskIkFTGvUPddICmtQSBIoPdiA' || 'GaQrvEMKBpkjofgnBi_B3IgIDmY_XYelVLB6GcRGrHc' }} + + - name: Create test output + id: test-outputs + run: | + TEST_RESULTS=$(cat results.txt) + echo "TEST_RESULTS<> $GITHUB_ENV + echo "$TEST_RESULTS" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + - name: Notify Failure + if: failure() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + text: 'IO Process Observation Failed!' + custom_payload: | + { + text: "IO Process Observation Failed", + attachments: [{ + fallback: 'IO Process Observation Failed', + color: 'danger', + title: 'Test Results', + text: 'The IO Process Observation test has failed.', + fields: [{ + title: 'Network', + value: '${{ matrix.network }}', + short: true + }, + { + title: 'Process ID', + value: '${{ matrix.network == 'testnet' && 'agYcCFJtrMG6cqMuZfskIkFTGvUPddICmtQSBIoPdiA' || 'GaQrvEMKBpkjofgnBi_B3IgIDmY_XYelVLB6GcRGrHc' }}', + short: true + }, + { + title: 'Error Details', + value: '${{ steps.monitor.outputs.stderr }}', + short: false + }], + }] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} + + # on sucess send a slack message + - name: Notify Success + if: success() + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_COLOR: ${{ job.status }} + SLACK_TITLE: IO Process Observation Success! + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + SLACK_CUSTOM_PAYLOAD: | + { + "text": "IO Process Observation Success!", + "attachments": [{ + "fallback": "IO Process Observation Success!", + "color": "good", + "title": "Test Results", + "text": "The IO Process Observation test has succeeded.", + "fields": [{ + "title": "Network", + "value": "${{ matrix.network }}", + "short": true + }, + { + "title": "Process ID", + "value": "${{ matrix.network == 'testnet' && 'agYcCFJtrMG6cqMuZfskIkFTGvUPddICmtQSBIoPdiA' || 'GaQrvEMKBpkjofgnBi_B3IgIDmY_XYelVLB6GcRGrHc' }}", + "short": true + }, + { + "title": "Test Output", + "value": "```\n${{ env.TEST_RESULTS }}\n```", + "short": false + } + ] + }] + } diff --git a/package.json b/package.json index ed7107c..5f533a4 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,15 @@ { + "type": "module", "scripts": { - "build": "node tools/bundle-aos.js", - "test:integration": "yarn build && node --test --experimental-wasm-memory64 **/*.test.js", + "build": "node tools/bundle-aos.mjs", + "test:integration": "yarn build && node --test --experimental-wasm-memory64 **/*.test.mjs", "test:unit": "busted . && luacov", "test": "yarn test:unit && yarn test:integration", + "monitor": "node tests/monitor/monitor.test.mjs", "evolve": "yarn build && node tools/evolve.mjs" }, "devDependencies": { - "@ar.io/sdk": "^2.1.0-alpha.6", + "@ar.io/sdk": "alpha", "@permaweb/ao-loader": "^0.0.35", "@permaweb/aoconnect": "^0.0.55", "arweave": "^1.15.1", diff --git a/src/constants.lua b/src/constants.lua index 50e436a..2e64d2e 100644 --- a/src/constants.lua +++ b/src/constants.lua @@ -19,7 +19,7 @@ constants.MAX_ALLOWED_UNDERNAMES = 10000 constants.UNDERNAME_LEASE_FEE_PERCENTAGE = 0.001 constants.UNDERNAME_PERMABUY_FEE_PERCENTAGE = 0.005 constants.oneYearMs = 31536000 * 1000 -constants.gracePeriodMs = 3 * 14 * 24 * 60 * 60 * 1000 +constants.gracePeriodMs = 14 * 24 * 60 * 60 * 1000 -- 2 weeks constants.maxLeaseLengthYears = 5 -- DEMAND diff --git a/test/arns.test.js b/tests/arns.test.mjs similarity index 98% rename from test/arns.test.js rename to tests/arns.test.mjs index 5a5f51d..41ef405 100644 --- a/test/arns.test.js +++ b/tests/arns.test.mjs @@ -1,11 +1,11 @@ -const { createAosLoader } = require('./utils'); -const { describe, it } = require('node:test'); -const assert = require('node:assert'); -const { +import { createAosLoader } from './utils.mjs'; +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { AO_LOADER_HANDLER_ENV, DEFAULT_HANDLE_OPTIONS, STUB_ADDRESS, -} = require('../tools/constants'); +} from '../tools/constants.mjs'; // EIP55-formatted test address const testEthAddress = '0xaAaAaAaaAaAaAaaAaAAAAAAAAaaaAaAaAaaAaaAa'; diff --git a/test/gar.test.js b/tests/gar.test.mjs similarity index 98% rename from test/gar.test.js rename to tests/gar.test.mjs index ccaf780..0239a06 100644 --- a/test/gar.test.js +++ b/tests/gar.test.mjs @@ -1,11 +1,11 @@ -const { createAosLoader } = require('./utils'); -const { describe, it } = require('node:test'); -const assert = require('node:assert'); -const { +import { createAosLoader } from './utils.mjs'; +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { AO_LOADER_HANDLER_ENV, DEFAULT_HANDLE_OPTIONS, STUB_ADDRESS, -} = require('../tools/constants'); +} from '../tools/constants.mjs'; describe('GatewayRegistry', async () => { const { handle: originalHandle, memory: startMemory } = diff --git a/tests/monitor/monitor.test.mjs b/tests/monitor/monitor.test.mjs new file mode 100644 index 0000000..0e60736 --- /dev/null +++ b/tests/monitor/monitor.test.mjs @@ -0,0 +1,113 @@ +import { IO_DEVNET_PROCESS_ID, IO, IO_TESTNET_PROCESS_ID } from '@ar.io/sdk'; +import { strict as assert } from 'node:assert'; +import { describe, it } from 'node:test'; + +const io = IO.init({ + processId: process.env.IO_PROCESS_ID || IO_TESTNET_PROCESS_ID, +}); + +describe('distribution totals', () => { + it('should always have correct eligible rewards for the current epoch (within 10 mIO)', async () => { + const { distributions: currentEpochDistributions } = + await io.getCurrentEpoch(); + + // TODO: for now pass if distributions are empty + if (Object.keys(currentEpochDistributions).length === 0) { + return; + } + + // add up all the eligible operators and delegates + const assignedRewards = Object.values( + currentEpochDistributions.rewards.eligible, + ).reduce((acc, curr) => { + const delegateRewards = Object.values(curr.delegateRewards).reduce( + (d, c) => d + c, + 0, + ); + return acc + curr.operatorReward + delegateRewards; + }, 0); + + // handle any rounding errors + const roundingError = + assignedRewards - currentEpochDistributions.totalEligibleRewards; + // assert total eligible rewards rounding is less than 10 + assert( + roundingError < 10, + `Rounding for eligible distributions is too large: ${roundingError}`, + ); + }); +}); + +describe('token supply', () => { + it('should always be 1 billion IO', async () => { + const totalSupply = await io.getTokenSupply(); + assert( + totalSupply === 1000000000 * 1000000, + `Total supply is not 1 billion IO: ${totalSupply}`, + ); + }); +}); + +// TODO: arns - ensure no invalid arns names + +describe('gateway registry', () => { + it('should only have valid gateways', async () => { + let cursor = ''; + do { + const { items: gateways, nextCursor } = await io.getGateways({ + cursor, + }); + for (const gateway of gateways) { + assert(gateway.operatorStake >= 50_000_000_000); + } + cursor = nextCursor; + } while (cursor !== undefined); + }); +}); + +// Gateway registry - ensure no invalid gateways +describe('arns names', () => { + const twoWeeks = 2 * 7 * 24 * 60 * 60 * 1000; + it('should not have any arns records older than two weeks', async () => { + let cursor = ''; + do { + const { items: arns, nextCursor } = await io.getArNSRecords({ + cursor, + }); + for (const arn of arns) { + assert(arn.processId, `ARNs name '${arn.name}' has no processId`); + assert(arn.type, `ARNs name '${arn.name}' has no type`); + assert( + arn.startTimestamp, + `ARNs name '${arn.name}' has no start timestamp`, + ); + assert( + arn.purchasePrice >= 0, + `ARNs name '${arn.name}' has no purchase price`, + ); + assert( + arn.undernameLimit >= 10, + `ARNs name '${arn.name}' has no undername limit`, + ); + if (arns.type === 'lease') { + assert( + arn.endTimestamp, + `ARNs name '${arn.name}' has no end timestamp`, + ); + assert( + arn.endTimestamp > Date.now() - twoWeeks, + `ARNs name '${arn.name}' is older than two weeks`, + ); + } + // if permabuy, assert no endTimestamp + if (arn.type === 'permabuy') { + assert( + !arn.endTimestamp, + `ARNs name '${arn.name}' has an end timestamp`, + ); + } + } + cursor = nextCursor; + } while (cursor !== undefined); + }); +}); diff --git a/test/transfer.test.js b/tests/transfer.test.mjs similarity index 96% rename from test/transfer.test.js rename to tests/transfer.test.mjs index 078b6c5..46bbdaf 100644 --- a/test/transfer.test.js +++ b/tests/transfer.test.mjs @@ -1,11 +1,11 @@ -const { createAosLoader } = require('./utils'); -const { describe, it } = require('node:test'); -const assert = require('node:assert'); -const { +import { createAosLoader } from './utils.mjs'; +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { AO_LOADER_HANDLER_ENV, DEFAULT_HANDLE_OPTIONS, STUB_ADDRESS, -} = require('../tools/constants'); +} from '../tools/constants.mjs'; describe('Transfers', async () => { const { handle: originalHandle, memory: startMemory } = diff --git a/test/utils.js b/tests/utils.mjs similarity index 79% rename from test/utils.js rename to tests/utils.mjs index 2a2727c..ecd872a 100644 --- a/test/utils.js +++ b/tests/utils.mjs @@ -1,17 +1,17 @@ -const AoLoader = require('@permaweb/ao-loader'); -const { +import AoLoader from '@permaweb/ao-loader'; +import { AOS_WASM, AO_LOADER_HANDLER_ENV, AO_LOADER_OPTIONS, DEFAULT_HANDLE_OPTIONS, BUNDLED_SOURCE_CODE, -} = require('../tools/constants'); +} from '../tools/constants.mjs'; /** * Loads the aos wasm binary and returns the handle function with program memory * @returns {Promise<{handle: Function, memory: WebAssembly.Memory}>} */ -async function createAosLoader() { +export async function createAosLoader() { const handle = await AoLoader(AOS_WASM, AO_LOADER_OPTIONS); const evalRes = await handle( null, @@ -30,7 +30,3 @@ async function createAosLoader() { memory: evalRes.Memory, }; } - -module.exports = { - createAosLoader, -}; diff --git a/tools/bundle-aos.js b/tools/bundle-aos.mjs similarity index 63% rename from tools/bundle-aos.js rename to tools/bundle-aos.mjs index 4042d68..18e8093 100644 --- a/tools/bundle-aos.js +++ b/tools/bundle-aos.mjs @@ -1,6 +1,9 @@ -const path = require('path'); -const fs = require('fs'); -const { bundle } = require('./lua-bundler.js'); +import { bundle } from './lua-bundler.mjs'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); async function main() { console.log('Bundling Lua...'); diff --git a/tools/constants.js b/tools/constants.mjs similarity index 67% rename from tools/constants.js rename to tools/constants.mjs index 48afc0f..b3eddc6 100644 --- a/tools/constants.js +++ b/tools/constants.mjs @@ -1,8 +1,13 @@ -const fs = require('fs'); -const path = require('path'); -const STUB_ADDRESS = ''.padEnd(43, '1'); +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export const STUB_ADDRESS = ''.padEnd(43, '1'); /* ao READ-ONLY Env Variables */ -const AO_LOADER_HANDLER_ENV = { +export const AO_LOADER_HANDLER_ENV = { Process: { Id: STUB_ADDRESS, Owner: STUB_ADDRESS, @@ -14,7 +19,7 @@ const AO_LOADER_HANDLER_ENV = { }, }; -const AO_LOADER_OPTIONS = { +export const AO_LOADER_OPTIONS = { format: 'wasm64-unknown-emscripten-draft_2024_02_15', inputEncoding: 'JSON-1', outputEncoding: 'JSON-1', @@ -23,19 +28,19 @@ const AO_LOADER_OPTIONS = { extensions: [], }; -const AOS_WASM = fs.readFileSync( +export const AOS_WASM = fs.readFileSync( path.join( __dirname, 'fixtures/aos-cbn0KKrBZH7hdNkNokuXLtGryrWM--PjSTBqIzw9Kkk.wasm', ), ); -const BUNDLED_SOURCE_CODE = fs.readFileSync( +export const BUNDLED_SOURCE_CODE = fs.readFileSync( path.join(__dirname, '../dist/aos-bundled.lua'), 'utf-8', ); -const DEFAULT_HANDLE_OPTIONS = { +export const DEFAULT_HANDLE_OPTIONS = { Id: ''.padEnd(43, '1'), ['Block-Height']: '1', // important to set the address so that that `Authority` check passes. Else the `isTrusted` with throw an error. @@ -45,12 +50,3 @@ const DEFAULT_HANDLE_OPTIONS = { From: STUB_ADDRESS, Timestamp: Date.now(), }; - -module.exports = { - BUNDLED_SOURCE_CODE, - AOS_WASM, - AO_LOADER_OPTIONS, - AO_LOADER_HANDLER_ENV, - STUB_ADDRESS, - DEFAULT_HANDLE_OPTIONS, -}; diff --git a/tools/lua-bundler.js b/tools/lua-bundler.mjs similarity index 96% rename from tools/lua-bundler.js rename to tools/lua-bundler.mjs index d39a930..c1bcd39 100644 --- a/tools/lua-bundler.js +++ b/tools/lua-bundler.mjs @@ -1,5 +1,5 @@ -const fs = require('fs'); -const path = require('path'); +import fs from 'fs'; +import path from 'path'; /** * @typedef Module @@ -105,11 +105,9 @@ function exploreNodes(node, cwd) { return requiredModules; } -function bundle(entryLuaPath) { +export function bundle(entryLuaPath) { const project = createProjectStructure(entryLuaPath); const [bundledLua] = createExecutableFromProject(project); return bundledLua; } - -module.exports = { bundle }; diff --git a/tools/observe.js b/tools/observe.js deleted file mode 100644 index 7887abf..0000000 --- a/tools/observe.js +++ /dev/null @@ -1,53 +0,0 @@ -const { IOToken, IO, mIOToken } = require('@ar.io/sdk'); - -const ioContract = IO.init(); -const assert = require('node:assert'); - -(async () => { - try { - let totalDifference = 0; - let totalRewards = 0; - const currentEpoch = await ioContract.getCurrentEpoch(); - const lastEpochDistribution = await ioContract.getEpoch({ - epochIndex: currentEpoch.epochIndex - 1, - }); - if (lastEpochDistribution.distributions.rewards) { - for (const reward of Object.values( - lastEpochDistribution.distributions.rewards || {}, - )) { - totalRewards += reward; - } - totalDifference += - lastEpochDistribution.distributions.totalDistributedRewards - - totalRewards; - } - assert( - totalDifference === 0, - 'Total distributed rewards mismatch. Expected: 0, got: ' + - totalDifference, - ); - - console.log('Total distributed rewards for last epoch: ', { - epochIndex: lastEpochDistribution.epochIndex, - totalDistributedRewards: - lastEpochDistribution.distributions.totalDistributedRewards, - }); - - // get the total supply - const totalSupply = await ioContract.getTokenSupply(); - const expectedTotalSupply = new IOToken(1_000_000_000).toMIO().valueOf(); - assert( - totalSupply === expectedTotalSupply, - 'Total supply mismatch. Expected: ' + - expectedTotalSupply + - ', got: ' + - totalSupply, - ); - console.log( - `Total token supply: ${new mIOToken(totalSupply).toIO().valueOf()} IO`, - ); - } catch (error) { - console.error('Assertion failed:', error.message); - process.exit(1); - } -})(); diff --git a/yarn.lock b/yarn.lock index a98dffa..e07d9b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,19 +2,20 @@ # yarn lockfile v1 -"@ar.io/sdk@^2.1.0-alpha.6": - version "2.1.0-alpha.7" - resolved "https://registry.yarnpkg.com/@ar.io/sdk/-/sdk-2.1.0-alpha.7.tgz#33b6ca4a7e8213b07b4e304c113b7e9e90f328d2" - integrity sha512-v35OICwdtlIgKJPzjNaVH/3ygVWjnm1/aXIwF5HagA14xooFjc0YUAh9MOsz6WEacBrXO+M5za2zy4qUR4D9xQ== +"@ar.io/sdk@alpha": + version "2.2.0-alpha.1" + resolved "https://registry.yarnpkg.com/@ar.io/sdk/-/sdk-2.2.0-alpha.1.tgz#3b4764bf39b1aecfb459ebf9815c2c34b4a8c32b" + integrity sha512-32zHXcruaEPcAxJlKRgBuDp0odhLake44t44qlzjg36CgeunHr/Amtehu0U6FuulGjLrh7OMkUvbyLrcurOU/w== dependencies: "@permaweb/aoconnect" "^0.0.57" arbundles "0.11.0" arweave "1.14.4" - axios "1.7.2" + axios "1.7.3" axios-retry "^4.3.0" eventemitter3 "^5.0.1" plimit-lit "^3.0.1" winston "^3.13.0" + zod "^3.23.8" "@colors/colors@1.6.0", "@colors/colors@^1.6.0": version "1.6.0" @@ -527,7 +528,16 @@ axios-retry@^4.3.0: dependencies: is-retry-allowed "^2.2.0" -axios@1.7.2, axios@^1.4.0: +axios@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.3.tgz#a1125f2faf702bc8e8f2104ec3a76fab40257d85" + integrity sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +axios@^1.4.0: version "1.7.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==