Skip to content

Commit

Permalink
feat(PE-5751): add blockheight and sortkey eval filters (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
dtfiedler committed Mar 8, 2024
2 parents 885ce68 + 56ebb08 commit 832a1ad
Show file tree
Hide file tree
Showing 30 changed files with 671 additions and 897 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
matrix:
node_version: [18.x, 20.x]
command: ['lint', 'format', 'test', 'build']
command: ['lint', 'format', 'test:integration', 'build']
steps:
- uses: actions/checkout@v4

Expand Down
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,19 +135,28 @@ Types are exported from `./lib/types/[node/web]/index.d.ts` and should be automa

<!-- TODO: add instantiation examples here for warp and remote cache for comparisons once write API's are available. -->

### Contract Data Provider Configuration
### Custom Contract Evaluation

By default - the `ArIO` client uses the `mainnet` contract and exposes APIs relevant to the `ArIO` contract. You can provide custom `contract` or `contractTxId` to the `ArIO` constructor and expose those APIs, assuming the contract is compatible with the `ArIO` contract.

```typescript
const arIoContractId = 'INSERT_CUSTOM_REGISTRY_CONTRACT_ID';
// provide a custom contractTxId to the client and default to remote evaluation
const remoteCustomArIO = new ArIO({
contractTxId: 'TESTNET_CONTRACT_TX_ID',
});

const contractDataProvider = new ArNSRemoteCache({
arIoContractId,
remoteCacheUrl: 'http://localhost:3000',
// provide a custom contract to the client, and specify local evaluation using warp
const localCustomArIO = new ArIO({
contract: new WarpContract<ArIOState>({
contractTxId: 'TESTNET_CONTRACT_TX_ID',
}),
});

const arIOLocal = new ArIO({
arIoContractId,
contractDataProvider,
// provide a custom contract to the client, and specify local evaluation using remote cache
const remoteCacheCustomArIO = new ArIO({
contract: new RemoteContract<ArIOState>({
contractTxId: 'TESTNET_CONTRACT_TX_ID',
}),
});
```

Expand Down
18 changes: 18 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version: '3'

services:
arns-service:
image: ghcr.io/ar-io/arns-service:latest
build: .
ports:
- '3000:3000'
environment:
- LOG_LEVEL=debug
- PREFETCH_CONTRACTS=true
- PREFETCH_CONTRACT_IDS=_NctcA2sRy1-J4OmIQZbYFPM17piNcbdBPH2ncX2RL8
- BOOTSTRAP_CONTRACTS=false
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck']
interval: 10s
timeout: 5s
retries: 5
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"format": "prettier --check .",
"format:fix": "prettier --write .",
"test": "yarn clean && c8 jest .",
"test:integration": "docker compose up -d && yarn test && docker compose down",
"prepare": "husky install",
"example:mjs": "yarn build:esm && node examples/node/index.mjs",
"example:cjs": "yarn build:cjs && node examples/node/index.cjs",
Expand Down Expand Up @@ -85,7 +86,6 @@
"http-server": "^14.1.1",
"husky": "^8.0.3",
"jest": "^29.7.0",
"node-stdlib-browser": "^1.2.0",
"prettier": "^3.0.2",
"rimraf": "^5.0.1",
"semantic-release": "^21.0.7",
Expand All @@ -99,7 +99,7 @@
"dependencies": {
"arweave": "^1.14.4",
"axios": "1.4.0",
"warp-contracts": "^1.4.34",
"warp-contracts": "1.4.29",
"winston": "^3.11.0"
}
}
File renamed without changes.
102 changes: 102 additions & 0 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/
import { ArIOState, ArNSNameData, Gateway } from './contract-state.js';

export type BlockHeight = number;
export type SortKey = string;
export type WalletAddress = string;

export type EvaluationOptions = {
evalTo?: { sortKey: SortKey } | { blockHeight: BlockHeight };
// TODO: any other evaluation constraints
};

// combine evaluation parameters with read interaction inputs
export type EvaluationParameters<T = NonNullable<unknown>> = {
evaluationOptions?: EvaluationOptions | Record<string, never> | undefined;
} & T;

export interface SmartWeaveContract<T> {
getContractState(params: EvaluationParameters): Promise<T>;
readInteraction<I, K>({
functionName,
inputs,
evaluationOptions,
}: EvaluationParameters<{ functionName: string; inputs?: I }>): Promise<K>;
// TODO: write interaction
}

// TODO: extend with additional methods
export interface ArIOContract {
getState({ evaluationOptions }: EvaluationParameters): Promise<ArIOState>;
getGateway({
address,
evaluationOptions,
}: EvaluationParameters<{ address: WalletAddress }>): Promise<
Gateway | undefined
>;
getGateways({
evaluationOptions,
}: EvaluationParameters): Promise<
Record<WalletAddress, Gateway> | Record<string, never>
>;
getBalance(
params: { address: WalletAddress } & EvaluationOptions,
): Promise<number>;
getBalances({
evaluationOptions,
}: EvaluationParameters): Promise<
Record<WalletAddress, number> | Record<string, never>
>;
getArNSRecord({
domain,
evaluationOptions,
}: EvaluationParameters<{ domain: string }>): Promise<
ArNSNameData | undefined
>;
getArNSRecords({
evaluationOptions,
}: EvaluationParameters): Promise<
Record<string, ArNSNameData> | Record<string, never>
>;
}

/* eslint-disable @typescript-eslint/no-explicit-any */
export interface Logger {
setLogLevel: (level: string) => void;
setLogFormat: (logFormat: string) => void;
info: (message: string, ...args: any[]) => void;
warn: (message: string, ...args: any[]) => void;
error: (message: string, ...args: any[]) => void;
debug: (message: string, ...args: any[]) => void;
}
/* eslint-enable @typescript-eslint/no-explicit-any */
export interface HTTPClient {
get<I, K>({
endpoint,
signal,
headers,
allowedStatuses,
params,
}: {
endpoint: string;
signal?: AbortSignal;
headers?: Record<string, string>;
allowedStatuses?: number[];
params?: object | I;
}): Promise<K>;
}
151 changes: 123 additions & 28 deletions src/common/ar-io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,139 @@
* 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 { ArIOContract, ArNSNameData, Gateway } from '../types/index.js';
import { ArNSRemoteCache } from './index.js';
import { ARNS_TESTNET_REGISTRY_TX } from '../constants.js';
import {
ArIOContract,
ArIOState,
ArNSNameData,
EvaluationParameters,
Gateway,
SmartWeaveContract,
} from '../types.js';
import { RemoteContract } from './contracts/remote-contract.js';

export type CacheConfiguration = {
remoteCacheUrl?: string;
contractTxId?: string;
};
export type ArIOConfiguration = {
cacheConfig?: CacheConfiguration;
};
// TODO: append this with other configuration options (e.g. local vs. remote evaluation)
export type ContractConfiguration =
| {
contract?: SmartWeaveContract<unknown>;
}
| {
contractTxId: string;
};

function isContractConfiguration<T>(
config: ContractConfiguration,
): config is { contract: SmartWeaveContract<T> } {
return 'contract' in config;
}

function isContractTxIdConfiguration(
config: ContractConfiguration,
): config is { contractTxId: string } {
return 'contractTxId' in config;
}

export class ArIO implements ArIOContract {
protected cache: ArIOContract;
private contract: SmartWeaveContract<ArIOState>;

constructor({ cacheConfig }: ArIOConfiguration = {}) {
this.cache = new ArNSRemoteCache({
contractTxId: cacheConfig?.contractTxId,
url: cacheConfig?.remoteCacheUrl,
});
constructor(
config: ContractConfiguration = {
// default to a contract that uses the arns service to do the evaluation
contract: new RemoteContract<ArIOState>({
contractTxId: ARNS_TESTNET_REGISTRY_TX,
}),
},
) {
if (isContractConfiguration<ArIOState>(config)) {
this.contract = config.contract;
} else if (isContractTxIdConfiguration(config)) {
this.contract = new RemoteContract<ArIOState>({
contractTxId: config.contractTxId,
});
}
}
// implement ArIOContract interface

async getArNSRecord({ domain }: { domain: string }): Promise<ArNSNameData> {
return this.cache.getArNSRecord({ domain });
/**
* Returns the current state of the contract.
*/
async getState(params: EvaluationParameters): Promise<ArIOState> {
const state = await this.contract.getContractState(params);
return state;
}
async getArNSRecords(): Promise<Record<string, ArNSNameData>> {
return this.cache.getArNSRecords();

/**
* Returns the ARNS record for the given domain.
*/
async getArNSRecord({
domain,
evaluationOptions,
}: EvaluationParameters<{ domain: string }>): Promise<
ArNSNameData | undefined
> {
const records = await this.getArNSRecords({ evaluationOptions });
return records[domain];
}
async getBalance({ address }: { address: string }): Promise<number> {
return this.cache.getBalance({ address });

/**
* Returns all ArNS records.
*/
async getArNSRecords({
evaluationOptions,
}: EvaluationParameters = {}): Promise<Record<string, ArNSNameData>> {
const state = await this.contract.getContractState({ evaluationOptions });
return state.records;
}
async getBalances(): Promise<Record<string, number>> {
return this.cache.getBalances();

/**
* Returns the balance of the given address.
*/
async getBalance({
address,
evaluationOptions,
}: EvaluationParameters<{ address: string }>): Promise<number> {
const balances = await this.getBalances({ evaluationOptions });
return balances[address] || 0;
}

/**
* Returns the balances of all addresses.
*/
async getBalances({ evaluationOptions }: EvaluationParameters = {}): Promise<
Record<string, number>
> {
const state = await this.contract.getContractState({ evaluationOptions });
return state.balances;
}
async getGateway({ address }: { address: string }): Promise<Gateway> {
return this.cache.getGateway({ address });

/**
* Returns the gateway for the given address, including weights.
*/
async getGateway({
address,
evaluationOptions,
}: EvaluationParameters<{ address: string }>): Promise<Gateway | undefined> {
return this.contract
.readInteraction<{ target: string }, Gateway>({
functionName: 'gateway',
inputs: {
target: address,
},
evaluationOptions,
})
.catch(() => {
return undefined;
});
}
async getGateways(): Promise<Record<string, Gateway>> {
return this.cache.getGateways();

/**
* Returns all gateways, including weights.
*/
async getGateways({ evaluationOptions }: EvaluationParameters = {}): Promise<
Record<string, Gateway> | Record<string, never>
> {
return this.contract.readInteraction({
functionName: 'gateways',
evaluationOptions,
});
}
}

0 comments on commit 832a1ad

Please sign in to comment.