Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(PE-5742): add records api to arns remote cache #8

Merged
merged 12 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,24 @@ import { ArIO } from '@ar-io/sdk';

const arIO = new ArIO({});
const address = 'QGWqtJdLLgm2ehFWiiPzMaoFLD50CnGuzZIPEdoDRGQ';
const domain = 'ardrive';
const contractTxIds = [
'I-cxQhfh0Zb9UqQNizC9PiLC41KpUeA9hjiVV02rQRw',
'DGWp8_6c1YywKgCgBFhncMglciQyCdfX1swil4qjNSc',
];
// testnet
const testnetBalance = await arIO.testnet.getBalance({ address });
const testnetGateway = await arIO.testnet.getGateway({ address });
const testnetRecord = await arIO.testnet.getRecord({ domain });
const testnetRecords = await arIO.testnet.getRecords({ contractTxIds });
const allTestnetRecords = await arIO.testnet.getRecords({});

// mainnet
const balance = await arIO.mainnet.getBalance({ address });
const gateway = await arIO.mainnet.getGateway({ address });
const record = await arIO.mainnet.getRecord({ domain });
const records = await arIO.mainnet.getRecords({ contractTxIds });
const allRecords = await arIO.mainnet.getRecords({});
dtfiedler marked this conversation as resolved.
Show resolved Hide resolved
atticusofsparta marked this conversation as resolved.
Show resolved Hide resolved
```

## Usage
Expand Down Expand Up @@ -94,12 +106,14 @@ Types are exported from `./lib/types/[node/web]/index.d.ts` and should be automa

The contract that the following methods retrieve data from are determined by the `testnet` or `devnet` clients - see examples above for implementation details.

| Method Name | Description |
| ------------------------- | ----------------------------------------------- |
| `getBalance({ address })` | Retrieves the balance of the specified address. |
| `getBalances()` | Retrieves all balances on the ArIO contract. |
| `getGateway({ address })` | Retrieves the specified gateway by address. |
| `getGateways()` | Retrieves all gateways. |
| Method Name | Description |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `getBalance({ address })` | Retrieves the balance of the specified address. |
| `getBalances()` | Retrieves all balances on the ArIO contract. |
| `getGateway({ address })` | Retrieves the specified gateway by address. |
| `getGateways()` | Retrieves all gateways. |
| `getRecord({ domain })` | Retrieves a specified ArNS record by the domain name. |
| `getRecords({ contractTxIds })` | Retrieves all records with an optional `contractTxIds` filter to only get records associated with specified ANT contracts. |

## Developers

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"dependencies": {
"arweave": "^1.14.4",
"axios": "1.4.0",
"lodash": "^4.17.21",
dtfiedler marked this conversation as resolved.
Show resolved Hide resolved
"warp-contracts": "^1.4.34",
"winston": "^3.11.0"
}
Expand Down
86 changes: 66 additions & 20 deletions src/common/caches/arns-remote-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { chunk } from 'lodash';

import { ARNS_TESTNET_REGISTRY_TX } from '../../constants.js';
import {
ArIOContract,
ArNSNameData,
ArNSStateResponse,
ContractCache,
Gateway,
Expand All @@ -26,6 +29,14 @@ import { NotFound } from '../error.js';
import { AxiosHTTPService } from '../http.js';
import { DefaultLogger } from '../logger.js';

const validateContractTxId = (contractTxId: string) => {
if (!contractTxId) {
throw new Error(
'Contract TxId not set, set one before calling this function.',
);
}
};

export class ArNSRemoteCache implements ContractCache, ArIOContract {
private contractTxId: string;
private logger: DefaultLogger;
Expand Down Expand Up @@ -55,11 +66,8 @@ export class ArNSRemoteCache implements ContractCache, ArIOContract {
}

async getGateway({ address }: { address: string }) {
if (!this.contractTxId) {
throw new Error(
'Contract TxId not set, set one before calling this function.',
);
}
validateContractTxId(this.contractTxId);
dtfiedler marked this conversation as resolved.
Show resolved Hide resolved

this.logger.debug(`Fetching gateway ${address}`);
const gateway = await this.getGateways().then((gateways) => {
if (gateways[address] === undefined) {
Expand All @@ -71,11 +79,8 @@ export class ArNSRemoteCache implements ContractCache, ArIOContract {
}

async getGateways() {
if (!this.contractTxId) {
throw new Error(
'Contract TxId not set, set one before calling this function.',
);
}
validateContractTxId(this.contractTxId);

this.logger.debug(`Fetching gateways`);
const { result } = await this.http.get<
ArNSStateResponse<'result', Record<string, Gateway>>
Expand All @@ -86,11 +91,8 @@ export class ArNSRemoteCache implements ContractCache, ArIOContract {
}

async getBalance({ address }: { address: string }) {
if (!this.contractTxId) {
throw new Error(
'Contract TxId not set, set one before calling this function.',
);
}
validateContractTxId(this.contractTxId);

this.logger.debug(`Fetching balance for ${address}`);
const { result } = await this.http
.get<ArNSStateResponse<'result', number>>({
Expand All @@ -106,11 +108,8 @@ export class ArNSRemoteCache implements ContractCache, ArIOContract {
}

async getBalances() {
if (!this.contractTxId) {
throw new Error(
'Contract TxId not set, set one before calling this function.',
);
}
validateContractTxId(this.contractTxId);

this.logger.debug(`Fetching balances`);
const { result } = await this.http.get<
ArNSStateResponse<'result', Record<string, number>>
Expand All @@ -119,4 +118,51 @@ export class ArNSRemoteCache implements ContractCache, ArIOContract {
});
return result;
}

async getRecord({ domain }: { domain: string }): Promise<ArNSNameData> {
validateContractTxId(this.contractTxId);

this.logger.debug(`Fetching record for ${domain}`);
const { record } = await this.http.get<
ArNSStateResponse<'record', ArNSNameData>
>({
endpoint: `/contract/${this.contractTxId.toString()}/records/${domain}`,
});
return record;
}

async getRecords({
contractTxIds,
}: {
contractTxIds?: string[];
}): Promise<Record<string, ArNSNameData>> {
validateContractTxId(this.contractTxId);

const contractTxIdsSet = new Set(contractTxIds);
const batches = chunk([...contractTxIdsSet]);

this.logger.debug(`Fetching records for ${contractTxIdsSet.size} ANT's`);
const records = await Promise.all(
batches.map((batch: string[]) =>
this.http.get<
ArNSStateResponse<'records', Record<string, ArNSNameData>>
>({
endpoint: `/contract/${this.contractTxId.toString()}/records${new URLSearchParams(batch.map((id) => ['contractTxId', id.toString()])).toString()}`,
}),
),
).then(
(
responses: ArNSStateResponse<'records', Record<string, ArNSNameData>>[],
) => {
const recordsObj = responses.reduce(
(acc: Record<string, ArNSNameData>, response) => {
return { ...acc, ...response.records };
},
{},
);
return recordsObj;
},
);
return records;
}
}
8 changes: 7 additions & 1 deletion src/types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Gateway } from './contract-state.js';
import { ArNSNameData, Gateway } from './contract-state.js';

export interface ContractCache {
/**
Expand All @@ -28,6 +28,12 @@ export interface ArIOContract {
getGateways(): Promise<Record<WalletAddress, Gateway>>;
getBalance({ address }: { address: WalletAddress }): Promise<number>;
getBalances(): Promise<Record<WalletAddress, number>>;
getRecord({ domain }: { domain: string }): Promise<ArNSNameData>;
getRecords({
contractTxIds,
dtfiedler marked this conversation as resolved.
Show resolved Hide resolved
}: {
contractTxIds?: string[];
}): Promise<Record<string, ArNSNameData>>;
}

/* eslint-disable @typescript-eslint/no-explicit-any */
Expand Down
42 changes: 42 additions & 0 deletions tests/arns-remote-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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();
Expand All @@ -18,6 +19,7 @@ describe('ArNSRemoteCache', () => {
expect(error).toBeInstanceOf(NotFound);
});

// balance tests
it('should fetch a balance', async () => {
const balance = await remoteCacheProvider.getBalance({
address: 'some-address',
Expand All @@ -29,4 +31,44 @@ describe('ArNSRemoteCache', () => {
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 records for list of contractIDs', async () => {
const allRecords = await remoteCacheProvider
.getRecords({})
.then((res) => Object.entries(res).slice(200))
.catch((e) => e); // deliberately attempting to get more than URL params can handle to test batching but limiting to 200 to not strain service

const contractTxIds = allRecords.map(([, record]) => record.contractTxId); // deliberately attempting to get more than URL params can handle to test batching but limiting to 200 to not strain service

const expectedRecords = allRecords
.map(([domain]) => domain)
.sort((a: string, b: string) => a.localeCompare(b));
const records = await remoteCacheProvider.getRecords({
contractTxIds: [...contractTxIds, ...contractTxIds], // mapping twice to test duplicates
});

const actualRecords = Object.keys(records).sort((a: string, b: string) =>
a.localeCompare(b),
);

expect(records).toBeDefined();
expect(actualRecords).toEqual(expectedRecords);
});
});