From f094f153f23e193a9b6c6e09ea37f5d50e7375d2 Mon Sep 17 00:00:00 2001 From: atticusofsparta Date: Thu, 29 Feb 2024 12:44:43 -0600 Subject: [PATCH] chore(tests): add tests for blockheight and sortkey, break tests into domain centric files --- src/common/caches/arns-remote-cache.ts | 47 ++++++++++---- src/common/http.ts | 3 + src/constants.ts | 4 ++ src/types/common.ts | 27 ++++++-- src/utils/index.ts | 1 + src/utils/smartweave/evaluation.test.ts | 13 ++++ src/utils/smartweave/evaluation.ts | 49 ++++++++++++++ src/utils/smartweave/index.ts | 17 +++++ tests/arns-remote-cache.test.ts | 57 ----------------- tests/arns-remote-cache/balances.test.ts | 76 ++++++++++++++++++++++ tests/arns-remote-cache/gateways.test.ts | 72 +++++++++++++++++++++ tests/arns-remote-cache/records.test.ts | 81 ++++++++++++++++++++++++ 12 files changed, 374 insertions(+), 73 deletions(-) create mode 100644 src/utils/smartweave/evaluation.test.ts create mode 100644 src/utils/smartweave/evaluation.ts create mode 100644 src/utils/smartweave/index.ts delete mode 100644 tests/arns-remote-cache.test.ts create mode 100644 tests/arns-remote-cache/balances.test.ts create mode 100644 tests/arns-remote-cache/gateways.test.ts create mode 100644 tests/arns-remote-cache/records.test.ts diff --git a/src/common/caches/arns-remote-cache.ts b/src/common/caches/arns-remote-cache.ts index f74fe625..ab83394e 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: sortKey?.toString() }, }); 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: sortKey?.toString() }, }) .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: sortKey?.toString() }, }); 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: sortKey?.toString() }, }); 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: sortKey?.toString() }, }); 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..7afd0c64 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -14,15 +14,32 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import { SmartWeaveSortKey } from '../utils/index.js'; import { ArNSNameData, Gateway } from './contract-state.js'; +export type EvaluationFilters = { + blockHeight?: number; + sortKey?: SmartWeaveSortKey; // 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 +62,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/src/utils/index.ts b/src/utils/index.ts index 4d2742ca..521e4f01 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -16,3 +16,4 @@ */ export * from './arweave.js'; export * from './http-client.js'; +export * from './smartweave/index.js'; diff --git a/src/utils/smartweave/evaluation.test.ts b/src/utils/smartweave/evaluation.test.ts new file mode 100644 index 00000000..dd8de782 --- /dev/null +++ b/src/utils/smartweave/evaluation.test.ts @@ -0,0 +1,13 @@ +import { SmartWeaveSortKey } from './evaluation.js'; + +describe(`Smartweave eval utils`, () => { + it(`should throw on a bad sort key`, async () => { + const sortKey = '123,456,abc'; + const error = await (async () => new SmartWeaveSortKey(sortKey))().catch( + (e) => e, + ); + + expect(error).toBeInstanceOf(Error); + expect(error.message).toContain(sortKey); + }); +}); diff --git a/src/utils/smartweave/evaluation.ts b/src/utils/smartweave/evaluation.ts new file mode 100644 index 00000000..6cc902f6 --- /dev/null +++ b/src/utils/smartweave/evaluation.ts @@ -0,0 +1,49 @@ +/** + * 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 { SORT_KEY_REGEX } from '../../constants.js'; + +export class SmartWeaveSortKey { + private _sortKey: string; + constructor(sortKey: string) { + if (!SmartWeaveSortKey.validate(sortKey)) { + throw new Error(`Invalid sort key: ${sortKey}`); + } + + this._sortKey = sortKey; + } + + static validate(sortKey: string): boolean { + return SORT_KEY_REGEX.test(sortKey); + } + + toString(): string { + return this._sortKey; + } + + parts(): string[] { + return this._sortKey.split(','); + } + blockHeight(): number { + return parseInt(this.parts()[0]); + } + timestamp(): number { + return parseInt(this.parts()[1]); + } + hash(): string { + return this.parts()[2]; + } +} diff --git a/src/utils/smartweave/index.ts b/src/utils/smartweave/index.ts new file mode 100644 index 00000000..c0451be1 --- /dev/null +++ b/src/utils/smartweave/index.ts @@ -0,0 +1,17 @@ +/** + * 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 . + */ +export * from './evaluation.js'; 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..cbcd3884 --- /dev/null +++ b/tests/arns-remote-cache/balances.test.ts @@ -0,0 +1,76 @@ +import { ArNSRemoteCache } from '../../src/common/caches/arns-remote-cache.js'; +import { SmartWeaveSortKey } from '../../src/utils/index.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 = new SmartWeaveSortKey( + '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 = new SmartWeaveSortKey( + '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..04ba6979 --- /dev/null +++ b/tests/arns-remote-cache/gateways.test.ts @@ -0,0 +1,72 @@ +import { ArNSRemoteCache } from '../../src/common/caches/arns-remote-cache.js'; +import { NotFound } from '../../src/common/error.js'; +import { SmartWeaveSortKey } from '../../src/utils/index.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 = new SmartWeaveSortKey( + '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 sortKey = new SmartWeaveSortKey( + '000001372179,0000000000000,1babf113056ce4d158c06f17ac8a1d0bff603dd6218dad98381d8e6d295f50a5', + ); + const gateways = await remoteCacheProvider.getGateways({ + sortKey, + }); + 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..22955acb --- /dev/null +++ b/tests/arns-remote-cache/records.test.ts @@ -0,0 +1,81 @@ +import { ArNSRemoteCache } from '../../src/common/caches/arns-remote-cache.js'; +import { NotFound } from '../../src/common/error.js'; +import { SmartWeaveSortKey } from '../../src/utils/index.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 = new SmartWeaveSortKey( + '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 = new SmartWeaveSortKey( + '000001372652,0000000000000,7c697ffe5ffdad0f554dbd4fe8aa4ac997ea58d34ff9bf54178ab894d47e41e8', + ); + const records = await remoteCacheProvider.getRecords({ + sortKey: registrationSortKey, + }); + expect(records[domain]).toBeDefined(); + }); +});