dria-oracle-sdk
Version:
An on-chain LLM Oracle SDK for Dria
461 lines (452 loc) • 17.9 kB
text/typescript
import { Hex, Address, Transport, Chain, PublicClient, WalletClient, Account, Prettify, BlockTag } from 'viem';
import { ArweaveIrys } from '@irys/sdk/node/flavours/arweave';
/** Re-export of the `JWKInterface` type, which represents an Arweave wallet.
*
* @example
* const path = "./wallet.json";
* const key = JSON.parse(fs.readFileSync(path, "utf-8")) as ArweaveWallet;
*/
type ArweaveWallet = ConstructorParameters<typeof ArweaveIrys>[0]["key"];
/** An Arweave key is considered to be an object with `arweave` field. */
type ArweaveStorageKey = {
arweave: string;
};
/**
* An Arweave wrapper for decentralized storage interface.
*
* It can be used to read Arweave values from the coordinator, as well as put Arweave-uploaded value to the coordinator.
*
* Note that for `write` operations, it uses Irys SDK to interact with the Arweave network,
* for `put` and `balance` commands ,so it requires `@irys/sdk` peer dependency for those.
*
* @example
* // read only
* const storage = new ArweaveStorage();
*
* @example
* // read & write
* const storage = new ArweaveStorage();
* const key: JWInterface = JSON.parse(fs.readFileSync("./wallet.json", "utf-8"));
* storage.init(key);
*/
declare class ArweaveStorage implements DecentralizedStorage<Buffer, ArweaveStorageKey> {
/** Byte threshold, beyond which data is uploaded to Arweave. */
bytesLimit: number;
/** Irys SDK instance. */
irys?: ArweaveIrys;
/** Base URL of the gateway. */
baseUrl: string;
/**
* Initializes the Arweave storage with the given key.
* @param key Arweave wallet object.
* @param bytesLimit Number of bytes such that smaller data are not uploaded, default is 1024 bytes.
*/
init(key: ArweaveWallet, bytesLimit?: number): void;
/** Return an `ArweaveKey` object if `key` is a stringified object of the form `{ arweave: string }`.
*
* @example // a valid key
* { arweave: "Zg6CZYfxXCWYnCuKEpnZCYfy7ghit1_v4-BCe53iWuA" }
*/
isKey(key: string): ArweaveStorageKey | null;
get(key: ArweaveStorageKey): Promise<Buffer | null>;
/**
* Uploads the given value to Arweave and returns the key.
*
* _Requires `init` to have been called._
*
* @param value Value to upload.
* @returns Key to access the value.
*/
put(value: Buffer): Promise<ArweaveStorageKey>;
/**
* Returns the balance of the Arweave wallet.
*
* _Requires `init` to have been called._
*
* @returns Balance in `bigint`.
*/
balance(): Promise<bigint>;
describe(): string;
}
/**
* A key-value storage that can be used to store arbitrary data in a decentralized-storage fashion.
* Most often, the `T` is a `Buffer` and `K` is anything custom defined.
*
* @template T The type of the data to store.
* @template K The type of the key to use.
*/
interface DecentralizedStorage<T = unknown, K = string> {
/** The number of bytes, after which storage shall be considered. */
bytesLimit: number;
/**
* @param key The key to get the value for.
* @returns The value at the key, or `null` if it does not exist.
*/
get(key: K): Promise<T | null>;
/**
* Puts a value to the storage, returns the key.
* @param value The value to put in the storage.
* @returns The key that can be used to retrieve the value.
*/
put(value: T): Promise<K>;
/**
* @param key The key to check.
* @returns Whether the key if its valid, or `null` if its invalid.
*/
isKey(key: string): K | null;
/** Returns the name of the storage. */
describe(): string;
}
/**
* Task status as it appears within the contract.
*
* - `None`: Task has not been created yet. (default)
* - `PendingGeneration`: Task is waiting for Oracle generation responses.
* - `PendingValidation`: Task is waiting for validation by validator Oracles.
* - `Completed`: The task has been completed.
*
* There are two scenarios:
* - With validation, the flow is `None -> PendingGeneration -> PendingValidation -> Completed`.
* - Without validation, the flow is `None -> PendingGeneration -> Completed`.
*
* Note that this type is compatible with the contract type (number).
*/
declare enum TaskStatus {
None = 0,
PendingGeneration = 1,
PendingValidation = 2,
Completed = 3
}
/** Collection of oracle task-related parameters. */
interface TaskParameters {
/** Difficulty of the task. */
difficulty: number;
/** Number of generations. */
numGenerations: number;
/** Number of validations. */
numValidations: number;
}
/**
* A task request for LLM generation.
* Fees are stored here as well in case fee changes occur within the duration of a task.
*
* Adapted from [`LLMOracleTask.sol`](https://github.com/firstbatchxyz/dria-oracle-contracts/blob/master/src/LLMOracleTask.sol#L34).
*/
interface TaskRequest {
/** Requesting address, also responsible of the fee payment. */
requester: Address;
/** Protocol string, such as `dria/0.1.0`. */
protocol: string;
/** Task parameters, e.g. difficulty and number of generations & validations. */
parameters: TaskParameters;
/** Task status. */
status: TaskStatus;
/** Fee paid to each generator per generation. */
generatorFee: bigint;
/** Fee paid to each validator per validated generation. */
validatorFee: bigint;
/** Fee paid to the platform */
platformFee: bigint;
/** Input data for the task, usually a human-readable string. */
input: string;
/** Allowed model names for the task. */
models: string;
}
/** A task generation response.
* @template O Output data type, defaults `Hex`.
* @template M Metadata type, defaults `Hex`.
*/
interface TaskResponse<O = Hex, M = Hex> {
/** Responding Oracle address. */
responder: Address;
/** Proof-of-Work nonce for SHA3(taskId, input, requester, responder, nonce) < difficulty. */
nonce: bigint;
/** Final validation score assigned by validators, stays 0 if there is no validation. */
score: bigint;
/** Output data for the task, usually the direct output of LLM. */
output: O;
/** Optional metadata for this generation. */
metadata: M;
}
/** A task validation for a response.
* @template M Metadata type, defaults `Hex`.
*/
interface TaskValidation<M = Hex> {
/** Responding validator address. */
validator: Address;
/** Proof-of-Work nonce for SHA3(taskId, input, requester, responder, nonce) < difficulty. */
nonce: bigint;
/** Validation scores */
scores: readonly bigint[];
/** Optional metadata for this validation. */
metadata: M;
}
/** A task validaiton score object.
*
* Within a task validation, we usually expect an array of these objects,
* one for each generation.
*
* The `final_score` here is the actual score considered by the contract,
* and `rationale` describes how the LLM decided that score.
*
* A score is expected to be a number between 1 and 5, inclusive; where 1 is worst and 5 is best.
*/
type TaskValidationScores = {
helpfulness: number;
instruction_following: number;
truthfulness: number;
/** The final score given by the node. */
final_score: number;
rationale: string;
};
/** A chat history entry. */
type ChatHistoryResponse = {
/** Role, usually `user`, `assistant` or `system`. */
role: string;
/** Message content. */
content: string;
};
/** A request with chat history. */
type ChatHistoryRequest = {
/** Task id of which the output will act like history. */
history_id: number | bigint;
/** Message content. */
content: string;
};
/** Return type for `request` function. */
type NewRequestReturnType = {
txHash: Hex;
protocol: string;
input: string;
models: Models;
taskParameters: TaskParameters;
};
/** Optional arguments for `request`. */
type TaskRequestOptions = {
taskParameters?: Partial<TaskParameters>;
protocol?: string;
};
/**
* Allowed Oracle models.
*
* The requested model(s) can be any of the following:
*
* - An array of model names, such as `["gemini-1.5-pro", "gpt-4o-mini"]`.
* - `*` for any model randomly (of the responder).
* - `!` for the first model (of the responder).
*
* You can look at the available models from [this repository](https://github.com/andthattoo/ollama-workflows/blob/main/src/program/models.rs#L14).
*/
type Models = string[] | "*" | "!";
/**
* The Oracle client is used to interact with the Dria Oracles. It allows you to make requests, read responses, and process them.
*
* It can be instantiated with a `storage` as well, which can be used to store large data in a decentralized manner.
* If the data to be written to contract is large, we can instead store that data in the storage and pass the key to the contract.
* This key can then be used to fetch the data from the storage.
*
* @template T transport type, e.g. HTTP or WebSocket (usually inferred)
* @template C chain type, e.g. Ethereum or Binance Smart Chain (usually inferred)
* @template K storage key type, e.g. `ArweaveKey` (usually inferred)
* @example
* // without storage
* const oracle = new Oracle({ public, wallet });
* await oracle.init(coordinatorAddress);
*
* @example
* // with storage (Arweave)
* const wallet = JSON.parse(readFileSync("./path/to/wallet.json", "utf-8"));
* const arweave = new ArweaveStorage(wallet);
* const oracle = new Oracle({ public, wallet }, arweave);
* await oracle.init(coordinatorAddress);
*/
declare class Oracle<T extends Transport, C extends Chain, K = unknown> {
readonly client: {
public: PublicClient<T, C>;
wallet: WalletClient<T, C, Account>;
};
readonly storage?: DecentralizedStorage<Buffer, K> | undefined;
coordinator?: ReturnType<InstanceType<typeof Oracle<Transport, Chain, K>>["Coordinator"]>;
token?: ReturnType<InstanceType<typeof Oracle<Transport, Chain, K>>["Token"]>;
taskParameters: TaskParameters;
protocol: string;
constructor(client: {
public: PublicClient<T, C>;
wallet: WalletClient<T, C, Account>;
}, storage?: DecentralizedStorage<Buffer, K> | undefined);
/**
* Initialize the oracle client by setting up contract instances.
* @param coordinatorAddress coordinator contract address
* @returns initialized oracle client
*/
init(coordinatorAddress: Address): Promise<this>;
/**
* Change the underlying default task parameters.
* @param opts new default task parameters
*/
withParameters(opts: Partial<TaskParameters>): this;
/**
* Change the underlying default protocol.
*
* The protocol is a string that should fit a `bytes32` type in Solidity. It is used
* to identify the source of the request, and can be used within event filters.
*
* It should have to format `name/version`, e.g. `dria-oracle-sdk/0.x.x`.
* @param protocol protocol name
*/
withProtocol(protocol: string): this;
/** Returns a new instance of the LLM coordinator contract.
* @warning This is an internal method, and making it `private` will break
* the type export due to the size of ABI-inferred types.
*/
private Coordinator;
/** Returns a new instance of an ERC20 token contract.
* @warning This is an internal method, and making it `private` will break
* the type export due to the size of ABI-inferred types.
*/
private Token;
/**
* Make an oracle request.
* @param input input string, or a chat input
* - a string input can be anything, like "What is 2+2?"
* - a chat input is an object `{historyId: number, content: string}` where
* the `historyId` is a task id the output of which is to be used as history
* @param models requested models, can be any of the following:
* - `*` for all models
* - `!` for first model of the responder
* - `["model1", "model2", ...]` for specific models
*
* (defaults to `*`)
* @param opts optional request arguments, such as `protocol` and `taskParameters`
* @returns task transaction hash
*/
request(input: string, models: Models, opts?: TaskRequestOptions): Promise<NewRequestReturnType>;
request(input: Prettify<ChatHistoryRequest>, models: Models, opts?: TaskRequestOptions): Promise<NewRequestReturnType>;
/**
* Waits until the request transaction is mined and returns the task id.
* @param txHash transaction hash for the request (see `request`)
* @returns taskId
*/
waitRequest(txHash: Hex): Promise<bigint>;
/**
* Alias for `getBestResponse` followed by `processResponse`.
* @param taskId task id
* @param kind task kind, e.g. `chat` for conversational models
* @returns processed task response
*/
read(taskId: bigint): Promise<TaskResponse<string | null, string | null>>;
/**
* Returns a boolean indicating if the task is completed.
* @param taskId task id
* @returns true if the task is completed, or `taskId` is 0
*/
isCompleted(taskId: bigint | number): Promise<boolean>;
/**
* Returns the task request for a given task id.
* @param taskId task id
* @returns task request
*/
getRequest(taskId: bigint | number): Promise<TaskRequest>;
/**
* Returns the highest scored response of a task.
* Will throw an error if the task is not completed yet!
* @param taskId task id
* @returns task response with the highest score
*/
getBestResponse(taskId: bigint | number): Promise<Prettify<TaskResponse>>;
/**
* Returns the validations of all generation responses for a task.
* @param taskId task id
* @returns array of task validations
*/
getValidations(taskId: bigint | number): Promise<readonly Prettify<TaskValidation>[]>;
/**
* Returns the generation responses for a task.
* @param taskId task id
* @returns array of task responses
*/
getResponses(taskId: bigint | number): Promise<readonly Prettify<TaskResponse>[]>;
/**
* Process the `output` and `metadata` of a task, with respect to the given storage.
* @param response existing response object
* @returns response object with processed `output` and `metadata`
*/
processResponse(response: TaskResponse): Promise<TaskResponse<string | null, string | null>>;
/**
* Process the `metadata` of a task, with respect to the given storage.
* @param response existing validation object
* @returns validation object with processed `metadata`
*/
processValidation(validation: TaskValidation): Promise<TaskValidation<TaskValidationScores[]>>;
/** Shorthand to parse a string input to a chat history response. */
toChatHistory(input: string): Prettify<ChatHistoryResponse>[];
/**
* Fetches the task events from the coordinator contract and returns its args.
* A returned task event has the taskId, protocol, statusBefore, and statusAfter.
* For a completed task, we are looking for the statusAfter to be `Completed` which is `3`.
*
* @param opts options for fetching tasks
* - `protocol`: protocol name
* - `from`: block to start from
* - `to`: block to end at
* - `status`: task status to filter by
* @returns array of task events
*/
getTaskEvents(opts: {
protocol?: string;
from?: bigint | BlockTag;
to?: bigint | BlockTag;
status?: TaskStatus;
}): Promise<{
taskId?: bigint | undefined;
protocol?: `0x${string}` | undefined;
statusBefore?: number | undefined;
statusAfter?: number | undefined;
}[]>;
/**
* Waits for a task to be completed, i.e. it should have all the required
* generations and validations be done.
* @param taskId task id
*/
wait(taskId: bigint | number): Promise<void>;
/** Returns the allowance of the client for the coordinator.
* @returns allowance amount
*/
allowance(): Promise<bigint>;
/**
* Approves the coordinator to spend the client's tokens.
* @param amount amount to approve, defaults to max uint256 (infinite)
* @returns transaction hash
*/
approve(amount?: bigint): Promise<Hex>;
}
/**
* Given a string, converts it to a `Hex` string.
*
* First, the string is converted to a `Uint8Array`.
* - If `storage` is given and the bytearray is large enough, it is uploaded to the storage and its key in `Hex` is returned.
* - Otherwise, the bytearray is converted to a `Hex` string.
*
* If storage is being used, make sure it can upload the data, e.g. `ArweaveStorage` must be `init`ed.
*
* @param bytes input string
* @param storage decentralized storage, optional
* @template K storage key type, inferred as `unknown` if storage is `undefined`
* @returns a `Hex` string, with 0x prefix
*/
declare function stringToContractBytesWithStorage<K>(input: string, storage?: DecentralizedStorage<Buffer, K>): Promise<Hex>;
/**
* Given a `bytes` Solidity type, converts it to a string.
*
* If `storage` is given, the resulting string is try-parsed as a key, and if it is a key,
* the actual value is fetched from the storage.
*
* If no value is found at the storage, it returns `null`.
*
* @param input bytes
* @param storage decentralized storage, optional
* @template K storage key type, inferred as `unknown` if storage is `undefined`
* @returns parsed string
*/
declare function contractBytesToStringWithStorage<K>(input: Hex, storage?: DecentralizedStorage<Buffer, K>): Promise<string | null>;
export { ArweaveStorage, type ArweaveWallet, type DecentralizedStorage, Oracle, type Models as OracleModels, TaskStatus, contractBytesToStringWithStorage, stringToContractBytesWithStorage };