diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e82e89c5..2fb50a51 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,9 +4,7 @@ concurrency: group: ${{ github.ref_name }} cancel-in-progress: true -on: - pull_request: - workflow_call: +on: [push, workflow_dispatch] jobs: build: @@ -31,16 +29,13 @@ jobs: test: runs-on: ubuntu-latest - strategy: - matrix: - node_version: [18.x] steps: - uses: actions/checkout@v4 - name: Set Up node uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node_version }} + node-version-file: .nvmrc cache: 'yarn' - name: Install dependencies @@ -53,3 +48,10 @@ jobs: uses: codecov/codecov-action@v4.0.1 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: Send Slack notification + uses: rtCamp/action-slack-notify@v2 + env: + SLACK_TITLE: ${{ job.status == 'success' && 'Build / E2E tests succeeded!' || 'Build / E2E tests failed!' }} + SLACK_COLOR: ${{ job.status == 'success' && 'good' || 'danger' }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 00000000..123ac74a --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +ignore-engines true diff --git a/README.md b/README.md index 9ca34418..ee99f2db 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,27 @@ This is the home of [ar.io] SDK. This SDK provides functionality for interacting ## Table of Contents + + +- [Table of Contents](#table-of-contents) - [Prerequisites](#prerequisites) - [Installation](#installation) - [Quick Start](#quick-start) - [Usage](#usage) - [Web](#web) + - [Bundlers (Webpack, Rollup, ESbuild, etc.)](#bundlers-webpack-rollup-esbuild-etc) + - [Browser](#browser) - [Node](#node) + - [ESM (NodeNext)](#esm-nodenext) + - [CJS](#cjs) - [Typescript](#typescript) - [IOToken & mIOToken](#iotoken--miotoken) - [Converting IO to mIO](#converting-io-to-mio) - [IO Process](#io-process) - - - [IO APIs](#apis) + - [APIs](#apis) - [`init({ signer })`](#init-signer-) - [`getInfo()`](#getinfo) + - [`getTokenSupply()`](#gettokensupply) - [`getBalance({ address })`](#getbalance-address-) - [`getBalances({ cursor, limit, sortBy, sortOrder })`](#getbalances-cursor-limit-sortby-sortorder-) - [`getGateway({ address })`](#getgateway-address-) @@ -31,6 +38,7 @@ This is the home of [ar.io] SDK. This SDK provides functionality for interacting - [`getEpoch({ epochIndex })`](#getepoch-epochindex-) - [`getCurrentEpoch()`](#getcurrentepoch) - [`getPrescribedObservers({ epochIndex })`](#getprescribedobservers-epochindex-) + - [`getTokenCost({ intent, ...args })`](#gettokencost-intent-args-) - [`joinNetwork(params)`](#joinnetworkparams) - [`leaveNetwork()`](#leavenetwork) - [`updateGatewaySettings(gatewaySettings)`](#updategatewaysettingsgatewaysettings) @@ -42,13 +50,12 @@ This is the home of [ar.io] SDK. This SDK provides functionality for interacting - [`transfer({ target, qty })`](#transfer-target-qty-) - [`increaseUndernameLimit({ name, qty })`](#increaseundernamelimit-name-qty-) - [`extendLease({ name, years })`](#extendlease-name-years-) - - [Configuration](#custom-configuration) - + - [Configuration](#configuration) - [Arweave Name Tokens (ANT's)](#arweave-name-tokens-ants) - - [ANT APIs](#ant-apis) - [`init({ processId, signer })`](#init-processid-signer-) - - [`getInfo()`](#getinfo) + - [`getInfo()`](#getinfo-1) + - [`getState()`](#getstate) - [`getOwner()`](#getowner) - [`getControllers()`](#getcontrollers) - [`getRecords()`](#getrecords) @@ -59,21 +66,19 @@ This is the home of [ar.io] SDK. This SDK provides functionality for interacting - [`removeRecord({ undername })`](#removerecord-undername-) - [`setName({ name })`](#setname-name-) - [`setTicker({ ticker })`](#setticker-ticker-) - - [Configuration](#configuration) - + - [Configuration](#configuration-1) - [Logging](#logging) - - - [Configuration](#configuration) - + - [Configuration](#configuration-2) - [Pagination](#pagination) - - [Developers](#developers) - [Requirements](#requirements) - - [Setup \& Build](#setup--build) + - [Setup & Build](#setup--build) - [Testing](#testing) - - [Linting \& Formatting](#linting--formatting) + - [Linting & Formatting](#linting--formatting) - [Architecture](#architecture) + + ## Prerequisites - `node>=v18.0.0` @@ -123,11 +128,19 @@ const gateways = await io.getGateways(); "failedConsecutiveEpochs": 0, "passedEpochCount": 30, "submittedEpochCount": 30, - "totalEpochParticipationCount": 31, + "totalEpochCount": 31, "totalEpochsPrescribedCount": 31 }, "status": "joined", - "vaults": {} + "vaults": {}, + "weights": { + "compositeWeight": 0.97688888893556, + "gatewayRewardRatioWeight": 1, + "tenureWeight": 0.19444444444444, + "observerRewardRatioWeight": 1, + "normalizedCompositeWeight": 0.19247316211083, + "stakeWeight": 5.02400000024 + } } ], "hasMore": true, @@ -265,6 +278,15 @@ const info = await io.getInfo(); +#### `getTokenSupply()` + +Retrieves the total supply of tokens, returned in mIO. + +```typescript +const io = IO.init(); +const supply = await io.getTokenSupply().then((s) => new mIOToken(s).toIO()); // convert it to IO for readability +``` + #### `getBalance({ address })` Retrieves the balance of the specified wallet address. @@ -276,7 +298,7 @@ const balance = await io .getBalance({ address: 'QGWqtJdLLgm2ehFWiiPzMaoFLD50CnGuzZIPEdoDRGQ', }) - .then((balance) => new mIOToken().toIO()); // convert it to IO for readability + .then((balance: number) => new mIOToken(balance).toIO()); // convert it to IO for readability ```
@@ -354,11 +376,19 @@ const gateway = await io.getGateway({ "failedConsecutiveEpochs": 0, "passedEpochCount": 30, "submittedEpochCount": 30, - "totalEpochParticipationCount": 31, + "totalEpochCount": 31, "totalEpochsPrescribedCount": 31 }, "status": "joined", - "vaults": {} + "vaults": {}, + "weights": { + "compositeWeight": 0.97688888893556, + "gatewayRewardRatioWeight": 1, + "tenureWeight": 0.19444444444444, + "observerRewardRatioWeight": 1, + "normalizedCompositeWeight": 0.19247316211083, + "stakeWeight": 5.02400000024 + } } ``` @@ -402,11 +432,19 @@ Available `sortBy` options are any of the keys on the gateway object, e.g. `oper "failedConsecutiveEpochs": 0, "passedEpochCount": 30, "submittedEpochCount": 30, - "totalEpochParticipationCount": 31, + "totalEpochCount": 31, "totalEpochsPrescribedCount": 31 }, "status": "joined", - "vaults": {} + "vaults": {}, + "weights": { + "compositeWeight": 0.97688888893556, + "gatewayRewardRatioWeight": 1, + "tenureWeight": 0.19444444444444, + "observerRewardRatioWeight": 1, + "normalizedCompositeWeight": 0.19247316211083, + "stakeWeight": 5.02400000024 + } } ], "hasMore": true, @@ -971,7 +1009,7 @@ The ANT client class exposes APIs relevant to compliant Arweave Name Token proce ### ANT APIs -#### `init({ processId, signer )` +#### `init({ processId, signer })` Factory function to that creates a read-only or writeable client. By providing a `signer` additional write APIs that require signing, like `setRecord` and `transfer` are available. By default, a read-only client is returned and no write APIs are available. @@ -1011,6 +1049,49 @@ const info = await ant.getInfo();
+#### `getState()` + +Retrieves the state of the ANT process. + +```typescript +const state = await ant.getState(); +``` + +
+ Output + +```json +{ + "TotalSupply": 1, + "Balances": { + "98O1_xqDLrBKRfQPWjF5p7xZ4Jx6GM8P5PeJn26xwUY": 1 + }, + "Controllers": [], + "Records": { + "v1-0-0_whitepaper": { + "transactionId": "lNjWn3LpyhKC95Kqe-x8X2qgju0j98MhucdDKK85vc4", + "ttlSeconds": 900 + }, + "@": { + "transactionId": "2rMLb2uHAyEt7jSu6bXtKx8e-jOfIf7E-DOgQnm8EtU", + "ttlSeconds": 3600 + }, + "whitepaper": { + "transactionId": "lNjWn3LpyhKC95Kqe-x8X2qgju0j98MhucdDKK85vc4", + "ttlSeconds": 900 + } + }, + "Initialized": true, + "Ticker": "ANT-AR-IO", + "Logo": "Sie_26dvgyok0PZD_-iQAFOhOd5YxDTkczOLoqTTL_A", + "Denomination": 0, + "Name": "AR.IO Foundation", + "Owner": "98O1_xqDLrBKRfQPWjF5p7xZ4Jx6GM8P5PeJn26xwUY" +} +``` + +
+ #### `getOwner()` Returns the owner of the configured ANT process. diff --git a/examples/cjs/index.cjs b/examples/cjs/index.cjs index 16633a5f..813665d0 100644 --- a/examples/cjs/index.cjs +++ b/examples/cjs/index.cjs @@ -1,4 +1,4 @@ -const { IO, ioDevnetProcessId, Logger } = require('@ar.io/sdk'); +const { IO, Logger, IO_TESTNET_PROCESS_ID } = require('@ar.io/sdk'); (async () => { // set the log level for the SDK @@ -8,15 +8,13 @@ const { IO, ioDevnetProcessId, Logger } = require('@ar.io/sdk'); // testnet gateways const testnetGateways = await arIO.getGateways(); const protocolBalance = await arIO.getBalance({ - address: ioDevnetProcessId, + address: IO_TESTNET_PROCESS_ID, }); const ardriveRecord = await arIO.getArNSRecord({ name: 'ardrive' }); - const partialRecords = await arIO - .getArNSRecords({ - page: 10, - pageSize: 5, - }) - .then((page) => page.items); + const partialRecords = await arIO.getArNSRecords({ + page: 10, + pageSize: 5, + }); const oldEpoch = await arIO.getEpoch({ epochIndex: 0, }); @@ -29,12 +27,8 @@ const { IO, ioDevnetProcessId, Logger } = require('@ar.io/sdk'); { testnetGateways, ardriveRecord, - partialRecords, + partialRecords: partialRecords.items, protocolBalance, - arnsStats: { - 'registered domains': Object.keys(allRecords).length, - ardrive: allRecords.ardrive, - }, oldEpoch, epoch, observations, diff --git a/examples/esm/index.mjs b/examples/esm/index.mjs index 91163212..7d6eead8 100644 --- a/examples/esm/index.mjs +++ b/examples/esm/index.mjs @@ -1,32 +1,27 @@ import { ANT, - ArweaveSigner, IO, - ioDevnetProcessId, - spawnANT, + IO_TESTNET_PROCESS_ID, + getANTProcessesOwnedByWallet, } from '@ar.io/sdk'; -import Arweave from 'arweave'; (async () => { - const arIO = IO.init({ - processId: ioDevnetProcessId, - }); - // devnet gateways + const arIO = IO.init(); const testnetGateways = await arIO.getGateways(); const protocolBalance = await arIO.getBalance({ - address: ioDevnetProcessId, + address: IO_TESTNET_PROCESS_ID, }); const contractInfo = await arIO.getInfo(); const ardriveRecord = await arIO.getArNSRecord({ name: 'ardrive' }); - const allRecords = await arIO.getArNSRecords(); + const partialRecords = await arIO.getArNSRecords(); const epoch = await arIO.getCurrentEpoch(); const currentObservations = await arIO.getObservations(); - const observations = await arIO.getObservations({ epochIndex: 19879 }); - const distributions = await arIO.getDistributions({ epochIndex: 19879 }); + const observations = await arIO.getObservations({ epochIndex: 0 }); + const distributions = await arIO.getDistributions({ epochIndex: 0 }); const buyRecordCost = await arIO.getTokenCost({ intent: 'Buy-Record', purchaseType: 'lease', - name: 'adriaaaaan', + name: 'ar-io-dapp-record', years: 1, }); const extendLeaseCost = await arIO.getTokenCost({ @@ -36,7 +31,7 @@ import Arweave from 'arweave'; }); const increaseUndernameCost = await arIO.getTokenCost({ intent: 'Increase-Undername-Limit', - name: 'vilenario', + name: 'ao', quantity: 1, }); @@ -50,7 +45,7 @@ import Arweave from 'arweave'; observations, distributions, protocolBalance, - names: Object.keys(allRecords), + records: partialRecords.items, buyRecordCost, extendLeaseCost, increaseUndernameCost, @@ -58,21 +53,13 @@ import Arweave from 'arweave'; { depth: 2 }, ); - // io ant - const arweave = Arweave.init({ - host: 'arweave.net', - port: 443, - protocol: 'https', - }); - - const jwk = await arweave.wallets.generate(); - - const processId = await spawnANT({ - signer: new ArweaveSigner(jwk), + // fetching ants owned by a wallet using an event emitter + const address = 'ZjmB2vEUlHlJ7-rgJkYP09N5IzLPhJyStVrK5u9dDEo'; + const affiliatedAnts = await getANTProcessesOwnedByWallet({ + address, }); - const ant = ANT.init({ - processId, + processId: affiliatedAnts[0], }); const antRecords = await ant.getRecords(); const rootRecord = await ant.getRecord({ undername: '@' }); @@ -81,6 +68,8 @@ import Arweave from 'arweave'; const info = await ant.getInfo(); console.dir( { + affiliatedAnts, + antProcessId: affiliatedAnts[0], antRecords, rootRecord, controllers, @@ -89,26 +78,4 @@ import Arweave from 'arweave'; }, { depth: 2 }, ); - - // fetching ants owned by a wallet using an event emitter - const address = 'ZjmB2vEUlHlJ7-rgJkYP09N5IzLPhJyStVrK5u9dDEo'; - const processEmitter = new ArNSNameEmitter({ contract: arIO }); - processEmitter.on('error', (e) => { - console.error(e); - }); - processEmitter.on('process', (processId, antState) => - console.log( - `Discovered process owned by wallet called "${antState.names}": `, - processId, - ), - ); - processEmitter.on('end', (res) => { - console.log( - 'Complete', - `${Object.keys(res).length} ids checked with ${antsInError} ants in error.`, - ); - }); - - // kick off the retrieval of ants owned by a process - processEmitter.fetchProcessesOwnedByWallet({ address }); })(); diff --git a/package.json b/package.json index 04c3e225..a86e20b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ar.io/sdk", - "version": "2.0.2", + "version": "2.1.0-alpha.9", "repository": { "type": "git", "url": "git+https://github.com/ar-io/ar-io-sdk.git" @@ -91,6 +91,7 @@ "@types/sinon": "^10.0.15", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^6.4.0", + "arconnect": "^1.0.3", "dotenv": "^16.4.5", "dotenv-cli": "^7.4.2", "esbuild": "^0.19.2", @@ -108,6 +109,7 @@ "husky": "^8.0.3", "jest": "^29.7.0", "lint-staged": "^15.2.2", + "markdown-toc-gen": "^1.0.1", "prettier": "^3.0.2", "rimraf": "^5.0.1", "semantic-release": "^21.0.7", @@ -118,7 +120,7 @@ "vite-plugin-node-polyfills": "^0.22.0" }, "dependencies": { - "@permaweb/aoconnect": "^0.0.55", + "@permaweb/aoconnect": "^0.0.57", "arbundles": "0.11.0", "arweave": "1.14.4", "axios": "1.7.2", @@ -130,6 +132,9 @@ "lint-staged": { "**/*.{ts,js,mjs,cjs,md,json}": [ "prettier --write ." + ], + "**/README.md": [ + "markdown-toc-gen insert" ] } } diff --git a/src/common.ts b/src/common.ts index 79a7ac04..cab11022 100644 --- a/src/common.ts +++ b/src/common.ts @@ -23,15 +23,7 @@ import { spawn, unmonitor, } from '@permaweb/aoconnect'; -import { ArconnectSigner, ArweaveSigner } from 'arbundles'; - -import { - AllowedProtocols, - GatewayConnectionSettings, - GatewayMetadata, - GatewayStakingSettings, -} from './contract-state.js'; -import { mIOToken } from './token.js'; +import { Signer } from 'arbundles'; export type BlockHeight = number; export type SortKey = string; @@ -41,7 +33,7 @@ export type TransactionId = string; export type ProcessId = string; // TODO: append this with other configuration options (e.g. local vs. remote evaluation) -export type ContractSigner = ArweaveSigner | ArconnectSigner; +export type ContractSigner = Signer | Window['arweaveWallet']; export type WithSigner> = { signer: ContractSigner; } & T; // TODO: optionally allow JWK in place of signer @@ -64,43 +56,12 @@ export type WriteParameters = WithSigner< export type AoMessageResult = { id: string }; -// Helper type to overwrite properties of A with B -type Overwrite = { - [K in keyof T]: K extends keyof U ? U[K] : T[K]; -}; - -export type JoinNetworkParams = Overwrite< - GatewayConnectionSettings & GatewayStakingSettings & GatewayMetadata, - { - minDelegatedStake: number | mIOToken; // TODO: this is for backwards compatibility - } ->; - -// Original type definition refined with proper field-specific types -export type UpdateGatewaySettingsParamsBase = { - allowDelegatedStaking?: boolean; - delegateRewardShareRatio?: number; - fqdn?: string; - label?: string; - minDelegatedStake?: number | mIOToken; // TODO: this is for backwards compatibility - eventually we'll drop number - note?: string; - port?: number; - properties?: string; - protocol?: AllowedProtocols; - autoStake?: boolean; - observerAddress?: WalletAddress; -}; - // Utility type to require at least one of the fields export type AtLeastOne< T, U = { [K in keyof T]-?: Record }, > = Partial & U[keyof U]; -// Define the type used for function parameters -export type UpdateGatewaySettingsParams = - AtLeastOne; - export interface HTTPClient { get({ endpoint, diff --git a/src/common/ant-registry.ts b/src/common/ant-registry.ts new file mode 100644 index 00000000..ae2c2a95 --- /dev/null +++ b/src/common/ant-registry.ts @@ -0,0 +1,118 @@ +/** + * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { ANT_REGISTRY_ID } from '../constants.js'; +import { + AoANTRegistryRead, + AoANTRegistryWrite, + AoMessageResult, + AoSigner, + OptionalSigner, + ProcessConfiguration, + WithSigner, + isProcessConfiguration, + isProcessIdConfiguration, +} from '../types.js'; +import { createAoSigner } from '../utils/ao.js'; +import { AOProcess, InvalidContractConfigurationError } from './index.js'; + +export class ANTRegistry { + static init(): AoANTRegistryRead; + static init( + config: Required & { signer?: undefined }, + ): AoANTRegistryRead; + static init({ + signer, + ...config + }: WithSigner>): AoANTRegistryRead; + static init( + config?: OptionalSigner, + ): AoANTRegistryRead | AoANTRegistryWrite { + if (config && config.signer) { + const { signer, ...rest } = config; + return new AoANTRegistryWriteable({ + ...rest, + signer, + }); + } + return new AoANTRegistryReadable(config); + } +} + +export class AoANTRegistryReadable implements AoANTRegistryRead { + protected process: AOProcess; + + constructor(config?: ProcessConfiguration) { + if ( + config && + (isProcessIdConfiguration(config) || isProcessConfiguration(config)) + ) { + if (isProcessConfiguration(config)) { + this.process = config.process; + } else if (isProcessIdConfiguration(config)) { + this.process = new AOProcess({ + processId: config.processId, + }); + } else { + throw new InvalidContractConfigurationError(); + } + } else { + this.process = new AOProcess({ + processId: ANT_REGISTRY_ID, + }); + } + } + + // Should we rename this to "getANTsByAddress"? seems more clear, though not same as handler name + async accessControlList({ + address, + }: { + address: string; + }): Promise<{ Owned: string[]; Controlled: string[] }> { + return this.process.read({ + tags: [ + { name: 'Action', value: 'Access-Control-List' }, + { name: 'Address', value: address }, + ], + }); + } +} + +export class AoANTRegistryWriteable + extends AoANTRegistryReadable + implements AoANTRegistryWrite +{ + private signer: AoSigner; + + constructor({ signer, ...config }: WithSigner) { + super(config); + this.signer = createAoSigner(signer); + } + + async register({ + processId, + }: { + processId: string; + }): Promise { + return this.process.send({ + tags: [ + { name: 'Action', value: 'Register' }, + { name: 'Process-Id', value: processId }, + ], + signer: this.signer, + }); + } +} diff --git a/src/common/ant.ts b/src/common/ant.ts index 0bf9af55..d87d3b3a 100644 --- a/src/common/ant.ts +++ b/src/common/ant.ts @@ -15,12 +15,12 @@ * along with this program. If not, see . */ import { - ANTRecord, AoANTRead, + AoANTRecord, AoANTState, AoANTWrite, AoMessageResult, - ContractSigner, + AoSigner, OptionalSigner, ProcessConfiguration, WalletAddress, @@ -28,6 +28,7 @@ import { isProcessConfiguration, isProcessIdConfiguration, } from '../types.js'; +import { createAoSigner } from '../utils/ao.js'; import { AOProcess, InvalidContractConfigurationError } from './index.js'; export class ANT { @@ -82,6 +83,7 @@ export class AoANTReadable implements AoANTRead { Ticker: string; Denomination: number; Owner: string; + ['Source-Code-TX-ID']?: string; }> { const tags = [{ name: 'Action', value: 'Info' }]; const info = await this.process.read<{ @@ -89,6 +91,7 @@ export class AoANTReadable implements AoANTRead { Ticker: string; Denomination: number; Owner: string; + ['Source-Code-TX-ID']?: string; }>({ tags, }); @@ -104,29 +107,29 @@ export class AoANTReadable implements AoANTRead { * ant.getRecord({ undername: "john" }); * ``` */ - async getRecord({ undername }: { undername: string }): Promise { + async getRecord({ undername }: { undername: string }): Promise { const tags = [ { name: 'Sub-Domain', value: undername }, { name: 'Action', value: 'Record' }, ]; - const record = await this.process.read({ + const record = await this.process.read({ tags, }); return record; } /** - * @returns {Promise>} All the undernames managed by the ANT. + * @returns {Promise>} All the undernames managed by the ANT. * @example * Get the current records * ```ts * ant.getRecords(); * ```` */ - async getRecords(): Promise> { + async getRecords(): Promise> { const tags = [{ name: 'Action', value: 'Records' }]; - const records = await this.process.read>({ + const records = await this.process.read>({ tags, }); return records; @@ -225,14 +228,14 @@ export class AoANTReadable implements AoANTRead { } export class AoANTWriteable extends AoANTReadable implements AoANTWrite { - private signer: ContractSigner; + private signer: AoSigner; constructor({ signer, ...config }: WithSigner>) { super(config); - this.signer = signer; + this.signer = createAoSigner(signer); } /** @@ -251,7 +254,6 @@ export class AoANTWriteable extends AoANTReadable implements AoANTWrite { return this.process.send({ tags, - data: {}, signer: this.signer, }); } @@ -276,7 +278,6 @@ export class AoANTWriteable extends AoANTReadable implements AoANTWrite { return this.process.send({ tags, - data: {}, signer: this.signer, }); } @@ -301,7 +302,6 @@ export class AoANTWriteable extends AoANTReadable implements AoANTWrite { return this.process.send({ tags, - data: {}, signer: this.signer, }); } @@ -332,7 +332,6 @@ export class AoANTWriteable extends AoANTReadable implements AoANTWrite { { name: 'Transaction-Id', value: transactionId }, { name: 'TTL-Seconds', value: ttlSeconds.toString() }, ], - data: { transactionId, ttlSeconds }, signer: this.signer, }); } @@ -355,7 +354,6 @@ export class AoANTWriteable extends AoANTReadable implements AoANTWrite { { name: 'Action', value: 'Remove-Record' }, { name: 'Sub-Domain', value: undername }, ], - data: { undername }, signer: this.signer, }); } @@ -374,7 +372,6 @@ export class AoANTWriteable extends AoANTReadable implements AoANTWrite { { name: 'Action', value: 'Set-Ticker' }, { name: 'Ticker', value: ticker }, ], - data: { ticker }, signer: this.signer, }); } @@ -392,7 +389,6 @@ export class AoANTWriteable extends AoANTReadable implements AoANTWrite { { name: 'Action', value: 'Set-Name' }, { name: 'Name', value: name }, ], - data: { name }, signer: this.signer, }); } diff --git a/src/common/contracts/ao-process.ts b/src/common/contracts/ao-process.ts index caed56b2..01e5d80e 100644 --- a/src/common/contracts/ao-process.ts +++ b/src/common/contracts/ao-process.ts @@ -15,9 +15,8 @@ * along with this program. If not, see . */ import { connect } from '@permaweb/aoconnect'; -import { createData } from 'arbundles'; -import { AOContract, AoClient, ContractSigner } from '../../types.js'; +import { AOContract, AoClient, AoSigner } from '../../types.js'; import { safeDecode } from '../../utils/json.js'; import { version } from '../../version.js'; import { WriteInteractionError } from '../error.js'; @@ -42,34 +41,6 @@ export class AOProcess implements AOContract { this.ao = ao; } - // TODO: could abstract into our own interface that constructs different signers - static async createAoSigner( - signer: ContractSigner, - ): Promise< - (args: { - data: string | Buffer; - tags?: { name: string; value: string }[]; - target?: string; - anchor?: string; - }) => Promise<{ id: string; raw: ArrayBuffer }> - > { - // ensure appropriate permissions are granted with injected signers. - if (signer.publicKey === undefined && 'setPublicKey' in signer) { - await signer.setPublicKey(); - } - - const aoSigner = async ({ data, tags, target, anchor }) => { - const dataItem = createData(data, signer, { tags, target, anchor }); - const signedData = dataItem.sign(signer).then(async () => ({ - id: await dataItem.id, - raw: await dataItem.getRaw(), - })); - return signedData; - }; - - return aoSigner; - } - async read({ tags, retries = 3, @@ -129,15 +100,15 @@ export class AOProcess implements AOContract { throw lastError; } - async send({ + async send({ tags, data, signer, retries = 3, }: { tags: Array<{ name: string; value: string }>; - data?: I; - signer: ContractSigner; + data?: string | undefined; + signer: AoSigner; retries?: number; }): Promise<{ id: string; result?: K }> { // main purpose of retries is to handle network errors/new process delays @@ -157,8 +128,8 @@ export class AOProcess implements AOContract { process: this.processId, // TODO: any other default tags we want to add? tags: [...tags, { name: 'AR-IO-SDK', value: version }], - data: typeof data !== 'string' ? JSON.stringify(data) : data, - signer: await AOProcess.createAoSigner(signer), + data, + signer, }); this.logger.debug(`Sent message to process`, { diff --git a/src/common/index.ts b/src/common/index.ts index 63a144b8..959e5ea1 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -17,6 +17,7 @@ export * from './error.js'; export * from './logger.js'; export * from './ant.js'; +export * from './ant-registry.js'; // ao export * from './io.js'; diff --git a/src/common/io.ts b/src/common/io.ts index 0ec31ba8..9cad01f9 100644 --- a/src/common/io.ts +++ b/src/common/io.ts @@ -17,12 +17,6 @@ import Arweave from 'arweave'; import { IO_TESTNET_PROCESS_ID } from '../constants.js'; -import { - ArNSReservedNameData, - EpochDistributionData, - EpochObservations, - WeightedObserver, -} from '../contract-state.js'; import { AoArNSNameData, AoEpochData, @@ -34,24 +28,29 @@ import { isProcessConfiguration, isProcessIdConfiguration, } from '../io.js'; -import { mIOToken } from '../token.js'; +import { AoSigner, mIOToken } from '../token.js'; import { AoArNSNameDataWithName, + AoArNSReservedNameData, AoBalanceWithAddress, + AoEpochDistributionData, + AoEpochObservationData, AoGatewayWithAddress, + AoJoinNetworkParams, AoMessageResult, + AoUpdateGatewaySettingsParams, + AoWeightedObserver, ContractSigner, - JoinNetworkParams, OptionalSigner, PaginationParams, PaginationResult, ProcessConfiguration, TransactionId, - UpdateGatewaySettingsParams, WalletAddress, WithSigner, WriteOptions, } from '../types.js'; +import { createAoSigner } from '../utils/ao.js'; import { defaultArweave } from './arweave.js'; import { AOProcess } from './contracts/ao-process.js'; import { InvalidContractConfigurationError } from './error.js'; @@ -128,6 +127,12 @@ export class IOReadable implements AoIORead { }); } + async getTokenSupply(): Promise { + return this.process.read({ + tags: [{ name: 'Action', value: 'Total-Token-Supply' }], + }); + } + async getEpochSettings(params?: EpochInput): Promise { const allTags = [ { name: 'Action', value: 'Epoch-Settings' }, @@ -236,9 +241,9 @@ export class IOReadable implements AoIORead { } async getArNSReservedNames(): Promise< - Record | Record + Record | Record > { - return this.process.read>({ + return this.process.read>({ tags: [{ name: 'Action', value: 'Reserved-Names' }], }); } @@ -247,8 +252,8 @@ export class IOReadable implements AoIORead { name, }: { name: string; - }): Promise { - return this.process.read({ + }): Promise { + return this.process.read({ tags: [ { name: 'Action', value: 'Reserved-Name' }, { name: 'Name', value: name }, @@ -347,7 +352,7 @@ export class IOReadable implements AoIORead { async getPrescribedObservers( epoch?: EpochInput, - ): Promise { + ): Promise { const allTags = [ { name: 'Action', value: 'Epoch-Prescribed-Observers' }, { @@ -378,7 +383,7 @@ export class IOReadable implements AoIORead { }): tag is { name: string; value: string } => tag.value !== undefined, ); - return this.process.read({ + return this.process.read({ tags: prunedTags, }); } @@ -419,7 +424,7 @@ export class IOReadable implements AoIORead { }); } - async getObservations(epoch?: EpochInput): Promise { + async getObservations(epoch?: EpochInput): Promise { const allTags = [ { name: 'Action', value: 'Epoch-Observations' }, { @@ -450,12 +455,12 @@ export class IOReadable implements AoIORead { }): tag is { name: string; value: string } => tag.value !== undefined, ); - return this.process.read({ + return this.process.read({ tags: prunedTags, }); } - async getDistributions(epoch?: EpochInput): Promise { + async getDistributions(epoch?: EpochInput): Promise { const allTags = [ { name: 'Action', value: 'Epoch-Distributions' }, { @@ -486,7 +491,7 @@ export class IOReadable implements AoIORead { }): tag is { name: string; value: string } => tag.value !== undefined, ); - return this.process.read({ + return this.process.read({ tags: prunedTags, }); } @@ -572,7 +577,7 @@ export class IOReadable implements AoIORead { export class IOWriteable extends IOReadable implements AoIOWrite { protected declare process: AOProcess; - private signer: ContractSigner; + private signer: AoSigner; constructor({ signer, ...config @@ -588,17 +593,17 @@ export class IOWriteable extends IOReadable implements AoIOWrite { processId: IO_TESTNET_PROCESS_ID, }), }); - this.signer = signer; + this.signer = createAoSigner(signer); } else if (isProcessConfiguration(config)) { super({ process: config.process }); - this.signer = signer; + this.signer = createAoSigner(signer); } else if (isProcessIdConfiguration(config)) { super({ process: new AOProcess({ processId: config.processId, }), }); - this.signer = signer; + this.signer = createAoSigner(signer); } else { throw new InvalidContractConfigurationError(); } @@ -646,10 +651,7 @@ export class IOWriteable extends IOReadable implements AoIOWrite { protocol, autoStake, observerAddress, - }: Omit & { - observerAddress: string; - operatorStake: number | mIOToken; - }, + }: AoJoinNetworkParams, options?: WriteOptions, ): Promise { const { tags = [] } = options || {}; @@ -662,11 +664,11 @@ export class IOWriteable extends IOReadable implements AoIOWrite { }, { name: 'Allow-Delegated-Staking', - value: allowDelegatedStaking.toString(), + value: allowDelegatedStaking?.toString(), }, { name: 'Delegate-Reward-Share-Ratio', - value: delegateRewardShareRatio.toString(), + value: delegateRewardShareRatio?.toString(), }, { name: 'FQDN', @@ -678,7 +680,7 @@ export class IOWriteable extends IOReadable implements AoIOWrite { }, { name: 'Min-Delegated-Stake', - value: minDelegatedStake.valueOf().toString(), + value: minDelegatedStake?.valueOf().toString(), }, { name: 'Note', @@ -686,7 +688,7 @@ export class IOWriteable extends IOReadable implements AoIOWrite { }, { name: 'Port', - value: port.toString(), + value: port?.toString(), }, { name: 'Properties', @@ -698,7 +700,7 @@ export class IOWriteable extends IOReadable implements AoIOWrite { }, { name: 'Auto-Stake', - value: autoStake.toString(), + value: autoStake?.toString(), }, { name: 'Observer-Address', @@ -740,7 +742,7 @@ export class IOWriteable extends IOReadable implements AoIOWrite { protocol, autoStake, observerAddress, - }: UpdateGatewaySettingsParams, + }: AoUpdateGatewaySettingsParams, options?: WriteOptions, ): Promise { const { tags = [] } = options || {}; @@ -862,13 +864,7 @@ export class IOWriteable extends IOReadable implements AoIOWrite { options?: WriteOptions, ): Promise { const { tags = [] } = options || {}; - return this.process.send< - { - reportTxId: TransactionId; - failedGateways: WalletAddress[]; - }, - never - >({ + return this.process.send({ signer: this.signer, tags: [ ...tags, @@ -882,10 +878,6 @@ export class IOWriteable extends IOReadable implements AoIOWrite { value: params.failedGateways.join(','), }, ], - data: { - reportTxId: params.reportTxId, - failedGateways: params.failedGateways, - }, }); } diff --git a/src/constants.ts b/src/constants.ts index 57055089..dd98981a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -37,8 +37,10 @@ export const IO_DEVNET_PROCESS_ID = export const ioDevnetProcessId = IO_DEVNET_PROCESS_ID; export const IO_TESTNET_PROCESS_ID = 'agYcCFJtrMG6cqMuZfskIkFTGvUPddICmtQSBIoPdiA'; + +export const ANT_REGISTRY_ID = 'i_le_yKKPVstLTDSmkHRqf-wYphMnwB9OhleiTgMkWc'; export const MIO_PER_IO = 1_000_000; export const AOS_MODULE_ID = 'cbn0KKrBZH7hdNkNokuXLtGryrWM--PjSTBqIzw9Kkk'; -export const ANT_LUA_ID = '3OlGzE5mrsN2GsxCYM0Tae1KzWepGOr5a94deOWmApM'; +export const ANT_LUA_ID = 'Flwio4Lr08g6s6uim6lEJNnVGD9ylvz0_aafvpiL8FI'; export const DEFAULT_SCHEDULER_ID = '_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA'; diff --git a/src/contract-state.ts b/src/contract-state.ts deleted file mode 100644 index 0ba06f82..00000000 --- a/src/contract-state.ts +++ /dev/null @@ -1,303 +0,0 @@ -/** - * Copyright (C) 2022-2024 Permanent Data Solutions, Inc. All Rights Reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -import { WalletAddress } from './common.js'; - -export type Balances = Record; -export type Fees = Record; -export type Observations = Record; - -export const ioContractReadInteractions = [ - 'gateway', - 'gateways', - 'prescribedObservers', - 'record', - 'auction', - 'balance', - 'epoch', - 'priceForInteraction', -] as const; - -export const ioContractWriteInteractions = [ - 'delegateStake', - 'decreaseDelegateStake', - 'joinNetwork', - 'increaseOperatorStake', - 'decreaseOperatorStake', - 'updateGatewaySettings', - 'saveObservations', - 'extendRecord', - 'buyRecord', - 'increaseUndernameCount', - 'transfer', -] as const; - -export const ioContractInteractions = [ - ...ioContractReadInteractions, - ...ioContractWriteInteractions, -] as const; - -export type IOContractReadInteractions = - (typeof ioContractReadInteractions)[number]; -export type IOContractWriteInteractions = - (typeof ioContractWriteInteractions)[number]; -export type IOContractInteractions = (typeof ioContractInteractions)[number]; - -export type IOContractInteractionsWithIOFees = Extract< - IOContractInteractions, - 'buyRecord' | 'extendRecord' | 'increaseUndernameCount' ->; - -export const AR_IO_CONTRACT_FUNCTIONS: Record = - { - GATEWAY: 'gateway', - GATEWAYS: 'gateways', - PRESCRIBED_OBSERVERS: 'prescribedObservers', - DELEGATE_STAKE: 'delegateStake', - DECREASE_DELEGATE_STAKE: 'decreaseDelegateStake', - JOIN_NETWORK: 'joinNetwork', - INCREASE_OPERATOR_STAKE: 'increaseOperatorStake', - DECREASE_OPERATOR_STAKE: 'decreaseOperatorStake', - UPDATE_GATEWAY_SETTINGS: 'updateGatewaySettings', - SAVE_OBSERVATIONS: 'saveObservations', - EXTEND_RECORD: 'extendRecord', - INCREASE_UNDERNAME_COUNT: 'increaseUndernameCount', - RECORD: 'record', - AUCTION: 'auction', - TRANSFER: 'transfer', - BALANCE: 'balance', - PRICE_FOR_INTERACTION: 'priceForInteraction', - EPOCH: 'epoch', - BUY_RECORD: 'buyRecord', - }; - -// Gateways - -export type Gateway = { - delegates: Record; - end: number; - observerWallet: WalletAddress; - operatorStake: number; - settings: GatewaySettings; - start: number; - stats: GatewayStats; - status: string; - totalDelegatedStake: number; - vaults: Record; - weights: ObserverWeights; -}; - -export type GatewayDelegate = { - delegatedStake: number; - start: number; - vaults: Record; -}; - -export type GatewayStakingSettings = { - allowDelegatedStaking: boolean; - delegateRewardShareRatio: number; - minDelegatedStake: number; - autoStake: boolean; -}; - -export type GatewayMetadata = { - label: string; - note: string; - properties: string; -}; - -export type GatewayConnectionSettings = { - fqdn: string; - port: number; - protocol: AllowedProtocols; -}; - -export type GatewaySettings = GatewayConnectionSettings & - GatewayStakingSettings & - GatewayMetadata; - -export type AllowedProtocols = 'https'; - -export type GatewayStats = { - failedConsecutiveEpochs: number; - passedEpochCount: number; - submittedEpochCount: number; - totalEpochParticipationCount: number; - totalEpochsPrescribedCount: number; -}; - -// Observations - -export type WeightedObserver = { - gatewayAddress: WalletAddress; - observerAddress: WalletAddress; - stake: number; - start: number; -} & ObserverWeights; - -export type ObserverWeights = { - stakeWeight: number; - tenureWeight: number; - gatewayRewardRatioWeight: number; - observerRewardRatioWeight: number; - compositeWeight: number; - normalizedCompositeWeight: number; -}; - -// Records -export type RegistrationType = 'lease' | 'permabuy'; -export type ArNSBaseNameData = { - contractTxId: string; // The ANT Contract used to manage this name - startTimestamp: number; // At what unix time (seconds since epoch) the lease starts - type: RegistrationType; - undernames: number; - purchasePrice: number; -}; - -export type ArNSPermabuyData = ArNSBaseNameData & { - type: 'permabuy'; -}; - -export type ArNSLeaseData = ArNSBaseNameData & { - type: 'lease'; - endTimestamp: number; // At what unix time (seconds since epoch) the lease ends -}; - -// TODO: break this up into union types to account for names that are permanently reserved -export type ArNSReservedNameData = { - target?: string; - endTimestamp?: number; -}; - -export type ArNSNameData = ArNSPermabuyData | ArNSLeaseData; - -// Vaults - -export type VaultData = { - balance: number; - start: number; - end: number; -}; - -// Balances - -export enum DENOMINATIONS { - IO = 'IO', - MIO = 'mIO', -} - -export type ReservedNameData = { - target?: string; // The target wallet address this name is reserved for - endTimestamp?: number; // At what unix time (seconds since epoch) this reserved name becomes available -}; - -export type ArNSBaseAuctionData = { - startPrice: number; - floorPrice: number; - startHeight: number; - endHeight: number; - type: RegistrationType; - initiator: string; - contractTxId: string; -}; - -export type ArNSLeaseAuctionData = ArNSBaseAuctionData & { - type: 'lease'; - years: 1; -}; - -export type ArNSPermabuyAuctionData = ArNSBaseAuctionData & { - type: 'permabuy'; -}; - -export type ArNSAuctionData = ArNSLeaseAuctionData | ArNSPermabuyAuctionData; - -export type DemandFactoringData = { - periodZeroBlockHeight: number; // TODO: The block height at which the contract was initialized - currentPeriod: number; - trailingPeriodPurchases: number[]; // Acts as a ring buffer of trailing period purchase counts - trailingPeriodRevenues: number[]; // Acts as a ring buffer of trailing period revenues - purchasesThisPeriod: number; - revenueThisPeriod: number; - demandFactor: number; - consecutivePeriodsWithMinDemandFactor: number; -}; - -export type EpochObservations = { - failureSummaries: Record; // an observers summary of all failed gateways in the epoch - reports: Record; // a reference point for the report submitted by this observer -}; - -export type EpochDistributionData = { - epochZeroStartHeight: number; - epochStartHeight: number; // the current epoch start height - epochEndHeight: number; // the current epoch end height - epochPeriod: number; - epochDistributionHeight: number; - epochBlockLength: number; -}; - -export type Vaults = Record; - -export type RegistryVaults = Record; - -export type PrescribedObservers = Record; - -export interface ArIOState { - balances: Balances; - name: string; // The friendly name of the token, shown in block explorers and marketplaces - records: Record; // The list of all ArNS names and their associated data - gateways: Record; // each gateway uses its public arweave wallet address to identify it in the gateway registry - fees: Fees; // starting list of all fees for purchasing ArNS names - reserved: Record; // list of all reserved names that are not allowed to be purchased at this time - auctions: Record; - lastTickedHeight: number; // periodicity management - demandFactoring: DemandFactoringData; - observations: Observations; - distributions: EpochDistributionData; - vaults: RegistryVaults; - prescribedObservers: PrescribedObservers; -} - -// ANT - -export type ANTRecord = { - transactionId: string; - ttlSeconds: number; -}; - -export type ANTRecords = Record & { '@': ANTRecord }; - -export type ANTState = { - owner: WalletAddress; - controllers: WalletAddress[]; - name: string; - ticker: string; - records: ANTRecords; - balances: Balances; -}; - -export const ANT_CONTRACT_FUNCTIONS = { - TRANSFER: 'transfer', - SET_CONTROLLER: 'setController', - REMOVE_CONTROLLER: 'removeController', - SET_NAME: 'setName', - SET_TICKER: 'setTicker', - SET_RECORD: 'setRecord', - REMOVE_RECORD: 'removeRecord', - BALANCE: 'balance', - EVOLVE: 'evolve', -}; diff --git a/src/io.ts b/src/io.ts index 8d73c15e..47df5fca 100644 --- a/src/io.ts +++ b/src/io.ts @@ -15,53 +15,21 @@ * along with this program. If not, see . */ import { AOProcess } from './common/index.js'; -import { - ANTRecord, - ArNSReservedNameData, - EpochDistributionData, - EpochObservations, - GatewayDelegate, - GatewaySettings, - RegistrationType, - VaultData, - WeightedObserver, -} from './contract-state.js'; import { mIOToken } from './token.js'; import { AoMessageResult, + AoSigner, + AtLeastOne, BlockHeight, - ContractSigner, - JoinNetworkParams, ProcessId, Timestamp, TransactionId, - UpdateGatewaySettingsParams, WalletAddress, WriteOptions, } from './types.js'; import { validateArweaveId } from './utils/arweave.js'; -export function isProcessConfiguration( - config: object, -): config is { process: AOProcess } { - return 'process' in config; -} - -export function isProcessIdConfiguration( - config: object, -): config is { processId: string } { - return ( - 'processId' in config && - typeof config.processId === 'string' && - validateArweaveId(config.processId) === true - ); -} - -export function isLeasedArNSRecord( - record: AoArNSNameData, -): record is AoArNSLeaseData { - return record.type === 'lease' && record.endTimestamp !== undefined; -} +// Pagination export type PaginationParams = { cursor?: string; @@ -79,6 +47,7 @@ export type PaginationResult = { hasMore: boolean; }; +// Configuration export type ProcessConfiguration = | { process?: AOProcess; @@ -96,6 +65,188 @@ export type EpochInput = } | undefined; +// AO/IO Contract +export type AoBalances = Record; +export type AoFees = Record; +export type AoObservations = Record; +export type AoEpochIndex = number; + +export interface AoIOState { + GatewayRegistry: Record; + Epochs: Record; + NameRegistry: { + records: Record; + reserved: Record; + }; + Balances: Record; + Vaults: Record; + Ticker: string; + Name: string; + Logo: string; +} + +export type AoEpochObservationData = { + failureSummaries: Record; + reports: Record; +}; + +export type AoVaultData = { + balance: number; + locked: number; + endTimestamp: Timestamp; +}; + +export type AoEpochDistributionData = { + rewards: Record; + distributedTimestamp: Timestamp; + totalDistributedRewards: number; + totalEligibleRewards: number; +}; + +export type AoArNSReservedNameData = { + target?: string; + endTimestamp?: number; +}; +export type AoArNSNameData = AoArNSPermabuyData | AoArNSLeaseData; +export type AoArNSNameDataWithName = AoArNSNameData & { name: string }; +export type AoArNSBaseNameData = { + processId: ProcessId; + startTimestamp: number; + type: 'lease' | 'permabuy'; + undernameLimit: number; + purchasePrice: number; +}; + +export type AoArNSPermabuyData = AoArNSBaseNameData & { + type: 'permabuy'; +}; + +export type AoArNSLeaseData = AoArNSBaseNameData & { + type: 'lease'; + endTimestamp: Timestamp; +}; + +export type AoEpochSettings = { + epochZeroStartTimestamp: Timestamp; + durationMs: number; + prescribedNameCount: number; + rewardPercentage: number; + maxObservers: number; + distributionDelayMs: number; +}; + +export type AoEpochData = { + epochIndex: AoEpochIndex; + startHeight: BlockHeight; + observations: AoObservations; + prescribedObservers: AoWeightedObserver[]; + startTimestamp: Timestamp; + endTimestamp: Timestamp; + distributionTimestamp: Timestamp; + distributions: AoEpochDistributionData; +}; + +export type AoGateway = { + settings: AoGatewaySettings; + stats: AoGatewayStats; + delegates: Record; + totalDelegatedStake: number; + vaults: Record; + startTimestamp: Timestamp; + endTimestamp: Timestamp; + observerAddress: WalletAddress; + operatorStake: number; + status: 'joined' | 'leaving'; + weights: AoGatewayWeights; +}; + +export type AoGatewayStats = { + passedConsecutiveEpochs: number; + failedConsecutiveEpochs: number; + totalEpochParticipationCount: number; + passedEpochCount: number; + failedEpochCount: number; + observedEpochCount: number; + prescribedEpochCount: number; +}; + +export type AoWeightedObserver = { + gatewayAddress: WalletAddress; + observerAddress: WalletAddress; + stake: number; + startTimestamp: number; +} & AoGatewayWeights; + +export type AoGatewayWeights = { + stakeWeight: number; + tenureWeight: number; + gatewayRewardRatioWeight: number; + observerRewardRatioWeight: number; + compositeWeight: number; + normalizedCompositeWeight: number; +}; + +export type AoGatewayWithAddress = AoGateway & { + gatewayAddress: WalletAddress; +}; + +export type AoGatewayDelegate = { + delegatedStake: number; + startTimestamp: Timestamp; + vaults: Record; +}; + +export type AoGatewaySettings = { + allowDelegatedStaking: boolean; + delegateRewardShareRatio: number; + minDelegatedStake: number; + autoStake: boolean; + label: string; + note: string; + properties: string; + fqdn: string; + port: number; + protocol: 'https'; +}; + +export type AoBalanceWithAddress = { + address: WalletAddress; + balance: number; +}; + +// ANT Contract + +export type AoANTState = { + Name: string; + Ticker: string; + Denomination: number; + Owner: WalletAddress; + Controllers: WalletAddress[]; + Records: Record; + Balances: Record; + Logo: string; + TotalSupply: number; + Initialized: boolean; +}; + +export type AoANTRecord = { + transactionId: string; + ttlSeconds: number; +}; + +// Input types + +// TODO: confirm what is required or if all can be optional and defaults will be provided +export type AoJoinNetworkParams = Pick< + AoGateway, + 'operatorStake' | 'observerAddress' +> & + Partial; + +export type AoUpdateGatewaySettingsParams = AtLeastOne; + +// Interfaces + export interface AOContract { read({ tags, @@ -104,14 +255,14 @@ export interface AOContract { tags?: { name: string; value: string }[]; retries?: number; }): Promise; - send({ + send({ tags, data, signer, }: { tags: { name: string; value: string }[]; - data: I; - signer: ContractSigner; + data: string | undefined; + signer: AoSigner; }): Promise<{ id: string; result?: K }>; } @@ -123,6 +274,7 @@ export interface AoIORead { Logo: string; Denomination: number; }>; + getTokenSupply(): Promise; getEpochSettings(params?: EpochInput): Promise; getGateway({ address, @@ -151,13 +303,13 @@ export interface AoIORead { name, }: { name: string; - }): Promise; + }): Promise; getEpoch(epoch?: EpochInput): Promise; getCurrentEpoch(): Promise; - getPrescribedObservers(epoch?: EpochInput): Promise; + getPrescribedObservers(epoch?: EpochInput): Promise; getPrescribedNames(epoch?: EpochInput): Promise; - getObservations(epoch?: EpochInput): Promise; - getDistributions(epoch?: EpochInput): Promise; + getObservations(epoch?: EpochInput): Promise; + getDistributions(epoch?: EpochInput): Promise; getTokenCost({ intent, purchaseType, @@ -199,10 +351,7 @@ export interface AoIOWrite extends AoIORead { protocol, autoStake, observerAddress, - }: Omit & { - observerAddress: string; - operatorStake: number | mIOToken; - }, + }: AoJoinNetworkParams, options?: WriteOptions, ): Promise; leaveNetwork(options?: WriteOptions): Promise; @@ -219,7 +368,7 @@ export interface AoIOWrite extends AoIORead { protocol, autoStake, observerAddress, - }: UpdateGatewaySettingsParams, + }: AoUpdateGatewaySettingsParams, options?: WriteOptions, ): Promise; increaseOperatorStake( @@ -287,9 +436,10 @@ export interface AoANTRead { Ticker: string; Denomination: number; Owner: string; + ['Source-Code-TX-ID']?: string; }>; - getRecord({ undername }): Promise; - getRecords(): Promise>; + getRecord({ undername }): Promise; + getRecords(): Promise>; getOwner(): Promise; getControllers(): Promise; getTicker(): Promise; @@ -324,109 +474,35 @@ export interface AoANTWrite extends AoANTRead { setName({ name }): Promise; } -// AO Contract types -export interface AoIOState { - GatewayRegistry: Record; - Epochs: Record; - NameRegistry: { - records: Record; - reserved: Record; - }; - Balances: Record; - Vaults: Record; - Ticker: string; - Name: string; - Logo: string; +export interface AoANTRegistryRead { + accessControlList(params: { + address: string; + }): Promise<{ Owned: string[]; Controlled: string[] }>; } -export type AoEpochIndex = number; -export type AoArNSReservedNameData = ArNSReservedNameData; -export type AoArNSNameData = AoArNSPermabuyData | AoArNSLeaseData; -export type AoArNSNameDataWithName = AoArNSNameData & { name: string }; -export type AoArNSBaseNameData = { - processId: ProcessId; - startTimestamp: number; - type: RegistrationType; - undernameLimit: number; - purchasePrice: number; -}; - -export type AoArNSPermabuyData = AoArNSBaseNameData & { - type: 'permabuy'; -}; - -export type AoArNSLeaseData = AoArNSBaseNameData & { - type: 'lease'; - endTimestamp: number; // At what unix time (seconds since epoch) the lease ends -}; - -export type AoEpochSettings = { - epochZeroStartTimestamp: Timestamp; - durationMs: number; - prescribedNameCount: number; - rewardPercentage: number; - maxObservers: number; - distributionDelayMs: number; -}; - -export type AoEpochData = { - epochIndex: AoEpochIndex; - startHeight: BlockHeight; - observations: EpochObservations; - prescribedObservers: WeightedObserver[]; - startTimestamp: Timestamp; - endTimestamp: Timestamp; - distributionTimestamp: Timestamp; - distributions: { - rewards: Record; - distributedTimestamp: Timestamp; - totalDistributedRewards: number; - totalEligibleRewards: number; - }; -}; - -export type AoGatewayStats = { - passedConsecutiveEpochs: number; - failedConsecutiveEpochs: number; - totalEpochParticipationCount: number; - passedEpochCount: number; - failedEpochCount: number; - observedEpochCount: number; - prescribedEpochCount: number; -}; - -export type AoGateway = { - settings: GatewaySettings; - stats: AoGatewayStats; - delegates: Record; - totalDelegatedStake: number; - vaults: Record; - startTimestamp: Timestamp; - endTimestamp: Timestamp; - observerAddress: WalletAddress; - operatorStake: number; - status: 'joined' | 'leaving'; - // TODO: add weights -}; +export interface AoANTRegistryWrite extends AoANTRegistryRead { + register(params: { processId: string }): Promise; +} -export type AoBalanceWithAddress = { - address: WalletAddress; - balance: number; -}; +// Typeguard functions +export function isProcessConfiguration( + config: object, +): config is { process: AOProcess } { + return 'process' in config; +} -export type AoGatewayWithAddress = AoGateway & { - gatewayAddress: WalletAddress; -}; +export function isProcessIdConfiguration( + config: object, +): config is { processId: string } { + return ( + 'processId' in config && + typeof config.processId === 'string' && + validateArweaveId(config.processId) === true + ); +} -export type AoANTState = { - Name: string; - Ticker: string; - Denomination: number; - Owner: WalletAddress; - Controllers: WalletAddress[]; - Records: Record; - Balances: Record; - Logo: string; - TotalSupply: number; - Initialized: boolean; -}; +export function isLeasedArNSRecord( + record: AoArNSNameData, +): record is AoArNSLeaseData { + return record.type === 'lease'; +} diff --git a/src/token.ts b/src/token.ts index c9ff0553..16fbcc14 100644 --- a/src/token.ts +++ b/src/token.ts @@ -153,3 +153,10 @@ export class mIOToken extends PositiveFiniteInteger { return new IOToken(this.valueOf() / MIO_PER_IO); } } + +export type AoSigner = (args: { + data: string | Buffer; + tags?: { name: string; value: string }[]; + target?: string; + anchor?: string; +}) => Promise<{ id: string; raw: ArrayBuffer }>; diff --git a/src/types.ts b/src/types.ts index 05383621..34f378e2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,7 +14,6 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -export * from './contract-state.js'; export * from './common.js'; export * from './token.js'; export * from './io.js'; diff --git a/src/utils/ao.ts b/src/utils/ao.ts index 2b15c664..9a810bf6 100644 --- a/src/utils/ao.ts +++ b/src/utils/ao.ts @@ -14,17 +14,24 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import { connect } from '@permaweb/aoconnect'; +import { connect, createDataItemSigner } from '@permaweb/aoconnect'; +import { createData } from 'arbundles'; import { defaultArweave } from '../common/arweave.js'; import { AOProcess } from '../common/index.js'; import { ANT_LUA_ID, + ANT_REGISTRY_ID, AOS_MODULE_ID, DEFAULT_SCHEDULER_ID, } from '../constants.js'; -import { ANTState } from '../contract-state.js'; -import { AoClient, ContractSigner } from '../types.js'; +import { + AoANTRecord, + AoClient, + AoSigner, + ContractSigner, + WalletAddress, +} from '../types.js'; export async function spawnANT({ signer, @@ -34,14 +41,23 @@ export async function spawnANT({ scheduler = DEFAULT_SCHEDULER_ID, state, stateContractTxId, + antRegistryId = ANT_REGISTRY_ID, }: { - signer: ContractSigner; + signer: AoSigner; module?: string; luaCodeTxId?: string; ao?: AoClient; scheduler?: string; - state?: ANTState; + state?: { + owner: WalletAddress; + controllers: WalletAddress[]; + name: string; + ticker: string; + records: Record; + balances: Record; + }; stateContractTxId?: string; + antRegistryId?: string; }): Promise { //TODO: cache locally and only fetch if not cached const luaString = (await defaultArweave.transactions.getData(luaCodeTxId, { @@ -52,7 +68,13 @@ export async function spawnANT({ const processId = await ao.spawn({ module, scheduler, - signer: await AOProcess.createAoSigner(signer), + signer, + tags: [ + { + name: 'ANT-Registry-Id', + value: antRegistryId, + }, + ], }); const aosClient = new AOProcess({ @@ -85,3 +107,63 @@ export async function spawnANT({ return processId; } + +export async function evolveANT({ + signer, + processId, + luaCodeTxId = ANT_LUA_ID, + ao = connect(), +}: { + signer: AoSigner; + processId: string; + luaCodeTxId?: string; + ao?: AoClient; +}): Promise { + const aosClient = new AOProcess({ + processId, + ao, + }); + + //TODO: cache locally and only fetch if not cached + const luaString = (await defaultArweave.transactions.getData(luaCodeTxId, { + decode: true, + string: true, + })) as string; + + const { id } = await aosClient.send({ + tags: [ + { name: 'Action', value: 'Eval' }, + { name: 'App-Name', value: 'ArNS-ANT' }, + { name: 'Source-Code-TX-ID', value: luaCodeTxId }, + ], + data: luaString, + signer, + }); + + return id; +} + +export function createAoSigner(signer: ContractSigner): AoSigner { + if (!('publicKey' in signer)) { + return createDataItemSigner(signer) as AoSigner; + } + + const aoSigner = async ({ data, tags, target, anchor }) => { + // ensure appropriate permissions are granted with injected signers. + if ( + signer.publicKey === undefined && + 'setPublicKey' in signer && + typeof signer.setPublicKey === 'function' + ) { + await signer.setPublicKey(); + } + const dataItem = createData(data, signer, { tags, target, anchor }); + const signedData = dataItem.sign(signer).then(async () => ({ + id: await dataItem.id, + raw: await dataItem.getRaw(), + })); + return signedData; + }; + + return aoSigner; +} diff --git a/src/utils/json.ts b/src/utils/json.ts index 0afbe79c..47d45d93 100644 --- a/src/utils/json.ts +++ b/src/utils/json.ts @@ -14,6 +14,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function safeDecode(data: any): R { try { return JSON.parse(data); diff --git a/src/utils/processes.ts b/src/utils/processes.ts index e2517141..15b59d66 100644 --- a/src/utils/processes.ts +++ b/src/utils/processes.ts @@ -17,11 +17,13 @@ import { EventEmitter } from 'eventemitter3'; import { pLimit } from 'plimit-lit'; +import { ANTRegistry } from '../common/ant-registry.js'; import { ANT } from '../common/ant.js'; import { IO } from '../common/io.js'; -import { ILogger } from '../common/logger.js'; +import { ILogger, Logger } from '../common/logger.js'; import { IO_TESTNET_PROCESS_ID } from '../constants.js'; import { + AoANTRegistryRead, AoANTState, AoArNSNameData, AoIORead, @@ -29,47 +31,18 @@ import { WalletAddress, } from '../types.js'; +/** + * @beta This API is in beta and may change in the future. + */ export const getANTProcessesOwnedByWallet = async ({ address, - contract = IO.init({ - processId: IO_TESTNET_PROCESS_ID, - }), + registry = ANTRegistry.init(), }: { address: WalletAddress; - contract?: AoIORead; + registry?: AoANTRegistryRead; }): Promise => { - const throttle = pLimit(50); - // get the record names of the registry - TODO: this may need to be paginated - const records: Record = await fetchAllArNSRecords({ - contract: contract, - }); - const uniqueContractProcessIds = Object.values(records) - .filter((record) => record.processId !== undefined) - .map((record) => record.processId); - - // check the contract owner and controllers - const ownedOrControlledByWallet = await Promise.all( - uniqueContractProcessIds.map(async (processId) => - throttle(async () => { - const ant = ANT.init({ - processId, - }); - const { Owner, Controllers } = await ant.getState(); - - if (Owner === address || Controllers.includes(address)) { - return processId; - } - return; - }), - ), - ); - - if (ownedOrControlledByWallet.length === 0) { - return []; - } - - // TODO: insert gql query to find ANT processes owned by wallet given wallet not currently in the registry - return [...new Set(ownedOrControlledByWallet)] as string[]; + const res = await registry.accessControlList({ address }); + return [...new Set([...res.Owned, ...res.Controlled])]; }; function timeout(ms: number, promise) { @@ -94,56 +67,75 @@ export class ArNSEventEmitter extends EventEmitter { protected contract: AoIORead; private timeoutMs: number; // timeout for each request to 3 seconds private throttle; + private logger: ILogger; constructor({ contract = IO.init({ processId: IO_TESTNET_PROCESS_ID, }), timeoutMs = 60_000, concurrency = 30, + logger = Logger.default, }: { contract?: AoIORead; timeoutMs?: number; concurrency?: number; - }) { + logger?: ILogger; + } = {}) { super(); this.contract = contract; this.timeoutMs = timeoutMs; this.throttle = pLimit(concurrency); + this.logger = logger; } - async fetchProcessesOwnedByWallet({ address }: { address: WalletAddress }) { + async fetchProcessesOwnedByWallet({ + address, + pageSize, + antRegistry = ANTRegistry.init(), + }: { + address: WalletAddress; + pageSize?: number; + antRegistry?: AoANTRegistryRead; + }) { const uniqueContractProcessIds: Record< string, - { state: AoANTState | undefined; names: Record } + { + state: AoANTState | undefined; + names: Record; + } > = {}; - + const antIdRes = await antRegistry.accessControlList({ address }); + const antIds = new Set([...antIdRes.Owned, ...antIdRes.Controlled]); await timeout( this.timeoutMs, - fetchAllArNSRecords({ contract: this.contract }), + fetchAllArNSRecords({ contract: this.contract, emitter: this, pageSize }), ) .catch((e) => { this.emit('error', `Error getting ArNS records: ${e}`); + this.logger.error(`Error getting ArNS records`, { + message: e?.message, + stack: e?.stack, + }); return {}; }) - .then((records) => { - if (!records) return; - Object.entries(records).forEach(([name, record]) => { - if (record.processId === undefined) { - return; + .then((records: Record) => { + Object.entries(records).forEach(([name, arnsRecord]) => { + if (antIds.has(arnsRecord.processId)) { + if (uniqueContractProcessIds[arnsRecord.processId] == undefined) { + uniqueContractProcessIds[arnsRecord.processId] = { + state: undefined, + names: {}, + }; + } + uniqueContractProcessIds[arnsRecord.processId].names[name] = + arnsRecord; } - if (uniqueContractProcessIds[record.processId] === undefined) { - uniqueContractProcessIds[record.processId] = { - state: undefined, - names: {}, - }; - } - uniqueContractProcessIds[record.processId].names[name] = record; }); }); const idCount = Object.keys(uniqueContractProcessIds).length; - // check the contract owner and controllers this.emit('progress', 0, idCount); + // check the contract owner and controllers await Promise.all( Object.keys(uniqueContractProcessIds).map(async (processId, i) => this.throttle(async () => { @@ -188,21 +180,30 @@ export const fetchAllArNSRecords = async ({ contract = IO.init({ processId: IO_TESTNET_PROCESS_ID, }), - logger, + emitter, + logger = Logger.default, + pageSize = 50_000, }: { contract?: AoIORead; + emitter?: EventEmitter; logger?: ILogger; + pageSize?: number; }): Promise> => { let cursor: string | undefined; + const startTimestamp = Date.now(); const records: Record = {}; do { const pageResult = await contract - .getArNSRecords({ cursor }) + .getArNSRecords({ cursor, limit: pageSize }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any .catch((e: any) => { logger?.error(`Error getting ArNS records`, { message: e?.message, stack: e?.stack, }); + + emitter?.emit('arns:error', `Error getting ArNS records: ${e}`); + return undefined; }); @@ -214,8 +215,29 @@ export const fetchAllArNSRecords = async ({ const { name, ...recordDetails } = record; records[name] = recordDetails; }); + + logger.debug('Fetched page of ArNS records', { + totalRecordCount: pageResult.totalItems, + fetchedRecordCount: Object.keys(records).length, + cursor: pageResult.nextCursor, + }); + + emitter?.emit('arns:pageLoaded', { + totalRecordCount: pageResult.totalItems, + fetchedRecordCount: Object.keys(records).length, + records: pageResult.items, + cursor: pageResult.nextCursor, + }); + cursor = pageResult.nextCursor; } while (cursor !== undefined); + emitter?.emit('arns:end', records); + + logger.debug('Fetched all ArNS records', { + totalRecordCount: Object.keys(records).length, + durationMs: Date.now() - startTimestamp, + }); + return records; }; diff --git a/src/version.ts b/src/version.ts index fd984ab2..98eb6c3c 100644 --- a/src/version.ts +++ b/src/version.ts @@ -17,4 +17,4 @@ // AUTOMATICALLY GENERATED FILE - DO NOT TOUCH -export const version = '2.0.2'; +export const version = '2.1.0-alpha.10'; diff --git a/tests/e2e/cjs/index.test.js b/tests/e2e/cjs/index.test.js index 72441c8c..05ab78a3 100644 --- a/tests/e2e/cjs/index.test.js +++ b/tests/e2e/cjs/index.test.js @@ -4,7 +4,7 @@ const assert = require('node:assert/strict'); * Ensure that npm link has been ran prior to running these tests * (simply running npm run test:integration will ensure npm link is ran) */ -const { IO, ioDevnetProcessId } = require('@ar.io/sdk'); +const { IO, ioDevnetProcessId, ANTRegistry } = require('@ar.io/sdk'); const io = IO.init({ processId: ioDevnetProcessId, @@ -15,6 +15,11 @@ describe('IO', async () => { assert.ok(epoch); }); + it('should be able to get the total token supply', async () => { + const tokenSupply = await io.getTokenSupply(); + assert.ok(tokenSupply); + }); + it('should be able to get first set of arns records', async () => { const records = await io.getArNSRecords(); assert.ok(records); @@ -82,11 +87,6 @@ describe('IO', async () => { assert.ok(epochSettings); }); - it('should be able to get the current prescribed observers', async () => { - const observers = await io.getPrescribedObservers(); - assert.ok(observers); - }); - it('should be able to get reserved names', async () => { const reservedNames = await io.getArNSReservedNames(); assert.ok(reservedNames); @@ -118,6 +118,14 @@ describe('IO', async () => { assert(typeof gateway.startTimestamp === 'number'); assert(typeof gateway.operatorStake === 'number'); assert(typeof gateway.totalDelegatedStake === 'number'); + assert(typeof gateway.settings === 'object'); + assert(typeof gateway.weights === 'object'); + assert(typeof gateway.weights.normalizedCompositeWeight === 'number'); + assert(typeof gateway.weights.compositeWeight === 'number'); + assert(typeof gateway.weights.stakeWeight === 'number'); + assert(typeof gateway.weights.tenureWeight === 'number'); + assert(typeof gateway.weights.observerRewardRatioWeight === 'number'); + assert(typeof gateway.weights.gatewayRewardRatioWeight === 'number'); }); }); @@ -147,6 +155,14 @@ describe('IO', async () => { assert(typeof gateway.startTimestamp === 'number'); assert(typeof gateway.operatorStake === 'number'); assert(typeof gateway.totalDelegatedStake === 'number'); + assert(typeof gateway.settings === 'object'); + assert(typeof gateway.weights === 'object'); + assert(typeof gateway.weights.normalizedCompositeWeight === 'number'); + assert(typeof gateway.weights.compositeWeight === 'number'); + assert(typeof gateway.weights.stakeWeight === 'number'); + assert(typeof gateway.weights.tenureWeight === 'number'); + assert(typeof gateway.weights.observerRewardRatioWeight === 'number'); + assert(typeof gateway.weights.gatewayRewardRatioWeight === 'number'); }); }); @@ -216,6 +232,22 @@ describe('IO', async () => { assert.ok(prescribedNames); }); + it('should return the prescribed observers for a given epoch', async () => { + const observers = await io.getPrescribedObservers(); + assert.ok(observers); + for (const observer of observers) { + assert(typeof observer.gatewayAddress === 'string'); + assert(typeof observer.observerAddress === 'string'); + assert(typeof observer.stake === 'number'); + assert(typeof observer.startTimestamp === 'number'); + assert(typeof observer.stakeWeight === 'number'); + assert(typeof observer.tenureWeight === 'number'); + assert(typeof observer.gatewayRewardRatioWeight === 'number'); + assert(typeof observer.observerRewardRatioWeight === 'number'); + assert(typeof observer.compositeWeight === 'number'); + } + }); + it('should be able to get token cost for leasing a name', async () => { const tokenCost = await io.getTokenCost({ intent: 'Buy-Record', @@ -234,3 +266,14 @@ describe('IO', async () => { assert.ok(tokenCost); }); }); + +describe('ANTRegistry', async () => { + const registry = ANTRegistry.init(); + const address = '7waR8v4STuwPnTck1zFVkQqJh5K9q9Zik4Y5-5dV7nk'; + + it('should retrieve ids from registry', async () => { + const affiliatedAnts = await registry.accessControlList({ address }); + assert(Array.isArray(affiliatedAnts.Owned)); + assert(Array.isArray(affiliatedAnts.Controlled)); + }); +}); diff --git a/tests/e2e/cjs/package.json b/tests/e2e/cjs/package.json index c6594080..693831f3 100644 --- a/tests/e2e/cjs/package.json +++ b/tests/e2e/cjs/package.json @@ -8,5 +8,6 @@ }, "dependencies": { "@ar.io/sdk": "*" - } + }, + "license": "AGPL-3.0-or-later" } diff --git a/tests/e2e/esm/index.test.js b/tests/e2e/esm/index.test.js index a4587eae..98feda2e 100644 --- a/tests/e2e/esm/index.test.js +++ b/tests/e2e/esm/index.test.js @@ -1,4 +1,4 @@ -import { IO, ioDevnetProcessId } from '@ar.io/sdk'; +import { ANTRegistry, IO, ioDevnetProcessId } from '@ar.io/sdk'; import { strict as assert } from 'node:assert'; import { describe, it } from 'node:test'; @@ -16,6 +16,11 @@ describe('IO', async () => { assert.ok(epoch); }); + it('should be able to get the total token supply', async () => { + const tokenSupply = await io.getTokenSupply(); + assert.ok(tokenSupply); + }); + it('should be able to get first set of arns records', async () => { const records = await io.getArNSRecords(); assert.ok(records); @@ -83,11 +88,6 @@ describe('IO', async () => { assert.ok(epochSettings); }); - it('should be able to get the current prescribed observers', async () => { - const observers = await io.getPrescribedObservers(); - assert.ok(observers); - }); - it('should be able to get reserved names', async () => { const reservedNames = await io.getArNSReservedNames(); assert.ok(reservedNames); @@ -119,6 +119,14 @@ describe('IO', async () => { assert(typeof gateway.startTimestamp === 'number'); assert(typeof gateway.operatorStake === 'number'); assert(typeof gateway.totalDelegatedStake === 'number'); + assert(typeof gateway.settings === 'object'); + assert(typeof gateway.weights === 'object'); + assert(typeof gateway.weights.normalizedCompositeWeight === 'number'); + assert(typeof gateway.weights.compositeWeight === 'number'); + assert(typeof gateway.weights.stakeWeight === 'number'); + assert(typeof gateway.weights.tenureWeight === 'number'); + assert(typeof gateway.weights.observerRewardRatioWeight === 'number'); + assert(typeof gateway.weights.gatewayRewardRatioWeight === 'number'); }); }); @@ -148,6 +156,14 @@ describe('IO', async () => { assert(typeof gateway.startTimestamp === 'number'); assert(typeof gateway.operatorStake === 'number'); assert(typeof gateway.totalDelegatedStake === 'number'); + assert(typeof gateway.settings === 'object'); + assert(typeof gateway.weights === 'object'); + assert(typeof gateway.weights.normalizedCompositeWeight === 'number'); + assert(typeof gateway.weights.compositeWeight === 'number'); + assert(typeof gateway.weights.stakeWeight === 'number'); + assert(typeof gateway.weights.tenureWeight === 'number'); + assert(typeof gateway.weights.observerRewardRatioWeight === 'number'); + assert(typeof gateway.weights.gatewayRewardRatioWeight === 'number'); }); }); @@ -217,6 +233,22 @@ describe('IO', async () => { assert.ok(prescribedNames); }); + it('should return the prescribed observers for a given epoch', async () => { + const observers = await io.getPrescribedObservers(); + assert.ok(observers); + for (const observer of observers) { + assert(typeof observer.gatewayAddress === 'string'); + assert(typeof observer.observerAddress === 'string'); + assert(typeof observer.stake === 'number'); + assert(typeof observer.startTimestamp === 'number'); + assert(typeof observer.stakeWeight === 'number'); + assert(typeof observer.tenureWeight === 'number'); + assert(typeof observer.gatewayRewardRatioWeight === 'number'); + assert(typeof observer.observerRewardRatioWeight === 'number'); + assert(typeof observer.compositeWeight === 'number'); + } + }); + it('should be able to get token cost for leasing a name', async () => { const tokenCost = await io.getTokenCost({ intent: 'Buy-Record', @@ -235,3 +267,14 @@ describe('IO', async () => { assert.ok(tokenCost); }); }); + +describe('ANTRegistry', async () => { + const registry = ANTRegistry.init(); + const address = '7waR8v4STuwPnTck1zFVkQqJh5K9q9Zik4Y5-5dV7nk'; + + it('should retrieve ids from registry', async () => { + const affiliatedAnts = await registry.accessControlList({ address }); + assert(Array.isArray(affiliatedAnts.Owned)); + assert(Array.isArray(affiliatedAnts.Controlled)); + }); +}); diff --git a/tests/e2e/esm/package.json b/tests/e2e/esm/package.json index 67f82db1..67645af2 100644 --- a/tests/e2e/esm/package.json +++ b/tests/e2e/esm/package.json @@ -9,5 +9,6 @@ }, "dependencies": { "@ar.io/sdk": "*" - } + }, + "license": "AGPL-3.0-or-later" } diff --git a/tests/e2e/web/src/App.test.tsx b/tests/e2e/web/src/App.test.tsx index 88267927..f93caaca 100644 --- a/tests/e2e/web/src/App.test.tsx +++ b/tests/e2e/web/src/App.test.tsx @@ -14,7 +14,7 @@ describe('ESM browser validation', () => { screen.getByTestId('load-info-result'); }, { - interval: 2000, + interval: 10000, timeout: 30000, }, ); @@ -23,4 +23,21 @@ describe('ESM browser validation', () => { // check the sdk loaded the data expect(result).toHaveTextContent('true'); }); + + it('should retrieve ids from registry', async () => { + await act(async () => render()); + + await waitFor( + () => { + screen.getByTestId('load-registry-result'); + }, + { + interval: 2000, + timeout: 30000, + }, + ); + + const result = screen.getByTestId('load-registry-result'); + expect(result).toHaveTextContent('true'); + }); }); diff --git a/tests/e2e/web/src/App.tsx b/tests/e2e/web/src/App.tsx index 2cbe28e1..005e85ce 100644 --- a/tests/e2e/web/src/App.tsx +++ b/tests/e2e/web/src/App.tsx @@ -1,4 +1,4 @@ -import { IO } from '@ar.io/sdk/web'; +import { ANTRegistry, IO } from '@ar.io/sdk/web'; import { useEffect, useState } from 'react'; import Markdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; @@ -6,32 +6,58 @@ import remarkGfm from 'remark-gfm'; import './App.css'; const io = IO.init(); +const antRegistry = ANTRegistry.init(); function App() { const [contract, setContract] = useState('Loading...'); - const [success, setSuccess] = useState(false); + const [ants, setAnts] = useState('Loading...'); + const [ioContractSuccess, setIoContractSuccess] = useState(false); + const [antRegistrySuccess, setAntRegistrySuccess] = useState(false); const [loaded, setLoaded] = useState(false); useEffect(() => { io.getInfo() - .then((state) => { + .then((state: any) => { setContract(`\`\`\`json\n${JSON.stringify(state, null, 2)}`); - setSuccess(true); + setIoContractSuccess(true); }) - .catch((error) => { + .catch((error: any) => { console.error(error); - setSuccess(false); + setIoContractSuccess(false); setContract('Error loading contract state'); }) .finally(() => { setLoaded(true); }); + antRegistry + .accessControlList({ + address: '7waR8v4STuwPnTck1zFVkQqJh5K9q9Zik4Y5-5dV7nk', + }) + .then((affiliatedAnts: { Owned: string[]; Controlled: string[] }) => { + setAnts(`\`\`\`json\n${JSON.stringify(affiliatedAnts, null, 2)}`); + setAntRegistrySuccess(true); + }) + .catch((error: any) => { + console.error(error); + setAntRegistrySuccess(false); + setAnts('Error loading affiliated ants'); + }); }, []); return (
- {loaded &&
{`${success}`}
} +
+ {loaded && ( +
{`${ioContractSuccess}`}
+ )} + + {contract} + +
+ {loaded && ( +
{`${antRegistrySuccess}`}
+ )} - {contract} + {ants}
); diff --git a/yarn.lock b/yarn.lock index 2f7d412b..b524e48f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1701,28 +1701,28 @@ dependencies: "@octokit/openapi-types" "^22.2.0" -"@permaweb/ao-scheduler-utils@~0.0.16": - version "0.0.19" - resolved "https://registry.yarnpkg.com/@permaweb/ao-scheduler-utils/-/ao-scheduler-utils-0.0.19.tgz#69d35c19583624ace3f500f53b4b4d73fca883e1" - integrity sha512-xwIe9FqQ1UZxEYWvSGJDONz0xr4vDq2Ny1NeRUiO0dKYoonShN+oI1ULgrHocKOjOPNEgRX70vMCKGLe+3x70A== +"@permaweb/ao-scheduler-utils@~0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@permaweb/ao-scheduler-utils/-/ao-scheduler-utils-0.0.20.tgz#5cee9650ef9234a01ecf3a57e8e0870f1f431076" + integrity sha512-bJkcmnQm/rCGqklJt46q5TnHfWkFzSBcSf9Z3uy8ylHRAheS9NyR1BJMAj3EXDjHCpg7JfnLRo6Uc3Xdw1lmOA== dependencies: lru-cache "^10.2.2" ramda "^0.30.0" zod "^3.23.5" -"@permaweb/aoconnect@^0.0.55": - version "0.0.55" - resolved "https://registry.yarnpkg.com/@permaweb/aoconnect/-/aoconnect-0.0.55.tgz#d856a078d3702154ac58541d09478d25ed3acf2c" - integrity sha512-W2GtLZedVseuDkCKk4CmM9SFmi0DdrMKqvhMBm9xo65z+Mzr/t1TEjMJKRNzEA2qh5IdwM43sWJ5fmbBYLg6TQ== +"@permaweb/aoconnect@^0.0.57": + version "0.0.57" + resolved "https://registry.yarnpkg.com/@permaweb/aoconnect/-/aoconnect-0.0.57.tgz#dd779563e1b994e78509251b74df64dc89ea62ea" + integrity sha512-l1+47cZuQ8pOIMOdRXymcegCmefXjqR8Bc2MY6jIzWv9old/tG6mfCue2W1QviGyhjP3zEVQgr7YofkY2lq35Q== dependencies: - "@permaweb/ao-scheduler-utils" "~0.0.16" + "@permaweb/ao-scheduler-utils" "~0.0.20" buffer "^6.0.3" - debug "^4.3.4" + debug "^4.3.5" hyper-async "^1.1.2" mnemonist "^0.39.8" - ramda "^0.29.1" + ramda "^0.30.1" warp-arbundles "^1.0.4" - zod "^3.22.4" + zod "^3.23.8" "@pkgjs/parseargs@^0.11.0": version "0.11.0" @@ -2604,6 +2604,11 @@ arconnect@^0.4.2: dependencies: arweave "^1.10.13" +arconnect@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arconnect/-/arconnect-1.0.3.tgz#8650ee6fbbcaa492cc31e3feba547dcc1706b97e" + integrity sha512-A4BTa1hl8SIbAMVUOsIiLwavmCvIMpbEFSem64klDBuBOMoIuEMUPxP5AoUfUxJZwUdL3NhNj584lCWUSGtdyA== + are-we-there-yet@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" @@ -3349,6 +3354,15 @@ cli-truncate@^4.0.0: slice-ansi "^5.0.0" string-width "^7.0.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -3732,6 +3746,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" @@ -3818,6 +3839,11 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== + diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -5339,6 +5365,11 @@ into-stream@^7.0.0: from2 "^2.3.0" p-is-promise "^3.0.0" +inversify@^5.0.5: + version "5.1.1" + resolved "https://registry.yarnpkg.com/inversify/-/inversify-5.1.1.tgz#6fbd668c591337404e005a1946bfe0d802c08730" + integrity sha512-j8grHGDzv1v+8T1sAQ+3boTCntFPfvxLCkNcxB1J8qA0lUN+fAlSyYd+RXKvaPRL4AGyPxViutBEJHNXOyUdFQ== + ip-address@^9.0.5: version "9.0.5" resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" @@ -5796,6 +5827,16 @@ jest-config@^29.7.0: slash "^3.0.0" strip-json-comments "^3.1.1" +jest-diff@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== + dependencies: + chalk "^4.0.0" + diff-sequences "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + jest-diff@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" @@ -5836,6 +5877,11 @@ jest-environment-node@^29.7.0: jest-mock "^29.7.0" jest-util "^29.7.0" +jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== + jest-get-type@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" @@ -6808,6 +6854,16 @@ map-obj@^4.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== +markdown-toc-gen@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/markdown-toc-gen/-/markdown-toc-gen-1.0.1.tgz#838c875d987445e84c86ded1cf04f6ee83a9df1a" + integrity sha512-vBa1eD9TOmru18LBiWq/s2unihvfbR4Tj5ZImwgB30/hHI1kQxYXri0vttNI8rDGm0Bl/LBkMAEX4PizO46JHg== + dependencies: + inversify "^5.0.5" + jest-diff "^27.0.2" + reflect-metadata "^0.1.13" + yargs "^16.2.0" + marked-terminal@^5.1.1: version "5.2.0" resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-5.2.0.tgz#c5370ec2bae24fb2b34e147b731c94fa933559d3" @@ -8137,6 +8193,15 @@ prettier@^3.0.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== +pretty-format@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + pretty-format@^29.0.0, pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" @@ -8283,16 +8348,16 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== -ramda@^0.29.1: - version "0.29.1" - resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.29.1.tgz#408a6165b9555b7ba2fc62555804b6c5a2eca196" - integrity sha512-OfxIeWzd4xdUNxlWhgFazxsA/nl3mS4/jGZI5n00uWOoSSFRhC1b6gl6xvmzUamgmqELraWp0J/qqVlXYPDPyA== - ramda@^0.30.0: version "0.30.0" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.30.0.tgz#3cc4f0ddddfa6334dad2f371bd72c33237d92cd0" integrity sha512-13Y0iMhIQuAm/wNGBL/9HEqIfRGmNmjKnTPlKWfA9f7dnDkr8d45wQ+S7+ZLh/Pq9PdcGxkqKUEA7ySu1QSd9Q== +ramda@^0.30.1: + version "0.30.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.30.1.tgz#7108ac95673062b060025052cd5143ae8fc605bf" + integrity sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw== + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -8318,6 +8383,11 @@ rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react-is@^18.0.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" @@ -8446,6 +8516,11 @@ redeyed@~2.1.0: dependencies: esprima "~4.0.0" +reflect-metadata@^0.1.13: + version "0.1.14" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859" + integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A== + regexp.prototype.flags@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" @@ -10023,7 +10098,7 @@ yaml@2.4.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.2.tgz#7a2b30f2243a5fc299e1f14ca58d475ed4bc5362" integrity sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA== -yargs-parser@^20.2.3: +yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== @@ -10033,6 +10108,19 @@ yargs-parser@^21.0.1, yargs-parser@^21.1.1: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@^17.0.0, yargs@^17.3.1, yargs@^17.5.1: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" @@ -10061,7 +10149,7 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== -zod@^3.22.4, zod@^3.23.5: +zod@^3.23.5, zod@^3.23.8: version "3.23.8" resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==