diff --git a/src/common/caches/arns-remote-cache.ts b/src/common/caches/arns-remote-cache.ts index f74fe625..ae5cead8 100644 --- a/src/common/caches/arns-remote-cache.ts +++ b/src/common/caches/arns-remote-cache.ts @@ -21,6 +21,7 @@ import { ArNSStateResponse, Gateway, HTTPClient, + ReadInteractionFilters, } from '../../types/index.js'; import { NotFound } from '../error.js'; import { AxiosHTTPService } from '../http.js'; @@ -58,32 +59,44 @@ export class ArNSRemoteCache implements ArIOContract { } } - async getGateway({ address }: { address: string }) { + async getGateway({ + address, + blockHeight, + sortKey, + }: { address: string } & ReadInteractionFilters) { this.logger.debug(`Fetching gateway ${address}`); - const gateway = await this.getGateways().then((gateways) => { - if (gateways[address] === undefined) { - throw new NotFound(`Gateway not found: ${address}`); - } - return gateways[address]; - }); + const gateway = await this.getGateways({ blockHeight, sortKey }).then( + (gateways) => { + if (gateways[address] === undefined) { + throw new NotFound(`Gateway not found: ${address}`); + } + return gateways[address]; + }, + ); return gateway; } - async getGateways() { + async getGateways({ blockHeight, sortKey }: ReadInteractionFilters = {}) { this.logger.debug(`Fetching gateways`); const { result } = await this.http.get< ArNSStateResponse<'result', Record> >({ endpoint: `/contract/${this.contractTxId.toString()}/read/gateways`, + params: { blockHeight, sortKey }, }); return result; } - async getBalance({ address }: { address: string }) { + async getBalance({ + address, + blockHeight, + sortKey, + }: { address: string } & ReadInteractionFilters) { this.logger.debug(`Fetching balance for ${address}`); const { result } = await this.http .get>({ endpoint: `/contract/${this.contractTxId.toString()}/state/balances/${address}`, + params: { blockHeight, sortKey }, }) .catch((e) => { if (e instanceof NotFound) { @@ -94,32 +107,42 @@ export class ArNSRemoteCache implements ArIOContract { return result; } - async getBalances() { + async getBalances({ blockHeight, sortKey }: ReadInteractionFilters = {}) { this.logger.debug(`Fetching balances`); const { result } = await this.http.get< ArNSStateResponse<'result', Record> >({ endpoint: `/contract/${this.contractTxId.toString()}/state/balances`, + params: { blockHeight, sortKey }, }); return result; } - async getRecord({ domain }: { domain: string }): Promise { + async getRecord({ + domain, + blockHeight, + sortKey, + }: { domain: string } & ReadInteractionFilters): Promise { this.logger.debug(`Fetching record for ${domain}`); const { result } = await this.http.get< ArNSStateResponse<'result', ArNSNameData> >({ endpoint: `/contract/${this.contractTxId.toString()}/state/records/${domain}`, + params: { blockHeight, sortKey }, }); return result; } - async getRecords(): Promise> { + async getRecords({ + blockHeight, + sortKey, + }: ReadInteractionFilters = {}): Promise> { this.logger.debug(`Fetching all records`); const { result } = await this.http.get< ArNSStateResponse<'result', Record> >({ endpoint: `/contract/${this.contractTxId.toString()}/state/records`, + params: { blockHeight, sortKey }, }); return result; } diff --git a/src/common/http.ts b/src/common/http.ts index d9751152..ebbf4b6c 100644 --- a/src/common/http.ts +++ b/src/common/http.ts @@ -39,16 +39,19 @@ export class AxiosHTTPService implements HTTPClient { signal, allowedStatuses = [200, 202], headers, + params, }: { endpoint: string; signal?: AbortSignal; allowedStatuses?: number[]; headers?: Record; + params?: Record; // eslint-disable-line @typescript-eslint/no-explicit-any }): Promise { this.logger.debug(`Get request to endpoint: ${endpoint}`); const { status, statusText, data } = await this.axios.get(endpoint, { headers, signal, + params, }); if (!allowedStatuses.includes(status)) { diff --git a/src/constants.ts b/src/constants.ts index 3e49f6c0..54bfdf75 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,6 +16,10 @@ */ export const ARWEAVE_TX_REGEX = new RegExp('^[a-zA-Z0-9_-]{43}$'); +// sortkey: padded blockheight to 12, JS timestamp, hash of transactionID + block hash. Timestamp only applicable to L2 and normally is all zeros. +export const SORT_KEY_REGEX = new RegExp( + '^[0-9]{12},[0-9]{13},[a-fA-F0-9]{64}$', +); export const ARNS_TESTNET_REGISTRY_TX = process.env.ARNS_REGISTRY_TX ?? 'bLAgYxAdX2Ry-nt6aH2ixgvJXbpsEYm28NgJgyqfs-U'; diff --git a/src/types/common.ts b/src/types/common.ts index 76ec92b2..7621a151 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -16,13 +16,29 @@ */ import { ArNSNameData, Gateway } from './contract-state.js'; +export type EvaluationFilters = { + blockHeight?: number; + sortKey?: string; // should be tested against regex for validity +}; + +// TODO: extend type with other read filters (e.g max eval time) +export type ReadInteractionFilters = EvaluationFilters; + // TODO: extend with additional methods export interface ArIOContract { - getGateway({ address }: { address: WalletAddress }): Promise; + getGateway( + props: { address: WalletAddress } & ReadInteractionFilters, + ): Promise; getGateways(): Promise>; - getBalance({ address }: { address: WalletAddress }): Promise; - getBalances(): Promise>; - getRecord({ domain }: { domain: string }): Promise; + getBalance( + props: { address: WalletAddress } & ReadInteractionFilters, + ): Promise; + getBalances( + props: ReadInteractionFilters, + ): Promise>; + getRecord( + props: { domain: string } & ReadInteractionFilters, + ): Promise; getRecords(): Promise>; } @@ -45,11 +61,13 @@ export interface HTTPClient { signal, headers, allowedStatuses, + params, }: { endpoint: string; signal?: AbortSignal; headers?: Record; allowedStatuses?: number[]; + params?: Record; // eslint-disable-line @typescript-eslint/no-explicit-any }): Promise; // TODO: add post method // post({ diff --git a/tests/arns-remote-cache.test.ts b/tests/arns-remote-cache.test.ts deleted file mode 100644 index 610be2a8..00000000 --- a/tests/arns-remote-cache.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { ArNSRemoteCache } from '../src/common/caches/arns-remote-cache.js'; -import { NotFound } from '../src/common/error.js'; - -describe('ArNSRemoteCache', () => { - const remoteCacheProvider = new ArNSRemoteCache({}); - - // gateway tests - it('should be able to fetch gateways', async () => { - const gateways = await remoteCacheProvider.getGateways(); - expect(gateways).toBeDefined(); - }); - - it('should should throw NotFound error on non existent gateway', async () => { - const error = await remoteCacheProvider - .getGateway({ - address: 'some-address', - }) - .catch((e) => e); - expect(error).toBeInstanceOf(NotFound); - }); - - // balance tests - it('should fetch a balance', async () => { - const balance = await remoteCacheProvider.getBalance({ - address: 'some-address', - }); - expect(balance).toEqual(0); - }); - - it('should fetch all balances', async () => { - const balances = await remoteCacheProvider.getBalances(); - expect(balances).toBeDefined(); - }); - - // records tests - it('should fetch a record', async () => { - const record = await remoteCacheProvider.getRecord({ - domain: 'ar-io', - }); - expect(record).toBeDefined(); - }); - - it('should throw NotFound error on non existent record', async () => { - const error = await remoteCacheProvider - .getRecord({ - domain: 'some-domain', - }) - .catch((e) => e); - expect(error).toBeInstanceOf(NotFound); - }); - - it('should fetch all records', async () => { - const records = await remoteCacheProvider.getRecords(); - - expect(records).toBeDefined(); - }); -}); diff --git a/tests/arns-remote-cache/balances.test.ts b/tests/arns-remote-cache/balances.test.ts new file mode 100644 index 00000000..2c311b11 --- /dev/null +++ b/tests/arns-remote-cache/balances.test.ts @@ -0,0 +1,73 @@ +import { ArNSRemoteCache } from '../../src/common/caches/arns-remote-cache.js'; + +describe('ArNSRemoteCache ~ BALANCES', () => { + const remoteCacheProvider = new ArNSRemoteCache({}); + + // balance tests + it('should fetch a balance', async () => { + const balance = await remoteCacheProvider.getBalance({ + address: 'some-address', + }); + expect(balance).toEqual(0); + }); + + it('should fetch all balances', async () => { + const balances = await remoteCacheProvider.getBalances(); + expect(balances).toBeDefined(); + }); + + it('should return balance at a given block height', async () => { + const address = '7waR8v4STuwPnTck1zFVkQqJh5K9q9Zik4Y5-5dV7nk'; + const currentBalance = 2_363_250; + const transferAmount = 1000; + const transferBlockHeight = 1305612; + const balance = await remoteCacheProvider.getBalance({ + address, + blockHeight: transferBlockHeight, + }); + expect(balance).toEqual(currentBalance); + + const previousBalance = await remoteCacheProvider.getBalance({ + address, + blockHeight: transferBlockHeight - 1, + }); + expect(previousBalance).toEqual(currentBalance + transferAmount); + }); + + it('should return balance at a given sort key', async () => { + const address = '7waR8v4STuwPnTck1zFVkQqJh5K9q9Zik4Y5-5dV7nk'; + const balanceSortKey = + '000001305612,0000000000000,6806919fa401ad27fd86db576ef578857bd22a11d6905324d643368069146d4e'; + const balance = await remoteCacheProvider.getBalance({ + address, + sortKey: balanceSortKey, + }); + expect(balance).toEqual(2363250); + }); + + it('should return balances at a given block height', async () => { + const address = '7waR8v4STuwPnTck1zFVkQqJh5K9q9Zik4Y5-5dV7nk'; + const currentBalance = 2363250; + const transferAmount = 1000; + const transferBlockHeight = 1305612; + const balances = await remoteCacheProvider.getBalances({ + blockHeight: transferBlockHeight, + }); + expect(balances[address]).toEqual(currentBalance); + + const previousBalances = await remoteCacheProvider.getBalances({ + blockHeight: transferBlockHeight - 1, + }); + expect(previousBalances[address]).toEqual(currentBalance + transferAmount); + }); + + it('should return balances at a given sort key', async () => { + const address = '7waR8v4STuwPnTck1zFVkQqJh5K9q9Zik4Y5-5dV7nk'; + const balanceSortKey = + '000001305612,0000000000000,6806919fa401ad27fd86db576ef578857bd22a11d6905324d643368069146d4e'; + const balances = await remoteCacheProvider.getBalances({ + sortKey: balanceSortKey, + }); + expect(balances[address]).toEqual(2363250); + }); +}); diff --git a/tests/arns-remote-cache/gateways.test.ts b/tests/arns-remote-cache/gateways.test.ts new file mode 100644 index 00000000..eff2afc6 --- /dev/null +++ b/tests/arns-remote-cache/gateways.test.ts @@ -0,0 +1,68 @@ +import { ArNSRemoteCache } from '../../src/common/caches/arns-remote-cache.js'; +import { NotFound } from '../../src/common/error.js'; + +describe('ArNSRemoteCache ~ GATEWAYS', () => { + const remoteCacheProvider = new ArNSRemoteCache({}); + + // gateway tests + it('should be able to fetch gateways', async () => { + const gateways = await remoteCacheProvider.getGateways(); + expect(gateways).toBeDefined(); + }); + + it('should should throw NotFound error on non existent gateway', async () => { + const error = await remoteCacheProvider + .getGateway({ + address: 'some-address', + }) + .catch((e) => e); + expect(error).toBeInstanceOf(NotFound); + }); + + it('should return gateway state at a given block height', async () => { + const blockHeight = 1372179; + const address = 'usOg4jFzqinXK_ExoU5NijjEyggNA255998LNiM8Vtc'; + const gateway = await remoteCacheProvider.getGateway({ + address, + blockHeight, + }); + expect(gateway).toBeDefined(); + + const previousGatewayState = await remoteCacheProvider + .getGateway({ + address, + blockHeight: blockHeight - 1, + }) + .catch((e) => e); + expect(previousGatewayState).toBeInstanceOf(NotFound); + }); + + it('should return gateway state at a given sort key', async () => { + const sortKey = + '000001372179,0000000000000,1babf113056ce4d158c06f17ac8a1d0bff603dd6218dad98381d8e6d295f50a5'; + const address = 'usOg4jFzqinXK_ExoU5NijjEyggNA255998LNiM8Vtc'; + const gateway = await remoteCacheProvider.getGateway({ + address, + sortKey, + }); + expect(gateway).toBeDefined(); + }); + + it('should return gateways state at a given block height', async () => { + const blockHeight = 1372179; + const address = 'usOg4jFzqinXK_ExoU5NijjEyggNA255998LNiM8Vtc'; + const gateways = await remoteCacheProvider.getGateways({ + blockHeight, + }); + expect(gateways[address]).toBeDefined(); + }); + + it('should return gateways state at a given sort key', async () => { + const address = 'usOg4jFzqinXK_ExoU5NijjEyggNA255998LNiM8Vtc'; + const gateways = await remoteCacheProvider.getGateways({ + sortKey: + '000001372179,0000000000000,1babf113056ce4d158c06f17ac8a1d0bff603dd6218dad98381d8e6d295f50a5', + }); + expect(gateways[address]).toBeDefined(); + }); +}); diff --git a/tests/arns-remote-cache/records.test.ts b/tests/arns-remote-cache/records.test.ts new file mode 100644 index 00000000..56fa5d3a --- /dev/null +++ b/tests/arns-remote-cache/records.test.ts @@ -0,0 +1,78 @@ +import { ArNSRemoteCache } from '../../src/common/caches/arns-remote-cache.js'; +import { NotFound } from '../../src/common/error.js'; + +describe('ArNSRemoteCache ~ RECORDS', () => { + const remoteCacheProvider = new ArNSRemoteCache({}); + // records tests + it('should fetch a record', async () => { + const record = await remoteCacheProvider.getRecord({ + domain: 'ar-io', + }); + expect(record).toBeDefined(); + }); + + it('should throw NotFound error on non existent record', async () => { + const error = await remoteCacheProvider + .getRecord({ + domain: 'some-domain', + }) + .catch((e) => e); + expect(error).toBeInstanceOf(NotFound); + }); + + it('should fetch all records', async () => { + const records = await remoteCacheProvider.getRecords(); + + expect(records).toBeDefined(); + }); + + it('should return record at a given block height', async () => { + const domain = 'raiman'; + const registrationBlockHeight = 1372652; + const currentRecord = await remoteCacheProvider.getRecord({ + domain, + blockHeight: registrationBlockHeight, + }); + expect(currentRecord).toBeDefined(); + + const error = await remoteCacheProvider + .getRecord({ domain, blockHeight: registrationBlockHeight - 1 }) + .catch((e) => e); + expect(error).toBeInstanceOf(NotFound); + }); + + it('should return record at a given sort key', async () => { + const domain = 'raiman'; + const registrationSortKey = + '000001372652,0000000000000,7c697ffe5ffdad0f554dbd4fe8aa4ac997ea58d34ff9bf54178ab894d47e41e8'; + const record = await remoteCacheProvider.getRecord({ + domain, + sortKey: registrationSortKey, + }); + expect(record).toBeDefined(); + }); + + it('should return records at a given block height', async () => { + const domain = 'raiman'; + const registrationBlockHeight = 1372652; + const currentRecords = await remoteCacheProvider.getRecords({ + blockHeight: registrationBlockHeight, + }); + expect(currentRecords[domain]).toBeDefined(); + + const previousRecords = await remoteCacheProvider.getRecords({ + blockHeight: registrationBlockHeight - 1, + }); + expect(previousRecords[domain]).not.toBeDefined(); + }); + + it('should return records at a given sort key', async () => { + const domain = 'raiman'; + const registrationSortKey = + '000001372652,0000000000000,7c697ffe5ffdad0f554dbd4fe8aa4ac997ea58d34ff9bf54178ab894d47e41e8'; + const records = await remoteCacheProvider.getRecords({ + sortKey: registrationSortKey, + }); + expect(records[domain]).toBeDefined(); + }); +});