diff --git a/docker-compose.yaml b/docker-compose.yaml index f856b1db..322eae9b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,10 +9,16 @@ services: environment: - LOG_LEVEL=debug - PREFETCH_CONTRACTS=true - - PREFETCH_CONTRACT_IDS=_NctcA2sRy1-J4OmIQZbYFPM17piNcbdBPH2ncX2RL8 + - PREFETCH_CONTRACT_IDS=['_NctcA2sRy1-J4OmIQZbYFPM17piNcbdBPH2ncX2RL8', 'UC2zwawQoTnh0TNd9mYLQS4wObBBeaOU5LPQTNETqA4'] - BOOTSTRAP_CONTRACTS=false healthcheck: - test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck'] + test: + [ + 'CMD', + 'curl', + '-f', + 'http://localhost:3000/healthcheck' + ] interval: 10s timeout: 5s retries: 5 diff --git a/src/common.ts b/src/common.ts index 405246fd..d511655f 100644 --- a/src/common.ts +++ b/src/common.ts @@ -15,6 +15,8 @@ * along with this program. If not, see . */ import { + ANTRecord, + ANTState, ArIOState, ArNSNameData, EpochDistributionData, @@ -27,6 +29,27 @@ export type BlockHeight = number; export type SortKey = string; export type WalletAddress = string; +// TODO: append this with other configuration options (e.g. local vs. remote evaluation) +export type ContractConfiguration = + | { + contract?: SmartWeaveContract; + } + | { + contractTxId: string; + }; + +export function isContractConfiguration( + config: ContractConfiguration, +): config is { contract: SmartWeaveContract } { + return 'contract' in config; +} + +export function isContractTxIdConfiguration( + config: ContractConfiguration, +): config is { contractTxId: string } { + return 'contractTxId' in config; +} + export type EvaluationOptions = { evalTo?: { sortKey: SortKey } | { blockHeight: BlockHeight }; // TODO: any other evaluation constraints @@ -102,6 +125,18 @@ export interface ArIOContract { }: EvaluationParameters): Promise; } +export interface ANTContract { + getState({ evaluationOptions }: EvaluationParameters): Promise; + getRecord({ domain, evaluationOptions }: EvaluationParameters<{ domain: string }>): Promise; + getRecords({ evaluationOptions }: EvaluationParameters): Promise>; + getOwner({ evaluationOptions }: EvaluationParameters): Promise; + getControllers({ evaluationOptions }: EvaluationParameters): Promise; + getTicker({ evaluationOptions }: EvaluationParameters): Promise; + getName({ evaluationOptions }: EvaluationParameters): Promise; + getBalance({ address, evaluationOptions }: EvaluationParameters<{ address: string }>): Promise; + getBalances({ evaluationOptions }: EvaluationParameters): Promise>; +} + /* eslint-disable @typescript-eslint/no-explicit-any */ export interface Logger { setLogLevel: (level: string) => void; diff --git a/src/common/ant.ts b/src/common/ant.ts new file mode 100644 index 00000000..8ea7fe91 --- /dev/null +++ b/src/common/ant.ts @@ -0,0 +1,93 @@ +/** + * 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 { + ANTContract, + ANTRecord, + ANTState, + ContractConfiguration, + EvaluationOptions, + EvaluationParameters, + SmartWeaveContract, + isContractConfiguration, + isContractTxIdConfiguration, +} from '../types.js'; +import { RemoteContract } from './contracts/remote-contract.js'; + +export class ANT implements ANTContract { + private contract: SmartWeaveContract; + + constructor(config: ContractConfiguration) { + + if (isContractConfiguration(config)) { + this.contract = config.contract; + } else if (isContractTxIdConfiguration(config)) { + this.contract = new RemoteContract({ + contractTxId: config.contractTxId, + }); + } + } + + /** + * Returns the current state of the contract. + */ + async getState(params: EvaluationParameters): Promise { + const state = await this.contract.getContractState(params); + return state; + } + + async getRecord({ domain, evaluationOptions }: EvaluationParameters<{ domain: string; }>): Promise { + const records = await this.getRecords({ evaluationOptions }); + + return records[domain] + } + async getRecords({ evaluationOptions }: { evaluationOptions?: EvaluationOptions | Record | undefined; }): Promise> { + const state = await this.contract.getContractState({ evaluationOptions }); + return state.records; + } + + async getOwner({ evaluationOptions }: { evaluationOptions?: EvaluationOptions | Record | undefined; }): Promise { + const state = await this.contract.getContractState({ evaluationOptions }); + return state.owner; + } + + async getControllers({ evaluationOptions }: { evaluationOptions?: EvaluationOptions | Record | undefined; }): Promise { + const state = await this.contract.getContractState({ evaluationOptions }); + return state.controllers; + } + + async getName({ evaluationOptions }: { evaluationOptions?: EvaluationOptions | Record | undefined; }): Promise { + const state = await this.contract.getContractState({ evaluationOptions }); + return state.name; + } + + async getTicker({ evaluationOptions }: { evaluationOptions?: EvaluationOptions | Record | undefined; }): Promise { + const state = await this.contract.getContractState({ evaluationOptions }); + return state.ticker; + } + + async getBalances({ evaluationOptions }: { evaluationOptions?: EvaluationOptions | Record | undefined; }): Promise> { + const state = await this.contract.getContractState({ evaluationOptions }); + return state.balances; + } + + async getBalance({ address, evaluationOptions }: EvaluationParameters<{ address: string; }>): Promise { + const balances = await this.getBalances({ evaluationOptions }); + return balances[address]; + } + + +} diff --git a/src/common/ar-io.ts b/src/common/ar-io.ts index a7dbb9ac..1bd4f575 100644 --- a/src/common/ar-io.ts +++ b/src/common/ar-io.ts @@ -19,35 +19,18 @@ import { ArIOContract, ArIOState, ArNSNameData, + ContractConfiguration, EpochDistributionData, EvaluationParameters, Gateway, Observations, SmartWeaveContract, WeightedObserver, + isContractConfiguration, + isContractTxIdConfiguration, } from '../types.js'; import { RemoteContract } from './contracts/remote-contract.js'; -// TODO: append this with other configuration options (e.g. local vs. remote evaluation) -export type ContractConfiguration = - | { - contract?: SmartWeaveContract; - } - | { - contractTxId: string; - }; - -function isContractConfiguration( - config: ContractConfiguration, -): config is { contract: SmartWeaveContract } { - return 'contract' in config; -} - -function isContractTxIdConfiguration( - config: ContractConfiguration, -): config is { contractTxId: string } { - return 'contractTxId' in config; -} export class ArIO implements ArIOContract { private contract: SmartWeaveContract; diff --git a/src/contract-state.ts b/src/contract-state.ts index 6dbece4b..8b15b55c 100644 --- a/src/contract-state.ts +++ b/src/contract-state.ts @@ -181,3 +181,20 @@ export interface ArIOState { vaults: RegistryVaults; prescribedObservers: PrescribedObservers; } + + +// ANT + +export type ANTRecord = { + transactionId: string; + ttlSeconds: number; +}; + +export type ANTState = { + owner: WalletAddress; + controllers: WalletAddress[]; + name: string; + ticker: string; + records: Record; + balances: Balances; +}; \ No newline at end of file diff --git a/tests/ant.test.ts b/tests/ant.test.ts new file mode 100644 index 00000000..c4e2d2a9 --- /dev/null +++ b/tests/ant.test.ts @@ -0,0 +1,64 @@ +import { ANT } from '../src/common/ant'; +import { RemoteContract } from '../src/common/contracts/remote-contract'; +import { ANTState } from '../src/contract-state'; + + +describe('ANT contract apis', () => { + + const ant = new ANT({ + contract: new RemoteContract({ + url: process.env.REMOTE_CACHE_URL || 'http://localhost:3000', + contractTxId: "UC2zwawQoTnh0TNd9mYLQS4wObBBeaOU5LPQTNETqA4", + }), + }); + + const sortKey = "000001383961,0000000000000,13987aba2d71b6229989690c15d2838a4deef0a90c3fc9e4d7227ed17e35d0bd"; + const blockHeight = 1383961; + + it.each([[{ sortKey }], [{ blockHeight }]])(`should get contract state with evaluation options: ${JSON.stringify('%s')}`, async (evalTo) => { + const state = await ant.getState({ evaluationOptions: { evalTo } }); + expect(state).toBeDefined(); + }); + + it.each([[{ sortKey }], [{ blockHeight }]])(`should get record: ${JSON.stringify('%s')}`, async (evalTo) => { + const record = await ant.getRecord({ domain: "@", evaluationOptions: { evalTo } }); + expect(record).toBeDefined(); + }); + + it.each([[{ sortKey }], [{ blockHeight }]])(`should get records with evaluation options: ${JSON.stringify('%s')}`, async (evalTo) => { + const records = await ant.getRecords({ evaluationOptions: { evalTo } }); + console.dir({ records: records['@'] }, { depth: 4 }) + expect(records).toBeDefined(); + }); + + it.each([[{ sortKey }], [{ blockHeight }]])(`should get owner with evaluation options: ${JSON.stringify('%s')}`, async (evalTo) => { + const owner = await ant.getOwner({ evaluationOptions: { evalTo } }); + expect(owner).toBeDefined(); + }); + + it.each([[{ sortKey }], [{ blockHeight }]])(`should get controllers with evaluation options: ${JSON.stringify('%s')}`, async (evalTo) => { + const controllers = await ant.getControllers({ evaluationOptions: { evalTo } }); + expect(controllers).toBeDefined(); + }); + + it.each([[{ sortKey }], [{ blockHeight }]])(`should get name with evaluation options: ${JSON.stringify('%s')}`, async (evalTo) => { + const state = await ant.getName({ evaluationOptions: { evalTo } }); + expect(state).toBeDefined(); + }); + + it.each([[{ sortKey }], [{ blockHeight }]])(`should get ticker with evaluation options: ${JSON.stringify('%s')}`, async (evalTo) => { + const state = await ant.getTicker({ evaluationOptions: { evalTo } }); + expect(state).toBeDefined(); + }); + + it.each([[{ sortKey }], [{ blockHeight }]])(`should get balances with evaluation options: ${JSON.stringify('%s')}`, async (evalTo) => { + const state = await ant.getBalances({ evaluationOptions: { evalTo } }); + expect(state).toBeDefined(); + }); + + it.each([[{ sortKey }], [{ blockHeight }]])(`should get balance with evaluation options: ${JSON.stringify('%s')}`, async (evalTo) => { + const state = await ant.getBalance({ address: "TRVCopHzzO1VSwRUUS8umkiO2MpAL53XtVGlLaJuI94", evaluationOptions: { evalTo } }); + expect(state).toBeDefined(); + }); + +}) \ No newline at end of file