Merge pull request #1 from arkhai-io/refactor

Refactor: Move nla.ts, Update sample command, Support other AI providers, Update new SDK version
This commit is contained in:
疒奀 2026-01-02 17:51:46 +08:00 committed by GitHub
commit 3eb8b35122
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 477 additions and 247 deletions

View File

@ -34,7 +34,19 @@ ORACLE_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b7
# ================================
# AI CONFIGURATION
# ================================
# At least ONE of the following API keys is required for the oracle to function
# OpenAI API Key (required for oracle)
# OpenAI API Key (optional)
# Get your API key from https://platform.openai.com/api-keys
# Supports models: gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-3.5-turbo
OPENAI_API_KEY=sk-proj...
# Anthropic API Key (optional)
# Get your API key from https://console.anthropic.com/
# Supports models: claude-3-5-sonnet-20241022, claude-3-opus-20240229, claude-3-sonnet-20240229
# ANTHROPIC_API_KEY=sk-ant-...
# OpenRouter API Key (optional)
# Get your API key from https://openrouter.ai/keys
# Supports any model available on OpenRouter (e.g., openai/gpt-4, anthropic/claude-3-opus)
# OPENROUTER_API_KEY=sk-or-...

3
.gitignore vendored
View File

@ -39,3 +39,6 @@ anvil.log
.DS_Store
.env
# Local deployment files
cli/deployments/localhost.json

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"cSpell.words": [
"Tierable"
]
}

156
README.md
View File

@ -2,46 +2,29 @@
## Prerequisites
This project depends on the `alkahest` repository, which must be cloned in the same parent directory.
- [Bun](https://bun.sh) - Fast all-in-one JavaScript runtime
- [Foundry](https://book.getfoundry.sh/getting-started/installation) - Ethereum development toolkit (for Anvil)
### Setup Instructions
1. **Clone both repositories in the same parent directory:**
1. **Clone the repository:**
```bash
# Navigate to your projects directory
cd ~/Desktop # or your preferred location
# Clone the alkahest repository
git clone https://github.com/arkhai-io/alkahest.git
# Clone this repository
git clone https://github.com/arkhai-io/natural-language-agreements.git
# Your directory structure should look like:
# parent-directory/
# ├── alkahest/
# │ └── sdks/
# │ └── ts/
# └── natural-language-agreements/
```
2. **Install alkahest dependencies:**
```bash
cd alkahest
bun install
cd ..
```
3. **Install this project's dependencies:**
```bash
cd natural-language-agreements
```
2. **Install dependencies:**
```bash
bun install
```
4. **Install the `nla` CLI globally (optional but recommended):**
3. **Install the `nla` CLI globally (optional but recommended):**
```bash
# Link the CLI to make it available globally
@ -110,7 +93,26 @@ Watch the oracle terminal - you'll see it process arbitration requests in real-t
## CLI Tools
The `nla` CLI provides a unified interface for all Natural Language Agreement operations.
The `nla` CLI provides a unified interface for all Natural Language Agreement operations with support for multiple LLM providers.
### Supported LLM Providers
The oracle supports multiple AI providers for arbitration:
1. **OpenAI** (default)
- Models: `gpt-4o`, `gpt-4o-mini`, `gpt-4-turbo`, `gpt-3.5-turbo`
- API Key: Get from [OpenAI Platform](https://platform.openai.com/api-keys)
- Environment Variable: `OPENAI_API_KEY`
2. **Anthropic (Claude)**
- Models: `claude-3-5-sonnet-20241022`, `claude-3-opus-20240229`, `claude-3-sonnet-20240229`
- API Key: Get from [Anthropic Console](https://console.anthropic.com/)
- Environment Variable: `ANTHROPIC_API_KEY`
3. **OpenRouter**
- Models: Any model available on OpenRouter (e.g., `openai/gpt-4`, `anthropic/claude-3-opus`)
- API Key: Get from [OpenRouter](https://openrouter.ai/keys)
- Environment Variable: `OPENROUTER_API_KEY`
### Installation
@ -139,13 +141,22 @@ For a complete guide to all CLI commands and options, see [CLI Documentation](cl
### Quick CLI Examples
```bash
# Create an escrow
# Create an escrow with OpenAI (default)
nla escrow:create \
--demand "The sky is blue" \
--amount 10 \
--token 0xa513e6e4b8f2a923d98304ec87f64353c4d5c853 \
--oracle 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
# Create an escrow with custom arbitration settings
nla escrow:create \
--demand "Deliver package by Friday" \
--amount 100 \
--token 0xa513e6e4b8f2a923d98304ec87f64353c4d5c853 \
--oracle 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \
--arbitration-provider "Anthropic" \
--arbitration-model "claude-3-5-sonnet-20241022"
# Fulfill an escrow
nla escrow:fulfill \
--escrow-uid 0x... \
@ -212,6 +223,26 @@ nla escrow:status [options] # Check escrow status
nla help # Show help
```
### Escrow Creation Options
When creating an escrow, you can customize the arbitration settings:
```bash
nla escrow:create \
--demand "Your natural language demand" \
--amount <token-amount> \
--token <erc20-token-address> \
--oracle <oracle-address> \
--arbitration-provider "OpenAI|Anthropic|OpenRouter" \ # Optional, default: OpenAI
--arbitration-model "model-name" \ # Optional, default: gpt-4o-mini
--arbitration-prompt "Custom prompt template" # Optional
```
**Default Arbitration Settings:**
- Provider: `OpenAI`
- Model: `gpt-4o-mini`
- Prompt: Standard evaluation template
**NPM Scripts (alternative):**
```bash
bun run setup # Same as: nla dev
@ -273,12 +304,12 @@ natural-language-agreements/
│ ├── index.ts # Main CLI entry point (nla command)
│ ├── README.md # CLI documentation
│ ├── client/ # User-facing escrow tools
│ │ ├── create-escrow.ts # Create escrow CLI
│ │ ├── create-escrow.ts # Create escrow CLI with arbitration config
│ │ ├── fulfill-escrow.ts # Fulfill escrow CLI
│ │ └── collect-escrow.ts # Collect escrow CLI
│ ├── server/ # Server-side components
│ │ ├── deploy.ts # Contract deployment script
│ │ └── oracle.ts # Oracle service
│ │ └── oracle.ts # Multi-provider oracle service
│ ├── scripts/ # Shell scripts for orchestration
│ │ ├── dev.sh # Development environment setup
│ │ ├── deploy.sh # Deployment wrapper
@ -288,12 +319,14 @@ natural-language-agreements/
│ ├── localhost.json
│ ├── sepolia.json
│ └── mainnet.json
├── clients/
│ └── nla.ts # Natural Language Agreement client library
├── nla.ts # Natural Language Agreement client library
│ # - Multi-provider LLM support
│ # - Arbitration encoding/decoding
│ # - OpenAI, Anthropic, OpenRouter integration
├── tests/
│ ├── nla.test.ts # Basic tests
│ └── nlaOracle.test.ts # Oracle arbitration tests
├── index.ts # Development entry point
├── index.ts # Main exports
├── package.json
└── README.md
```
@ -301,8 +334,9 @@ natural-language-agreements/
## Troubleshooting
### "Cannot find module 'alkahest-ts'"
- Ensure alkahest is cloned in the parent directory
- Run `bun install` in both alkahest and this project
- Run `bun install` to ensure all dependencies are installed
- Clear the cache: `rm -rf node_modules && bun install`
- Check that package.json includes alkahest-ts dependency
### "Deployer has no ETH"
- Fund your deployer account before running deployment
@ -317,7 +351,22 @@ natural-language-agreements/
### "OpenAI API errors"
- Verify API key is valid and active
- Check OpenAI usage limits and billing
- Ensure model name is correct (e.g., "gpt-4o")
- Ensure model name is correct (e.g., "gpt-4o-mini", "gpt-4o")
### "Anthropic API errors"
- Verify ANTHROPIC_API_KEY is set correctly
- Check Anthropic usage limits and billing
- Ensure model name is correct (e.g., "claude-3-5-sonnet-20241022")
### "Arbitration provider not found"
- The oracle was configured with a different provider than the escrow
- Make sure the oracle has the correct API keys for the provider specified in the escrow
- Supported providers: OpenAI, Anthropic, OpenRouter
### "Module resolution errors"
- Run `bun install` to ensure alkahest-ts is properly installed
- Check that you're using the correct version of Bun: `bun --version`
- Clear Bun's cache: `rm -rf node_modules && bun install`
## Security Notes
@ -327,5 +376,38 @@ natural-language-agreements/
- Use environment variables or secure secret management for production
- The `.env` file is gitignored by default
- The example private key in `.env.example` is from Anvil and should NEVER be used in production
- Ensure your OpenAI API key is kept secure and not exposed in logs or error messages
- Keep all API keys secure (OpenAI, Anthropic, OpenRouter):
* Don't expose them in logs or error messages
* Use environment variables or secure secret management
* Rotate keys regularly
* Monitor usage for unauthorized access
- Run the oracle in a secure environment with proper access controls
- For production deployments:
* Use hardware wallets or secure key management services
* Implement rate limiting on the oracle
* Monitor arbitration decisions for anomalies
* Consider using a multi-signature setup for critical operations
## Features
✨ **Multi-Provider LLM Support**
- OpenAI (GPT-4, GPT-4o, GPT-3.5-turbo)
- Anthropic (Claude 3 family)
- OpenRouter (Access to any model)
- Configurable per-escrow arbitration settings
🔧 **Flexible Configuration**
- Custom arbitration prompts
- Provider and model selection
- Default settings with override capability
🚀 **Easy Deployment**
- One-command development setup (`nla dev`)
- Automated contract deployment
- Built-in test token distribution
⚡ **Developer Friendly**
- TypeScript support
- Comprehensive CLI tools
- Unified interface for all operations
- Detailed error messages and logging

View File

@ -5,11 +5,12 @@
"": {
"name": "natural-language-agreement-extension",
"dependencies": {
"@ai-sdk/openai": "^2.0.50",
"@ai-sdk/anthropic": "^3.0.2",
"@ai-sdk/openai": "^3.0.2",
"@viem/anvil": "^0.0.10",
"ai": "^5.0.68",
"alkahest-ts": "git+https://github.com/VAR-META-Tech/alkahest.git#ts-package",
"ai": "^6.0.5",
"arktype": "^2.1.23",
"alkahest-ts": "github:arkhai-io/alkahest",
"viem": "^2.42.1",
"zod": "^3.25.76",
},
@ -24,13 +25,15 @@
"packages": {
"@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-BwV7DU/lAm3Xn6iyyvZdWgVxgLu3SNXzl5y57gMvkW4nGhAOV5269IrJzQwGt03bb107sa6H6uJwWxc77zXoGA=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.2", "", { "dependencies": { "@ai-sdk/provider": "3.0.1", "@ai-sdk/provider-utils": "4.0.2" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-D6iSsrOYryBSPsFtOiEDv54jnjVCU/flIuXdjuRY7LdikB0KGjpazN8Dt4ONXzL+ux69ds2nzFNKke/w/fgLAA=="],
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.86", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-obsLIOyA93lbQiSt1rvBItoVQp1U2RDPs0bNG0JYhm6Gku8Dg/0Cm8e4NUWT5p5PN10/doKSb3SMSKCixwIAKA=="],
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.4", "", { "dependencies": { "@ai-sdk/provider": "3.0.1", "@ai-sdk/provider-utils": "4.0.2", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OlccjNYZ5+4FaNyvs0kb3N5H6U/QCKlKPTGsgUo8IZkqfMQu8ALI1XD6l/BCuTKto+OO9xUPObT/W7JhbqJ5nA=="],
"@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="],
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.2", "", { "dependencies": { "@ai-sdk/provider": "3.0.1", "@ai-sdk/provider-utils": "4.0.2" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-GONwavgSWtcWO+t9+GpGK8l7nIYh+zNtCL/NYDSeHxHiw6ksQS9XMRWrZyE5NpJ0EXNxSAWCHIDmb1WvTqhq9Q=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.19", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA=="],
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-2lR4w7mr9XrydzxBSjir4N6YMGdXD+Np1Sh0RXABh7tWdNFFwIeRI1Q+SaYZMbfL8Pg8RRLcrxQm51yxTLhokg=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.2", "", { "dependencies": { "@ai-sdk/provider": "3.0.1", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-KaykkuRBdF/ffpI5bwpL4aSCmO/99p8/ci+VeHwJO8tmvXtiVAb99QeyvvvXmL61e9Zrvv4GBGoajW19xdjkVQ=="],
"@ark/schema": ["@ark/schema@0.56.0", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA=="],
@ -50,7 +53,7 @@
"@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
@ -62,9 +65,9 @@
"abitype": ["abitype@1.1.0", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A=="],
"ai": ["ai@5.0.113", "", { "dependencies": { "@ai-sdk/gateway": "2.0.21", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-26vivpSO/mzZj0k1Si2IpsFspp26ttQICHRySQiMrtWcRd5mnJMX2a8sG28vmZ38C+JUn1cWmfZrsLMxkSMw9g=="],
"ai": ["ai@6.0.5", "", { "dependencies": { "@ai-sdk/gateway": "3.0.4", "@ai-sdk/provider": "3.0.1", "@ai-sdk/provider-utils": "4.0.2", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CKL3dDHedWskC6EY67LrULonZBU9vL+Bwa+xQEcprBhJfxpogntG3utjiAkYuy5ZQatyWk+SmWG8HLvcnhvbRg=="],
"alkahest-ts": ["alkahest-ts@github:VAR-META-Tech/alkahest#d40de37", {}, "VAR-META-Tech-alkahest-d40de37"],
"alkahest-ts": ["alkahest-ts@github:arkhai-io/alkahest#5df4180", {}, "arkhai-io-alkahest-5df4180"],
"arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="],

View File

@ -145,7 +145,7 @@ async function main() {
console.log("💰 Collecting escrow...\n");
// Collect the escrow
const collectionHash = await client.erc20.collectEscrow(
const collectionHash = await client.erc20.escrow.nonTierable.collect(
escrowUid as `0x${string}`,
fulfillmentUid as `0x${string}`,
);

View File

@ -13,7 +13,7 @@ import { foundry } from "viem/chains";
import { existsSync, readFileSync } from "fs";
import { resolve } from "path";
import { makeClient } from "alkahest-ts";
import { makeLLMClient } from "../../clients/nla.ts";
import { makeLLMClient } from "../..";
import {fixtures} from "alkahest-ts";
// Helper function to display usage
@ -34,6 +34,9 @@ Options:
--private-key <key> Your private key (required)
--deployment <path> Path to deployment file (default: ./cli/deployments/localhost.json)
--rpc-url <url> RPC URL (default: from deployment file)
--arbitration-provider <name> Arbitration provider (default: OpenAI)
--arbitration-model <model> Arbitration model (default: gpt-4o-mini)
--arbitration-prompt <text> Custom arbitration prompt (optional)
--help, -h Display this help message
Environment Variables (alternative to CLI options):
@ -67,6 +70,9 @@ function parseCliArgs() {
"private-key": { type: "string" },
"deployment": { type: "string" },
"rpc-url": { type: "string" },
"arbitration-provider": { type: "string" },
"arbitration-model": { type: "string" },
"arbitration-prompt": { type: "string" },
"help": { type: "boolean", short: "h" },
},
strict: true,
@ -92,6 +98,16 @@ async function main() {
const oracleAddress = args.oracle;
const privateKey = args["private-key"] || process.env.PRIVATE_KEY;
const deploymentPath = args.deployment || "./cli/deployments/localhost.json";
// Arbitration configuration with defaults
const arbitrationProvider = args["arbitration-provider"] || "OpenAI";
const arbitrationModel = args["arbitration-model"] || "gpt-4o-mini";
const arbitrationPrompt = args["arbitration-prompt"] ||
`Evaluate the fulfillment against the demand and decide whether the demand was validly fulfilled
Demand: {{demand}}
Fulfillment: {{obligation}}`;
// Validate required parameters
if (!demand) {
@ -167,17 +183,11 @@ async function main() {
deployment.addresses
);
// Extend with LLM client
// Extend with LLM client (only for encoding the demand, no API calls needed)
const llmClient = client.extend((c) => ({
llm: makeLLMClient([]),
}));
// Add OpenAI provider (needed for encoding demands)
llmClient.llm.addProvider({
providerName: "OpenAI",
apiKey: process.env.OPENAI_API_KEY || "",
});
// Check token balance
const tokenBalance = await walletClient.readContract({
address: tokenAddress as `0x${string}`,
@ -197,22 +207,18 @@ async function main() {
// Encode the demand with oracle arbiter
const arbiter = deployment.addresses.trustedOracleArbiter;
const encodedDemand = client.arbiters.general.trustedOracle.encode({
const encodedDemand = client.arbiters.general.trustedOracle.encodeDemand({
oracle: oracleAddress as `0x${string}`,
data: llmClient.llm.encodeDemand({
arbitrationProvider: "OpenAI",
arbitrationModel: "gpt-4.1",
arbitrationPrompt: `Evaluate the fulfillment against the demand and decide whether the demand was validly fulfilled
Demand: {{demand}}
Fulfillment: {{obligation}}`,
arbitrationProvider,
arbitrationModel,
arbitrationPrompt,
demand: demand
})
});
// Create the escrow
const { attested: escrow } = await client.erc20.permitAndBuyWithErc20(
const { attested: escrow } = await client.erc20.escrow.nonTierable.permitAndCreate(
{
address: tokenAddress as `0x${string}`,
value: BigInt(amount),
@ -228,10 +234,16 @@ Fulfillment: {{obligation}}`,
console.log(` Recipient: ${escrow.recipient}`);
console.log("🎯 Next Steps:");
console.log("1. Wait for someone to fulfill the obligation");
console.log("2. The oracle will arbitrate the fulfillment");
console.log("3. If approved, you can collect the escrow");
console.log(`\n Escrow UID: ${escrow.uid}`);
console.log("1. Someone fulfills the obligation:");
console.log(` nla escrow:fulfill \\`);
console.log(` --escrow-uid ${escrow.uid} \\`);
console.log(` --fulfillment "Yes, the sky is blue" \\`);
console.log(` --oracle ${oracleAddress}`);
console.log("\n2. The oracle will arbitrate the fulfillment automatically");
console.log("\n3. If approved, collect the escrow:");
console.log(` nla escrow:collect \\`);
console.log(` --escrow-uid ${escrow.uid} \\`);
console.log(` --fulfillment-uid <fulfillment-uid>`);
} catch (error) {
console.error("❌ Failed to create escrow:", error);

View File

@ -13,6 +13,7 @@ import { foundry } from "viem/chains";
import { existsSync, readFileSync } from "fs";
import { resolve } from "path";
import { makeClient } from "alkahest-ts";
import { makeLLMClient } from "../..";
// Helper function to display usage
function displayHelp() {
@ -161,26 +162,28 @@ async function main() {
fulfillment,
escrowUid as `0x${string}`,
);
console.log("✅ Fulfillment created!\n");
console.log("📋 Fulfillment Details:");
console.log(` UID: ${fulfillmentAttestation.uid}`);
console.log(` Attester: ${fulfillmentAttestation.attester}\n`);
console.log("📤 Requesting arbitration from oracle...\n");
const escrow = await client.getAttestation(escrowUid as `0x${string}`);
const decodedEscrow = client.erc20.escrow.nonTierable.decodeObligation(escrow.data);
// Request arbitration
await client.oracle.requestArbitration(
await client.arbiters.general.trustedOracle.requestArbitration(
fulfillmentAttestation.uid,
oracleAddress as `0x${string}`,
decodedEscrow.demand
);
console.log("✨ Arbitration requested successfully!\n");
console.log("🎯 Next Steps:");
console.log("1. Wait for the oracle to arbitrate (usually a few seconds)");
console.log("2. Check the result with the oracle");
console.log("3. If approved, collect the escrow");
console.log(`\n Fulfillment UID: ${fulfillmentAttestation.uid}`);
console.log("\n2. If approved, collect the escrow:");
console.log(` nla escrow:collect \\`);
console.log(` --escrow-uid ${escrowUid} \\`);
console.log(` --fulfillment-uid ${fulfillmentAttestation.uid}`);
} catch (error) {
console.error("❌ Failed to fulfill escrow:", error);

View File

@ -1,19 +0,0 @@
{
"network": "localhost",
"chainId": 31337,
"rpcUrl": "http://localhost:8545",
"deployedAt": "2025-12-15T17:03:01.822Z",
"deployer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"addresses": {
"easSchemaRegistry": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
"eas": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512",
"trustedOracleArbiter": "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0",
"stringObligation": "0xcf7ed3acca5a467e9e704c703e8d87f634fb0fc9",
"erc20EscrowObligation": "0xdc64a140aa3e981100a9beca4e685f962f0cf6c9",
"erc20PaymentObligation": "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707",
"erc20BarterUtils": "0x0165878a594ca255338adfa4d48449f69242eb8f",
"mockERC20A": "0xa513e6e4b8f2a923d98304ec87f64353c4d5c853",
"mockERC20B": "0x2279b7a0a67db372996a5fab50d91eaa73d2ebe6",
"mockERC20C": "0x8a791620dd6260079bf849dc5567adc3f2fdc318"
}
}

View File

@ -36,6 +36,9 @@ Options (vary by command):
--private-key <key> Private key (all commands)
--rpc-url <url> RPC URL (default: http://localhost:8545)
--deployment <file> Load addresses from deployment file
--arbitration-provider <name> Arbitration provider (create, default: OpenAI)
--arbitration-model <model> Arbitration model (create, default: gpt-4o-mini)
--arbitration-prompt <text> Custom arbitration prompt (create, optional)
Environment Variables:
PRIVATE_KEY Private key for transactions
@ -107,6 +110,9 @@ function parseCliArgs() {
"private-key": { type: "string" },
"rpc-url": { type: "string" },
"deployment": { type: "string" },
"arbitration-provider": { type: "string" },
"arbitration-model": { type: "string" },
"arbitration-prompt": { type: "string" },
},
strict: true,
});
@ -261,7 +267,7 @@ async function runStatusCommand(args: any) {
const escrow = await publicClient.readContract({
address: addresses.eas,
abi: contracts.IEAS.abi,
abi: contracts.IEAS.abi.abi,
functionName: "getAttestation",
args: [escrowUid],
}) as any;
@ -292,7 +298,7 @@ async function runStatusCommand(args: any) {
const filter = await publicClient.createContractEventFilter({
address: addresses.eas,
abi: contracts.IEAS.abi,
abi: contracts.IEAS.abi.abi,
eventName: "Attested",
fromBlock: 0n,
});
@ -313,7 +319,7 @@ async function runStatusCommand(args: any) {
const fulfillmentUid = (fulfillment as any).args?.uid;
const fulfillmentAttestation = await publicClient.readContract({
address: addresses.eas,
abi: contracts.IEAS.abi,
abi: contracts.IEAS.abi.abi,
functionName: "getAttestation",
args: [fulfillmentUid],
}) as any;
@ -330,7 +336,7 @@ async function runStatusCommand(args: any) {
const decisionUid = (decision as any).args?.uid;
const decisionAttestation = await publicClient.readContract({
address: addresses.eas,
abi: contracts.IEAS.abi,
abi: contracts.IEAS.abi.abi,
functionName: "getAttestation",
args: [decisionUid],
}) as any;

View File

@ -159,7 +159,7 @@ async function main() {
const StringObligation = contracts.StringObligation;
const ERC20EscrowObligation = contracts.ERC20EscrowObligation;
const ERC20PaymentObligation = contracts.ERC20PaymentObligation;
const ERC20BarterCrossToken = contracts.ERC20BarterCrossToken;
const ERC20BarterUtils = contracts.ERC20BarterUtils;
console.log("✅ Contract artifacts loaded\n");
@ -213,8 +213,8 @@ async function main() {
addresses.trustedOracleArbiter = await deployContract(
"Trusted Oracle Arbiter",
TrustedOracleArbiter.abi,
TrustedOracleArbiter.bytecode.object,
TrustedOracleArbiter.abi.abi,
TrustedOracleArbiter.abi.bytecode.object,
[addresses.eas]
);
@ -223,32 +223,31 @@ async function main() {
addresses.stringObligation = await deployContract(
"String Obligation",
StringObligation.abi,
StringObligation.bytecode.object,
StringObligation.abi.abi,
StringObligation.abi.bytecode.object,
[addresses.eas, addresses.easSchemaRegistry]
);
addresses.erc20EscrowObligation = await deployContract(
"ERC20 Escrow Obligation",
ERC20EscrowObligation.abi,
ERC20EscrowObligation.bytecode.object,
ERC20EscrowObligation.abi.abi,
ERC20EscrowObligation.abi.bytecode.object,
[addresses.eas, addresses.easSchemaRegistry]
);
addresses.erc20PaymentObligation = await deployContract(
"ERC20 Payment Obligation",
ERC20PaymentObligation.abi,
ERC20PaymentObligation.bytecode.object,
ERC20PaymentObligation.abi.abi,
ERC20PaymentObligation.abi.bytecode.object,
[addresses.eas, addresses.easSchemaRegistry]
);
// Deploy barter utils (required for permitAndBuyWithErc20)
console.log("🔄 Deploying barter utils...\n");
addresses.erc20BarterUtils = await deployContract(
"ERC20 Barter Utils",
ERC20BarterCrossToken.abi,
ERC20BarterCrossToken.bytecode.object,
ERC20BarterUtils.abi.abi,
ERC20BarterUtils.abi.bytecode.object,
[
addresses.eas,
addresses.erc20EscrowObligation,
@ -259,6 +258,8 @@ async function main() {
"0x0000000000000000000000000000000000000000", // erc1155Payment (not used)
"0x0000000000000000000000000000000000000000", // tokenBundleEscrow (not used)
"0x0000000000000000000000000000000000000000", // tokenBundlePayment (not used)
"0x0000000000000000000000000000000000000000", // nativeEscrow (not used)
"0x0000000000000000000000000000000000000000", // nativePayment (not used)
]
);
@ -363,9 +364,17 @@ async function main() {
console.log("\n🎯 Next steps:");
console.log("1. Start the oracle:");
console.log(" ./scripts/start-oracle.sh " + network);
console.log("\n2. Create an escrow:");
console.log(` bun run escrow:create --demand "Your demand" --amount 10 --token ${addresses.mockERC20A} --oracle 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 --private-key 0xac09...`);
console.log(` nla start-oracle`);
console.log("\n2. Export your private key (use a test account private key):");
console.log(` export PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`);
console.log("\n3. Create an escrow:");
console.log(` nla escrow:create \\`);
console.log(` --demand "The sky is blue" \\`);
console.log(` --amount 10 \\`);
console.log(` --token ${addresses.mockERC20A} \\`);
console.log(` --oracle 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 \\`);
console.log(` --arbitration-provider "OpenAI" \\`);
console.log(` --arbitration-model "gpt-4o-mini"`);
} catch (error) {
console.error("❌ Deployment failed:", error);

View File

@ -3,7 +3,7 @@ import { parseArgs } from "util";
import { parseAbiParameters, createWalletClient, http, publicActions } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { foundry } from "viem/chains";
import { makeLLMClient } from "../../clients/nla";
import { makeLLMClient } from "../..";
import { existsSync, readFileSync } from "fs";
import { resolve } from "path";
import { makeClient } from "alkahest-ts";
@ -20,7 +20,9 @@ Usage:
Options:
--rpc-url <url> RPC URL for the blockchain network (required)
--private-key <key> Private key of the oracle operator (required)
--openai-api-key <key> OpenAI API key (required)
--openai-api-key <key> OpenAI API key (optional)
--anthropic-api-key <key> Anthropic API key (optional)
--openrouter-api-key <key> OpenRouter API key (optional)
--eas-contract <address> EAS contract address (optional)
--deployment <file> Load addresses from deployment file (optional)
--polling-interval <ms> Polling interval in milliseconds (default: 5000)
@ -30,6 +32,8 @@ Environment Variables (alternative to CLI options):
RPC_URL RPC URL for the blockchain network
ORACLE_PRIVATE_KEY Private key of the oracle operator
OPENAI_API_KEY OpenAI API key
ANTHROPIC_API_KEY Anthropic API key
OPENROUTER_API_KEY OpenRouter API key
EAS_CONTRACT_ADDRESS EAS contract address
Examples:
@ -55,6 +59,8 @@ function parseCliArgs() {
"rpc-url": { type: "string" },
"private-key": { type: "string" },
"openai-api-key": { type: "string" },
"anthropic-api-key": { type: "string" },
"openrouter-api-key": { type: "string" },
"eas-contract": { type: "string" },
"deployment": { type: "string" },
"polling-interval": { type: "string" },
@ -109,6 +115,8 @@ async function main() {
const privateKey = args["private-key"] || process.env.ORACLE_PRIVATE_KEY;
const openaiApiKey = args["openai-api-key"] || process.env.OPENAI_API_KEY;
const anthropicApiKey = args["anthropic-api-key"] || process.env.ANTHROPIC_API_KEY;
const openrouterApiKey = args["openrouter-api-key"] || process.env.OPENROUTER_API_KEY;
const pollingInterval = parseInt(args["polling-interval"] || "5000");
// Validate required parameters
@ -124,8 +132,10 @@ async function main() {
process.exit(1);
}
if (!openaiApiKey) {
console.error("❌ Error: OpenAI API key is required. Use --openai-api-key or set OPENAI_API_KEY environment variable.");
// Check if at least one API key is provided
if (!openaiApiKey && !anthropicApiKey && !openrouterApiKey) {
console.error("❌ Error: At least one LLM provider API key is required.");
console.error(" Set one of: OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY");
console.error("Run with --help for usage information.");
process.exit(1);
}
@ -134,7 +144,14 @@ async function main() {
console.log("Configuration:");
console.log(` 📡 RPC URL: ${rpcUrl}`);
console.log(` 🔑 Oracle Key: ${privateKey.slice(0, 6)}...${privateKey.slice(-4)}`);
console.log(` 🤖 AI Provider: OpenAI`);
// Show available providers
const availableProviders = [];
if (openaiApiKey) availableProviders.push("OpenAI");
if (anthropicApiKey) availableProviders.push("Anthropic");
if (openrouterApiKey) availableProviders.push("OpenRouter");
console.log(` 🤖 AI Providers: ${availableProviders.join(", ")}`);
if (easContract) {
console.log(` 📝 EAS Contract: ${easContract}`);
}
@ -158,19 +175,39 @@ async function main() {
llm: makeLLMClient([]),
}));
llmClient.llm.addProvider({
providerName: "OpenAI",
apiKey: openaiApiKey,
});
// Add all available providers
if (openaiApiKey) {
llmClient.llm.addProvider({
providerName: "OpenAI",
apiKey: openaiApiKey,
});
console.log("✅ OpenAI provider configured");
}
console.log("🎯 LLM Arbitrator configured and ready\n");
if (anthropicApiKey) {
llmClient.llm.addProvider({
providerName: "Anthropic",
apiKey: anthropicApiKey,
});
console.log("✅ Anthropic provider configured");
}
if (openrouterApiKey) {
llmClient.llm.addProvider({
providerName: "OpenRouter",
apiKey: openrouterApiKey,
});
console.log("✅ OpenRouter provider configured");
}
console.log("\n🎯 LLM Arbitrator configured and ready\n");
console.log("👂 Listening for arbitration requests...\n");
// Define the obligation ABI
const obligationAbi = parseAbiParameters("(string item)");
// Start listening and arbitrating
const { unwatch } = await client.oracle.listenAndArbitrate(
const { unwatch } = await client.arbiters.general.trustedOracle.listenAndArbitrate(
async (attestation: any) => {
console.log(`\n📨 New arbitration request received!`);
console.log(` Attestation UID: ${attestation.uid}`);

View File

@ -1,98 +0,0 @@
import {
decodeAbiParameters,
encodeAbiParameters,
parseAbiParameters,
} from "viem";
import { generateText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
export type LLMProvider = {
providerName: string;
apiKey?: string;
};
export type LLMDemand = {
arbitrationProvider: string;
arbitrationModel: string;
arbitrationPrompt: string;
demand: string;
};
export const makeLLMClient = (
providers: LLMProvider[],
) => {
const LLMAbi = parseAbiParameters(
"(string arbitrationProvider, string arbitrationModel, string arbitrationPrompt, string demand)",
);
const encodeDemand = (demand: LLMDemand) => {
return encodeAbiParameters(
LLMAbi,
[demand],
);
};
const arbitrate = async (demand: LLMDemand, obligation: string): Promise<boolean> => {
try {
const matchingProvider = providers.find(provider =>
provider.providerName.toLowerCase().includes(demand.arbitrationProvider.toLowerCase()) ||
demand.arbitrationProvider.toLowerCase().includes(provider.providerName.toLowerCase())
);
const selectedProvider = matchingProvider || providers[0];
if (!selectedProvider) {
throw new Error('No LLM provider available');
}
console.log(`Using provider: ${selectedProvider.providerName} for arbitration demand: ${JSON.stringify(demand)}`);
if (selectedProvider.providerName.toLowerCase() === 'openai') {
const openai = createOpenAI({
apiKey: selectedProvider.apiKey,
})
// Replace placeholders with actual values
const promptTemplate = `${demand.arbitrationPrompt}`
.replace(/\{\{demand\}\}/g, demand.demand)
.replace(/\{\{obligation\}\}/g, obligation);
const { text } = await generateText({
model: openai(demand.arbitrationModel),
system: "You are an arbitrator that always tells the truth. You must respond with only 'true' or 'false' - no other words or explanations.",
prompt: `${promptTemplate}
Based on the above information, determine if the fulfillment satisfies the demand.
Answer ONLY with 'true' or 'false' - no explanations or additional text.`
});
console.log(`LLM Response: ${text}`);
const cleanedResponse = text.trim().toLowerCase();
return cleanedResponse === 'true';
}
return false;
} catch (error) {
console.error('Error in LLM arbitration:', error);
throw new Error(`LLM arbitration failed: ${error}`);
}
};
const addProvider = (provider: LLMProvider): void => {
providers.push(provider);
};
const getProvider = (providerName: string): LLMProvider | undefined => {
return providers.find(provider => provider.providerName.toLowerCase() === providerName.toLowerCase());
};
return {
LLMAbi,
arbitrate,
encodeDemand,
addProvider,
getProvider,
providers,
};
};

View File

@ -1,12 +1,2 @@
import { generateText } from "ai"
import { createOpenAI } from "@ai-sdk/openai"
const openai = createOpenAI({
apiKey: undefined,
})
const { text } = await generateText({
model: openai("gpt-4.1"),
prompt: "What is love?",
})
console.log(text);
export { makeLLMClient } from "./nla";
export type { LLMProvider, LLMDemand } from "./nla";

183
nla.ts Normal file
View File

@ -0,0 +1,183 @@
/**
* Natural Language Agreement (NLA) Client
*
* Supports multiple LLM providers for arbitration:
*
* 1. OpenAI:
* - providerName: "OpenAI"
* - models: "gpt-4", "gpt-4-turbo", "gpt-3.5-turbo", etc.
* - Get API key from: https://platform.openai.com/api-keys
*
* 2. Anthropic (Claude):
* - providerName: "Anthropic" or "Claude"
* - models: "claude-3-5-sonnet-20241022", "claude-3-opus-20240229", etc.
* - Get API key from: https://console.anthropic.com/
*
* 3. OpenRouter:
* - providerName: "OpenRouter"
* - models: Any model available on OpenRouter (e.g., "openai/gpt-4", "anthropic/claude-3-opus")
* - Get API key from: https://openrouter.ai/keys
* - baseURL: "https://openrouter.ai/api/v1" (default)
*
* Example usage:
* ```typescript
* const llmClient = makeLLMClient([]);
*
* // Add OpenAI provider
* llmClient.addProvider({
* providerName: "OpenAI",
* apiKey: "sk-..."
* });
*
* // Add Anthropic provider
* llmClient.addProvider({
* providerName: "Anthropic",
* apiKey: "sk-ant-..."
* });
*
* // Add OpenRouter provider
* llmClient.addProvider({
* providerName: "OpenRouter",
* apiKey: "sk-or-...",
* baseURL: "https://openrouter.ai/api/v1"
* });
* ```
*/
import {
decodeAbiParameters,
encodeAbiParameters,
parseAbiParameters,
} from "viem";
import { generateText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
import { createAnthropic } from "@ai-sdk/anthropic";
export type LLMProvider = {
providerName: string;
apiKey?: string;
baseURL?: string; // For OpenRouter or custom endpoints
};
export type LLMDemand = {
arbitrationProvider: string;
arbitrationModel: string;
arbitrationPrompt: string;
demand: string;
};
export const makeLLMClient = (
providers: LLMProvider[],
) => {
const LLMAbi = parseAbiParameters(
"(string arbitrationProvider, string arbitrationModel, string arbitrationPrompt, string demand)",
);
const encodeDemand = (demand: LLMDemand) => {
return encodeAbiParameters(
LLMAbi,
[demand],
);
};
const arbitrate = async (demand: LLMDemand, obligation: string): Promise<boolean> => {
try {
const matchingProvider = providers.find(provider =>
provider.providerName.toLowerCase().includes(demand.arbitrationProvider.toLowerCase()) ||
demand.arbitrationProvider.toLowerCase().includes(provider.providerName.toLowerCase())
);
const selectedProvider = matchingProvider || providers[0];
if (!selectedProvider) {
throw new Error('No LLM provider available');
}
console.log(`Using provider: ${selectedProvider.providerName} for arbitration demand: ${JSON.stringify(demand)}`);
// Replace placeholders with actual values
const promptTemplate = `${demand.arbitrationPrompt}`
.replace(/\{\{demand\}\}/g, demand.demand)
.replace(/\{\{obligation\}\}/g, obligation);
const systemPrompt = "You are an arbitrator that always tells the truth. You must respond with only 'true' or 'false' - no other words or explanations.";
const userPrompt = `${promptTemplate}
Based on the above information, determine if the fulfillment satisfies the demand.
Answer ONLY with 'true' or 'false' - no explanations or additional text.`;
let text: string;
const providerName = selectedProvider.providerName.toLowerCase();
if (providerName === 'openai' || providerName.includes('openai')) {
const openai = createOpenAI({
apiKey: selectedProvider.apiKey,
baseURL: selectedProvider.baseURL,
});
const result = await generateText({
model: openai(demand.arbitrationModel),
system: systemPrompt,
prompt: userPrompt,
});
text = result.text;
} else if (providerName === 'anthropic' || providerName.includes('anthropic') || providerName.includes('claude')) {
const anthropic = createAnthropic({
apiKey: selectedProvider.apiKey,
baseURL: selectedProvider.baseURL,
});
const result = await generateText({
model: anthropic(demand.arbitrationModel),
system: systemPrompt,
prompt: userPrompt,
});
text = result.text;
} else if (providerName === 'openrouter' || providerName.includes('openrouter')) {
// OpenRouter uses OpenAI-compatible API
const openrouter = createOpenAI({
apiKey: selectedProvider.apiKey,
baseURL: selectedProvider.baseURL,
});
const result = await generateText({
model: openrouter(demand.arbitrationModel),
system: systemPrompt,
prompt: userPrompt,
});
text = result.text;
} else {
throw new Error(`Unsupported provider: ${selectedProvider.providerName}`);
}
console.log(`LLM Response: ${text}`);
const cleanedResponse = text.trim().toLowerCase();
return cleanedResponse === 'true';
} catch (error) {
console.error('Error in LLM arbitration:', error);
throw new Error(`LLM arbitration failed: ${error}`);
}
};
const addProvider = (provider: LLMProvider): void => {
providers.push(provider);
};
const getProvider = (providerName: string): LLMProvider | undefined => {
return providers.find(provider => provider.providerName.toLowerCase() === providerName.toLowerCase());
};
return {
LLMAbi,
arbitrate,
encodeDemand,
addProvider,
getProvider,
providers,
};
};

View File

@ -25,10 +25,11 @@
"typescript": "^5.9.3"
},
"dependencies": {
"@ai-sdk/openai": "^2.0.50",
"@ai-sdk/anthropic": "^3.0.2",
"@ai-sdk/openai": "^3.0.2",
"@viem/anvil": "^0.0.10",
"ai": "^5.0.68",
"alkahest-ts": "git+https://github.com/VAR-META-Tech/alkahest.git#ts-package",
"ai": "^6.0.5",
"alkahest-ts": "github:arkhai-io/alkahest",
"arktype": "^2.1.23",
"viem": "^2.42.1",
"zod": "^3.25.76"

View File

@ -1,6 +1,6 @@
import { afterAll, beforeAll, beforeEach, expect, test } from "bun:test";
import { makeLLMClient } from "../clients/nla";
import type { LLMProvider, LLMDemand } from "../clients/nla";
import { makeLLMClient } from "..";
import type { LLMProvider, LLMDemand } from "..";
import {
setupTestEnvironment,
type TestContext,

View File

@ -8,7 +8,7 @@ import {
setupTestEnvironment,
type TestContext,
} from "alkahest-ts";
import { makeLLMClient } from "../clients/nla";
import { makeLLMClient } from "..";
let testContext: TestContext;
let charlieClient: ReturnType<typeof testContext.charlie.client.extend<{ llm: ReturnType<typeof makeLLMClient> }>>;
@ -40,7 +40,7 @@ afterAll(async () => {
test("listenAndArbitrate Natural Language", async () => {
const arbiter = testContext.addresses.trustedOracleArbiter;
const demand = testContext.alice.client.arbiters.general.trustedOracle.encode({
const demand = testContext.alice.client.arbiters.general.trustedOracle.encodeDemand({
oracle: testContext.bob.address,
data: charlieClient.llm.encodeDemand({
arbitrationProvider: "OpenAI",
@ -55,7 +55,7 @@ Fulfillment: {{obligation}}`,
});
const { attested: escrow } =
await testContext.alice.client.erc20.permitAndBuyWithErc20(
await testContext.alice.client.erc20.escrow.nonTierable.permitAndCreate(
{
address: testContext.mockAddresses.erc20A,
value: 10n,
@ -66,7 +66,7 @@ Fulfillment: {{obligation}}`,
const obligationAbi = parseAbiParameters("(string item)");
const { decisions, unwatch } =
await testContext.bob.client.oracle.listenAndArbitrate(
await testContext.bob.client.arbiters.general.trustedOracle.listenAndArbitrate(
async (attestation) => {
console.log("arbitrating");
const obligation = testContext.bob.client.extractObligationData(
@ -102,15 +102,16 @@ Fulfillment: {{obligation}}`,
escrow.uid,
);
await testContext.bob.client.oracle.requestArbitration(
await testContext.bob.client.arbiters.general.trustedOracle.requestArbitration(
fulfillment.uid,
testContext.bob.address,
demand
);
//Should call WaitForArbitration()
await Bun.sleep(2000);
await Bun.sleep(5000);
const collectionHash = await testContext.bob.client.erc20.collectEscrow(
const collectionHash = await testContext.bob.client.erc20.escrow.nonTierable.collect(
escrow.uid,
fulfillment.uid,
);