@bolloon/bolloon-agent 0.4.31 → 0.4.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/chain/chain-config.js +504 -0
- package/dist/agents/chain/chain-index-query.js +135 -0
- package/dist/agents/chain/chain-indexer.js +1045 -0
- package/dist/agents/chain/chain-settlement.js +284 -0
- package/dist/agents/chain/chain-state-store.js +245 -0
- package/dist/agents/chain/chain-wallet.js +198 -0
- package/dist/agents/chain/escrow-client.js +433 -0
- package/dist/agents/chain/index.js +20 -0
- package/dist/agents/chain/onchain-trade.js +691 -0
- package/dist/agents/network-pulse.js +464 -2
- package/dist/agents/task/task-onchain-runner.js +284 -0
- package/dist/agents/x402/paid-info-store.js +75 -6
- package/dist/cli/commands/chain.js +926 -0
- package/dist/cli/commands/index.js +10 -2
- package/dist/cli/mcp/server.js +4 -1
- package/dist/cli/mcp/tools.js +348 -5
- package/dist/cli/protocol-envelope.js +12 -0
- package/dist/cli-entry.js +5 -0
- package/dist/electron-build/agents/chain/chain-config.js +556 -0
- package/dist/electron-build/agents/chain/chain-config.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-index-query.js +145 -0
- package/dist/electron-build/agents/chain/chain-index-query.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-indexer.js +1095 -0
- package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-settlement.js +291 -0
- package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -0
- package/dist/electron-build/agents/chain/escrow-client.js +443 -0
- package/dist/electron-build/agents/chain/escrow-client.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +478 -3
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/x402/paid-info-store.js +76 -6
- package/dist/electron-build/agents/x402/paid-info-store.js.map +1 -1
- package/dist/web/chain-explorer.js +617 -0
- package/dist/web/explorer.html +163 -0
- package/dist/web/index.html +5 -0
- package/dist/web/routes-chain-index.js +108 -0
- package/dist/web/routes-onchain-trade.js +99 -0
- package/dist/web/server.js +13 -0
- package/package.json +2 -1
- package/scripts/build-web.ts +20 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chain-wallet.ts — 链上交易的**签名放行闸** (P3, ③)
|
|
3
|
+
*
|
|
4
|
+
* 硬规则:
|
|
5
|
+
* 「链上交易签名走 `authorizeWalletSignature` 这个唯一放行闸 (fail-closed)
|
|
6
|
+
* + 写 `~/.bolloon/wallet-signatures.jsonl` 审计 (不记私钥、不记任务正文);
|
|
7
|
+
* 未授权一律拒。」
|
|
8
|
+
*
|
|
9
|
+
* 所以这个文件里**没有第二条**"要不要签"的判断: 任何链上写操作都必须先过
|
|
10
|
+
* `authorizeChainTransaction()`, 它内部只有 `task-contract.authorizeWalletSignature` 一个权威。
|
|
11
|
+
* 未授权 → 不造 signer、不发交易、不写审计, 直接拒绝并说明哪一条没过。
|
|
12
|
+
*
|
|
13
|
+
* 三层授权来源 (与 CLI `bolloon wallet sign` 完全一致, 只是入口不同):
|
|
14
|
+
* ① `~/.bolloon/signing-policy.json` 的 `agentAuthorized: true`
|
|
15
|
+
* ② 环境变量 `BOLLOON_AGENT_AUTHORIZED=1`
|
|
16
|
+
* ③ (钱包本身) `BOLLOON_WALLET_PRIVATE_KEY` / `~/.bolloon/wallet.json`
|
|
17
|
+
* 程序参数**只能收紧** (更小额度 / 白名单交集), 不能凭空授予。
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'fs';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import * as crypto from 'crypto';
|
|
22
|
+
import { authorizeWalletSignature, recordSignatureAudit, readSignatureAudit, isPaymentMode, } from '../task-contract.js';
|
|
23
|
+
import { fingerprintOf, payloadDigest } from '../local-signer.js';
|
|
24
|
+
import { bolloonHome, walletAvailable, assertNotForeignWalletPath } from './chain-config.js';
|
|
25
|
+
/** 链上签名的能力名 (授权白名单里用这个) */
|
|
26
|
+
export const CHAIN_SIGN_CAPABILITY = 'chain.escrow';
|
|
27
|
+
export function chainSigningPolicyPath(home) {
|
|
28
|
+
return path.join(bolloonHome(home), 'signing-policy.json');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 读本机签名授权策略 (语义与 `src/cli/commands/wallet.ts:readSigningPolicy` 一致;
|
|
32
|
+
* 两处都必须只有这 ①② 两个授予源)。
|
|
33
|
+
*/
|
|
34
|
+
export function readChainSigningPolicy(opts = {}) {
|
|
35
|
+
const env = opts.env || process.env;
|
|
36
|
+
const envOn = String(env.BOLLOON_AGENT_AUTHORIZED || '') === '1';
|
|
37
|
+
const out = {
|
|
38
|
+
agentAuthorized: envOn,
|
|
39
|
+
source: envOn ? 'env BOLLOON_AGENT_AUTHORIZED=1' : 'default (未授权)',
|
|
40
|
+
};
|
|
41
|
+
const f = chainSigningPolicyPath(opts.home);
|
|
42
|
+
assertNotForeignWalletPath(f);
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(f)) {
|
|
45
|
+
const j = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
46
|
+
if (j?.agentAuthorized === true) {
|
|
47
|
+
out.agentAuthorized = true;
|
|
48
|
+
out.source = `${f} (agentAuthorized=true)`;
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(j?.allowedNetworks))
|
|
51
|
+
out.allowedNetworks = j.allowedNetworks.map(String);
|
|
52
|
+
if (Array.isArray(j?.allowedCapabilities))
|
|
53
|
+
out.allowedCapabilities = j.allowedCapabilities.map(String);
|
|
54
|
+
if (typeof j?.maxPerTxAtomic === 'string')
|
|
55
|
+
out.maxPerTxAtomic = j.maxPerTxAtomic;
|
|
56
|
+
if (typeof j?.dailyLimitAtomic === 'string')
|
|
57
|
+
out.dailyLimitAtomic = j.dailyLimitAtomic;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
out.fileError = String(e?.message || e).slice(0, 160);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
const ATOMIC = /^[0-9]+$/;
|
|
66
|
+
const minAtomic = (a, b) => {
|
|
67
|
+
const okv = (v) => !!v && ATOMIC.test(v);
|
|
68
|
+
if (!okv(a))
|
|
69
|
+
return okv(b) ? b : undefined;
|
|
70
|
+
if (!okv(b))
|
|
71
|
+
return a;
|
|
72
|
+
return BigInt(a) <= BigInt(b) ? a : b;
|
|
73
|
+
};
|
|
74
|
+
/** 意图 → 幂等 requestId (同一 method+taskKey+金额+链 = 同一个请求, 只签一次) */
|
|
75
|
+
export function chainRequestIdOf(intent, chainId) {
|
|
76
|
+
const basis = [intent.method, intent.taskKey.toLowerCase(), intent.amountAtomic || '0', chainId ?? '?', intent.intentNonce || ''].join('|');
|
|
77
|
+
return `chaintx-${intent.method}-${crypto.createHash('sha256').update(basis).digest('hex').slice(0, 16)}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* ★ 链上签名的唯一放行判断 (fail-closed)。
|
|
81
|
+
* 任何一条不满足 → `allowed: false` 且给出原因; 调用方**必须**原样停下。
|
|
82
|
+
*/
|
|
83
|
+
export async function authorizeChainTransaction(intent, opts = {}) {
|
|
84
|
+
const policy = readChainSigningPolicy({ home: opts.home, env: opts.env });
|
|
85
|
+
const mode = isPaymentMode(intent.mode) ? intent.mode : 'agent-authorized';
|
|
86
|
+
const amountAtomic = String(intent.amountAtomic ?? '0');
|
|
87
|
+
const network = String(intent.network || 'unknown');
|
|
88
|
+
const capability = String(intent.capability || CHAIN_SIGN_CAPABILITY);
|
|
89
|
+
const readAudit = opts.auditReader || readSignatureAudit;
|
|
90
|
+
let audit = [];
|
|
91
|
+
try {
|
|
92
|
+
audit = await readAudit(opts.home, 1000);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
audit = [];
|
|
96
|
+
}
|
|
97
|
+
const dayStart = new Date();
|
|
98
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
99
|
+
const spentTodayAtomic = audit
|
|
100
|
+
.filter((a) => a.at >= dayStart.getTime())
|
|
101
|
+
.reduce((acc, a) => acc + (ATOMIC.test(String(a.amountAtomic || '')) ? BigInt(String(a.amountAtomic)) : 0n), 0n)
|
|
102
|
+
.toString();
|
|
103
|
+
const signedRequestIds = audit.map((a) => a.requestId);
|
|
104
|
+
const w = walletAvailable({ home: opts.home, env: opts.env });
|
|
105
|
+
const requestId = chainRequestIdOf(intent, opts.chainId);
|
|
106
|
+
const decision = authorizeWalletSignature({
|
|
107
|
+
mode,
|
|
108
|
+
agentAuthorized: policy.agentAuthorized,
|
|
109
|
+
amountAtomic,
|
|
110
|
+
network,
|
|
111
|
+
capability,
|
|
112
|
+
requestId,
|
|
113
|
+
signedRequestIds,
|
|
114
|
+
allowedNetworks: policy.allowedNetworks,
|
|
115
|
+
allowedCapabilities: policy.allowedCapabilities,
|
|
116
|
+
maxPerTxAtomic: minAtomic(policy.maxPerTxAtomic, intent.maxPerTxAtomic),
|
|
117
|
+
dailyLimitAtomic: minAtomic(policy.dailyLimitAtomic, intent.dailyLimitAtomic),
|
|
118
|
+
spentTodayAtomic,
|
|
119
|
+
walletAvailable: w.available,
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
allowed: decision.allowed,
|
|
123
|
+
requestId,
|
|
124
|
+
reason: decision.reason,
|
|
125
|
+
checks: decision.checks,
|
|
126
|
+
policy,
|
|
127
|
+
walletAvailable: w.available,
|
|
128
|
+
walletSource: w.source,
|
|
129
|
+
amountAtomic, network, capability, spentTodayAtomic,
|
|
130
|
+
privateKeyTouched: false,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** 审计 kind 映射 (schema 只有 4 种, 这里如实映射, 不新增字段) */
|
|
134
|
+
function auditKindOf(method) {
|
|
135
|
+
if (method === 'submitProofV2')
|
|
136
|
+
return 'task_result';
|
|
137
|
+
if (method === 'disputeV2')
|
|
138
|
+
return 'task_request';
|
|
139
|
+
return 'task_payment'; // createEscrowV2 / releaseV2 / claimAfterTimeoutV2 都是钱的移动
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* ★ 受控发交易: 放行闸 → (只在这里) 取私钥造 signer → 执行 → 写审计。
|
|
143
|
+
* 放行闸拒绝时: 不取私钥、不发交易、不写审计。
|
|
144
|
+
*
|
|
145
|
+
* `execute` 拿到的 signer 是唯一能用私钥的对象, 调用方不许把它带出作用域。
|
|
146
|
+
*/
|
|
147
|
+
export async function sendChainTxGuarded(params) {
|
|
148
|
+
const auth = await authorizeChainTransaction(params.intent, {
|
|
149
|
+
home: params.home, env: params.env, chainId: params.client.chainId, auditReader: params.auditReader,
|
|
150
|
+
});
|
|
151
|
+
if (!auth.allowed) {
|
|
152
|
+
return { allowed: false, reason: auth.reason, auth, auditWritten: false };
|
|
153
|
+
}
|
|
154
|
+
// ★ 只有走到这里才碰私钥 (局部变量, 不外泄)
|
|
155
|
+
let signer;
|
|
156
|
+
try {
|
|
157
|
+
signer = params.signer
|
|
158
|
+
|| (params.signerFactory ? params.signerFactory(params.client) : params.client.localSigner({ home: params.home }));
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
return {
|
|
162
|
+
allowed: false,
|
|
163
|
+
reason: `放行闸过了但取不到本机钱包: ${String(e?.message || e).slice(0, 200)}`,
|
|
164
|
+
auth, auditWritten: false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const outcome = await params.execute(signer);
|
|
168
|
+
// 审计: 只记摘要 (意图摘要, 不含私钥 / 不含任务正文)
|
|
169
|
+
const digest = payloadDigest(JSON.stringify({
|
|
170
|
+
method: params.intent.method, taskKey: params.intent.taskKey, amountAtomic: auth.amountAtomic,
|
|
171
|
+
}));
|
|
172
|
+
const entry = {
|
|
173
|
+
kind: auditKindOf(params.intent.method),
|
|
174
|
+
mode: (isPaymentMode(params.intent.mode) ? params.intent.mode : 'agent-authorized'),
|
|
175
|
+
requestId: auth.requestId,
|
|
176
|
+
taskId: params.intent.taskId,
|
|
177
|
+
amountAtomic: auth.amountAtomic,
|
|
178
|
+
currency: 'USDC',
|
|
179
|
+
network: auth.network,
|
|
180
|
+
capability: auth.capability,
|
|
181
|
+
signerFingerprint: fingerprintOf(String(params.client.escrowAddress)),
|
|
182
|
+
payloadDigest: digest,
|
|
183
|
+
};
|
|
184
|
+
let auditWritten = false;
|
|
185
|
+
try {
|
|
186
|
+
const rec = params.recordAudit || recordSignatureAudit;
|
|
187
|
+
await rec(entry, params.home);
|
|
188
|
+
auditWritten = true;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
auditWritten = false;
|
|
192
|
+
}
|
|
193
|
+
return { allowed: true, auth, outcome, auditWritten, auditEntry: entry };
|
|
194
|
+
}
|
|
195
|
+
/** 便捷包装: 让 4 个 v2 写方法都只能从放行闸过 */
|
|
196
|
+
export async function gatedEscrowWrite(client, intent, run, opts = {}) {
|
|
197
|
+
return sendChainTxGuarded({ client, intent, execute: run, ...opts });
|
|
198
|
+
}
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* escrow-client.ts — AgentEscrow v2 的链桥客户端 (P3: Bolloon 链桥)
|
|
3
|
+
*
|
|
4
|
+
* 职责 (只有这些, 不做判断):
|
|
5
|
+
* · 用 ethers v6 调 AgentEscrow v2 的 4 个写方法:
|
|
6
|
+
* createEscrowV2 / submitProofV2 / releaseV2 / claimAfterTimeoutV2
|
|
7
|
+
* · 读 escrow 结构 (19 字段) 与链上 hash 复算入口
|
|
8
|
+
* · 听 5 个 v2 事件
|
|
9
|
+
* · 读 receipt / 数确认数 (把"链上到底成没成"的**原始事实**拿出来)
|
|
10
|
+
*
|
|
11
|
+
* 它**不判断**"算不算已结算" —— 那是 chain-settlement.ts 的事。
|
|
12
|
+
* 这样客户端能随便换 provider 做测试, 而判定逻辑只有一份。
|
|
13
|
+
*
|
|
14
|
+
* 两个必须照做的本机坑:
|
|
15
|
+
* ① `new JsonRpcProvider(url, null, { cacheTimeout: -1 })`:
|
|
16
|
+
* ethers v6 默认缓存 eth_getTransactionCount/eth_blockNumber 250ms。
|
|
17
|
+
* 本地链瞬时出块 → 第二笔交易会拿旧 nonce → "nonce has already been used"。
|
|
18
|
+
* 构造入口统一走 `createJsonRpcProvider()`, 不散落在调用方。
|
|
19
|
+
* ② signer / provider **可注入**: 测试要能用假 provider 造重组 / 掉 RPC,
|
|
20
|
+
* 而不能真的去连网。
|
|
21
|
+
*/
|
|
22
|
+
import { JsonRpcProvider, Contract, Interface, Wallet, AbiCoder, encodeBytes32String, keccak256, } from 'ethers';
|
|
23
|
+
import { loadChainConfig, readWalletPrivateKey, DEFAULT_CONFIRMATIONS, LOCAL_DEV_CHAIN_ID, } from './chain-config.js';
|
|
24
|
+
// ── 冻结 hash 口径 (与 AgentEscrow.sol / MODEL_FREEZE.md §4 逐字一致) ─────────
|
|
25
|
+
export const TAG_TASK = encodeBytes32String('bolloon.task.v1'); // "bolloon.task.v1" → bytes32
|
|
26
|
+
const ABICODER = AbiCoder.defaultAbiCoder();
|
|
27
|
+
/** taskKey = keccak256(abi.encode(bytes32("bolloon.task.v1"), taskId)) */
|
|
28
|
+
export function computeTaskKeyOffChain(taskId) {
|
|
29
|
+
return keccak256(ABICODER.encode(['bytes32', 'string'], [TAG_TASK, taskId]));
|
|
30
|
+
}
|
|
31
|
+
/** v1 过渡兼容键 = keccak256(utf8(taskId)) */
|
|
32
|
+
export function computeLegacyTaskKeyOffChain(taskId) {
|
|
33
|
+
return keccak256(new TextEncoder().encode(taskId));
|
|
34
|
+
}
|
|
35
|
+
/** resultHash/inputHash/manifestHash = keccak256(utf8("sha256:<hex>")) */
|
|
36
|
+
export function computeResultHashOffChain(sha256Digest) {
|
|
37
|
+
return keccak256(new TextEncoder().encode(String(sha256Digest)));
|
|
38
|
+
}
|
|
39
|
+
/** proofHash = keccak256(abi.encode(bytes32("bolloon.proof.v1"), resultHash, proofVersion)) */
|
|
40
|
+
export function computeProofHashOffChain(resultHash, proofVersion) {
|
|
41
|
+
return keccak256(ABICODER.encode(['bytes32', 'bytes32', 'uint16'], [encodeBytes32String('bolloon.proof.v1'), resultHash, proofVersion]));
|
|
42
|
+
}
|
|
43
|
+
/** EscrowState 枚举名 (与合约顺序一致) */
|
|
44
|
+
export const ESCROW_STATES = ['ACTIVE', 'RELEASED', 'DISPUTED', 'REFUNDED'];
|
|
45
|
+
// ── ABI (只嵌 v2 面 + 复算入口 + 5 个 v2 事件; 不改合约, 只是本地 ABI 视图) ────
|
|
46
|
+
export const AGENT_ESCROW_V2_ABI = [
|
|
47
|
+
// ── 写 ──
|
|
48
|
+
'function createEscrowV2(bytes32 taskKey, address agent, uint256 amount, address paymentAsset, bytes32 termsHash, bytes32 quoteHash, bytes32 inputHash, bytes32 manifestHash, uint64 deadline, uint32 confirmationWindow, uint16 proofVersion) returns (bytes32)',
|
|
49
|
+
'function submitProofV2(bytes32 taskKey, bytes32 resultHash, bytes32 manifestHash, uint16 proofVersion) returns (bytes32 proofHash)',
|
|
50
|
+
'function releaseV2(bytes32 taskKey)',
|
|
51
|
+
'function claimAfterTimeoutV2(bytes32 taskKey)',
|
|
52
|
+
'function disputeV2(bytes32 taskKey, bytes32 reasonHash)',
|
|
53
|
+
'function refundV2(bytes32 taskKey, bytes32 reasonHash)',
|
|
54
|
+
'function releaseAfterArbitrationV2(bytes32 taskKey)',
|
|
55
|
+
// ── 读 ──
|
|
56
|
+
'function escrows(bytes32 taskKey) view returns (tuple(address buyer, address agent, uint256 amount, uint8 state, uint256 createdAt, bytes32 proofHash, bytes32 taskKey, bytes32 termsHash, bytes32 quoteHash, bytes32 inputHash, bytes32 resultHash, bytes32 manifestHash, uint256 chainId, uint32 contractVersion, uint64 createdBlock, uint64 deadline, uint32 confirmationWindow, address paymentAsset, uint16 proofVersion))',
|
|
57
|
+
'function claimableAt(bytes32 taskKey) view returns (uint256)',
|
|
58
|
+
'function balance() view returns (uint256)',
|
|
59
|
+
'function token() view returns (address)',
|
|
60
|
+
'function releaseTimeout() view returns (uint256)',
|
|
61
|
+
'function CONTRACT_VERSION() view returns (uint32)',
|
|
62
|
+
// P4 追加的只读视图 (与 contracts/deployments/abis/AgentEscrow.json 逐字一致; 只加读, 不改写面)
|
|
63
|
+
'function expireGrace() view returns (uint256)',
|
|
64
|
+
'function DEFAULT_EXPIRE_GRACE() view returns (uint256)',
|
|
65
|
+
'function expireAt(bytes32 taskKey) view returns (uint256)',
|
|
66
|
+
'function owner() view returns (address)',
|
|
67
|
+
'function taskIds(uint256 index) view returns (bytes32)',
|
|
68
|
+
// ── 链上复算入口 ──
|
|
69
|
+
'function computeTaskKey(string taskId) pure returns (bytes32)',
|
|
70
|
+
'function computeLegacyTaskKey(string taskId) pure returns (bytes32)',
|
|
71
|
+
'function computeResultHash(string sha256Digest) pure returns (bytes32)',
|
|
72
|
+
'function computeProofHash(bytes32 resultHash, uint16 proofVersion) pure returns (bytes32)',
|
|
73
|
+
// ── 5 个 v2 事件 ──
|
|
74
|
+
'event EscrowCreatedV2(bytes32 indexed taskKey, bytes32 indexed quoteHash, address indexed buyer, address agent, address paymentAsset, uint256 amount, uint256 deadline, uint32 confirmationWindow, uint16 proofVersion, uint32 contractVersion)',
|
|
75
|
+
'event ProofSubmittedV2(bytes32 indexed taskKey, bytes32 resultHash, bytes32 manifestHash, bytes32 proofHash, uint16 proofVersion)',
|
|
76
|
+
'event ReleasedV2(bytes32 indexed taskKey, address to, uint256 amount, uint8 by)',
|
|
77
|
+
'event RefundedV2(bytes32 indexed taskKey, address to, uint256 amount, bytes32 reasonHash)',
|
|
78
|
+
'event DisputedV2(bytes32 indexed taskKey, address by, bytes32 reasonHash)',
|
|
79
|
+
];
|
|
80
|
+
export const V2_EVENT_NAMES = ['EscrowCreatedV2', 'ProofSubmittedV2', 'ReleasedV2', 'RefundedV2', 'DisputedV2'];
|
|
81
|
+
/** ★ 唯一的 provider 构造入口 (cacheTimeout: -1 是本机链的硬要求)
|
|
82
|
+
* 注: ethers v6 的 TS 签名把 network 标成 `Networkish | undefined`, 但运行时支持 `null`
|
|
83
|
+
* (部署脚本 scripts/deploy.js 一直这么用)。这里保留 `null` 的运行时语义并显式转型,
|
|
84
|
+
* 不改成本地默认值 —— 本地瞬时出块不缓存 nonce 才是要点。 */
|
|
85
|
+
export function createJsonRpcProvider(rpcUrl) {
|
|
86
|
+
return new JsonRpcProvider(rpcUrl, null, { cacheTimeout: -1 });
|
|
87
|
+
}
|
|
88
|
+
export class EscrowClient {
|
|
89
|
+
provider;
|
|
90
|
+
escrowAddress;
|
|
91
|
+
confirmations;
|
|
92
|
+
iface;
|
|
93
|
+
chainId;
|
|
94
|
+
injectedSigner;
|
|
95
|
+
constructor(opts = {}) {
|
|
96
|
+
let cfg = opts.config;
|
|
97
|
+
if (!cfg && (opts.rpcUrl || opts.escrowAddress)) {
|
|
98
|
+
// 允许只给部分字段的轻量构造 (测试): 缺的用默认值填, 但地址必须有
|
|
99
|
+
cfg = {
|
|
100
|
+
chainId: LOCAL_DEV_CHAIN_ID, networkName: 'unknown', rpcUrl: opts.rpcUrl || '',
|
|
101
|
+
escrowAddress: opts.escrowAddress || '', tokenAddress: null, tokenDecimals: 6,
|
|
102
|
+
confirmations: opts.confirmations || DEFAULT_CONFIRMATIONS, sources: {},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (!cfg)
|
|
106
|
+
cfg = loadChainConfig();
|
|
107
|
+
if (!cfg.escrowAddress)
|
|
108
|
+
throw new Error('EscrowClient: 缺 escrowAddress (给 config 或 escrowAddress)');
|
|
109
|
+
this.escrowAddress = cfg.escrowAddress;
|
|
110
|
+
this.confirmations = opts.confirmations || cfg.confirmations || DEFAULT_CONFIRMATIONS;
|
|
111
|
+
this.chainId = cfg.chainId || undefined;
|
|
112
|
+
this.iface = new Interface(AGENT_ESCROW_V2_ABI);
|
|
113
|
+
this.injectedSigner = opts.signer;
|
|
114
|
+
if (opts.provider)
|
|
115
|
+
this.provider = opts.provider;
|
|
116
|
+
else if (cfg.rpcUrl)
|
|
117
|
+
this.provider = createJsonRpcProvider(cfg.rpcUrl);
|
|
118
|
+
else
|
|
119
|
+
throw new Error('EscrowClient: 既没注入 provider, 也没给 rpcUrl');
|
|
120
|
+
}
|
|
121
|
+
/** 只读合约 (事件过滤 / 读结构都走它) */
|
|
122
|
+
get readContract() {
|
|
123
|
+
return new Contract(this.escrowAddress, AGENT_ESCROW_V2_ABI, this.provider);
|
|
124
|
+
}
|
|
125
|
+
/** 造一个连着 signer 的合约 (写路径) */
|
|
126
|
+
contractWith(signer) {
|
|
127
|
+
return new Contract(this.escrowAddress, AGENT_ESCROW_V2_ABI, signer);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 本机钱包 signer。私钥只在这里的局部变量里存在一瞬, 不返回给外部。
|
|
131
|
+
* 注: 真正的"允不允许签"由 chain-wallet.ts 的放行闸决定 —— 本方法只负责造对象。
|
|
132
|
+
*/
|
|
133
|
+
localSigner(opts = {}) {
|
|
134
|
+
if (this.injectedSigner)
|
|
135
|
+
return this.injectedSigner;
|
|
136
|
+
const { privateKey } = readWalletPrivateKey({ home: opts.home });
|
|
137
|
+
return new Wallet(privateKey, this.provider);
|
|
138
|
+
}
|
|
139
|
+
// ── 读: escrow 结构 ────────────────────────────────────────────────────────
|
|
140
|
+
normalizeEscrow(raw) {
|
|
141
|
+
const state = Number(raw.state);
|
|
142
|
+
return {
|
|
143
|
+
buyer: raw.buyer,
|
|
144
|
+
agent: raw.agent,
|
|
145
|
+
amount: BigInt(raw.amount),
|
|
146
|
+
state,
|
|
147
|
+
stateName: ESCROW_STATES[state] || 'UNKNOWN',
|
|
148
|
+
createdAt: BigInt(raw.createdAt),
|
|
149
|
+
proofHash: raw.proofHash,
|
|
150
|
+
taskKey: raw.taskKey,
|
|
151
|
+
termsHash: raw.termsHash,
|
|
152
|
+
quoteHash: raw.quoteHash,
|
|
153
|
+
inputHash: raw.inputHash,
|
|
154
|
+
resultHash: raw.resultHash,
|
|
155
|
+
manifestHash: raw.manifestHash,
|
|
156
|
+
chainId: BigInt(raw.chainId),
|
|
157
|
+
contractVersion: Number(raw.contractVersion),
|
|
158
|
+
createdBlock: Number(raw.createdBlock),
|
|
159
|
+
deadline: BigInt(raw.deadline),
|
|
160
|
+
confirmationWindow: Number(raw.confirmationWindow),
|
|
161
|
+
paymentAsset: raw.paymentAsset,
|
|
162
|
+
proofVersion: Number(raw.proofVersion),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** 读 escrow; 不存在 (buyer == 0) → null (不假装读到了一条空 escrow) */
|
|
166
|
+
async getEscrow(taskKey) {
|
|
167
|
+
const raw = await this.readContract.escrows(taskKey);
|
|
168
|
+
const e = this.normalizeEscrow(raw);
|
|
169
|
+
if (/^0x0{40}$/i.test(String(e.buyer)))
|
|
170
|
+
return null;
|
|
171
|
+
return e;
|
|
172
|
+
}
|
|
173
|
+
async balance() {
|
|
174
|
+
return BigInt(await this.readContract.balance());
|
|
175
|
+
}
|
|
176
|
+
async tokenAddress() {
|
|
177
|
+
return await this.readContract.token();
|
|
178
|
+
}
|
|
179
|
+
async contractVersion() {
|
|
180
|
+
return Number(await this.readContract.CONTRACT_VERSION());
|
|
181
|
+
}
|
|
182
|
+
async claimableAt(taskKey) {
|
|
183
|
+
return BigInt(await this.readContract.claimableAt(taskKey));
|
|
184
|
+
}
|
|
185
|
+
/** P4: 托管过期宽限期 (秒) 与单条 taskKey 的过期时间 (只读) */
|
|
186
|
+
async expireGrace() {
|
|
187
|
+
return BigInt(await this.readContract.expireGrace());
|
|
188
|
+
}
|
|
189
|
+
async defaultExpireGrace() {
|
|
190
|
+
return BigInt(await this.readContract.DEFAULT_EXPIRE_GRACE());
|
|
191
|
+
}
|
|
192
|
+
async expireAt(taskKey) {
|
|
193
|
+
return BigInt(await this.readContract.expireAt(taskKey));
|
|
194
|
+
}
|
|
195
|
+
async owner() {
|
|
196
|
+
return await this.readContract.owner();
|
|
197
|
+
}
|
|
198
|
+
/** 链上任务数组的第 index 个 taskKey (越界会 revert; 调用方自己 try/catch) */
|
|
199
|
+
async taskIdAt(index) {
|
|
200
|
+
return await this.readContract.taskIds(index);
|
|
201
|
+
}
|
|
202
|
+
async releaseTimeout() {
|
|
203
|
+
return BigInt(await this.readContract.releaseTimeout());
|
|
204
|
+
}
|
|
205
|
+
/** 链上复算入口 (交叉核对口径用; 与本地复算不一致 = 口径漂了) */
|
|
206
|
+
async computeTaskKey(taskId) { return await this.readContract.computeTaskKey(taskId); }
|
|
207
|
+
async computeLegacyTaskKey(taskId) { return await this.readContract.computeLegacyTaskKey(taskId); }
|
|
208
|
+
async computeResultHash(sha256Digest) { return await this.readContract.computeResultHash(sha256Digest); }
|
|
209
|
+
async computeProofHash(resultHash, proofVersion) {
|
|
210
|
+
return await this.readContract.computeProofHash(resultHash, proofVersion);
|
|
211
|
+
}
|
|
212
|
+
// ── 读: receipt / 确认数 ──────────────────────────────────────────────────
|
|
213
|
+
/** 读 receipt; 读不到 (未打包 / RPC 报错) → null。异常原样抛出由调用方区分"没有"和"读不到"。 */
|
|
214
|
+
async getReceipt(txHash) {
|
|
215
|
+
return await this.provider.getTransactionReceipt(txHash);
|
|
216
|
+
}
|
|
217
|
+
async getLatestBlockNumber() {
|
|
218
|
+
return await this.provider.getBlockNumber();
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* 确认数 = latestBlock - receipt.blockNumber + 1。
|
|
222
|
+
* 读不到 receipt 或 latestBlock → 抛错 (调用方必须如实报"不知道")。
|
|
223
|
+
*/
|
|
224
|
+
async confirmationsOf(txHash) {
|
|
225
|
+
const rc = await this.getReceipt(txHash);
|
|
226
|
+
if (!rc)
|
|
227
|
+
throw new Error(`tx 还没上链 (拿不到 receipt): ${txHash}`);
|
|
228
|
+
const latestBlock = await this.getLatestBlockNumber();
|
|
229
|
+
return { confirmations: latestBlock - rc.blockNumber + 1, blockNumber: rc.blockNumber, latestBlock };
|
|
230
|
+
}
|
|
231
|
+
/** 交易被哪个块打包 (重组检测用: 同一个 tx 换块 = 提示) */
|
|
232
|
+
async transactionBlockNumber(txHash) {
|
|
233
|
+
const tx = await this.provider.getTransaction(txHash);
|
|
234
|
+
return tx ? Number(tx.blockNumber) : null;
|
|
235
|
+
}
|
|
236
|
+
// ── 写 (全部返回 TxOutcome; 失败如实报, 不抛成"成功") ──────────────────────
|
|
237
|
+
async send(fn, signer) {
|
|
238
|
+
const c = this.contractWith(signer);
|
|
239
|
+
let tx = null;
|
|
240
|
+
try {
|
|
241
|
+
tx = await fn(c);
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
// 广播前就失败 (estimateGas / 非白名单) → 没有 txHash, 如实报"没发出去"
|
|
245
|
+
const hash = e?.transaction?.hash || e?.receipt?.hash || '';
|
|
246
|
+
if (!hash) {
|
|
247
|
+
return { txHash: '', blockNumber: null, gasUsed: null, status: null, reverted: false, logs: [], broadcast: false, error: String(e?.shortMessage || e?.reason || e?.message || e).slice(0, 300) };
|
|
248
|
+
}
|
|
249
|
+
// 有 hash: 交易确实发出去了
|
|
250
|
+
const rc = e?.receipt;
|
|
251
|
+
return {
|
|
252
|
+
txHash: hash, blockNumber: rc?.blockNumber ?? null, gasUsed: rc ? rc.gasUsed.toString() : null,
|
|
253
|
+
status: rc ? Number(rc.status) : null, reverted: rc ? Number(rc.status) === 0 : false,
|
|
254
|
+
logs: rc?.logs ?? [], broadcast: true,
|
|
255
|
+
error: String(e?.shortMessage || e?.reason || e?.message || e).slice(0, 300),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const txHash = tx.hash;
|
|
259
|
+
try {
|
|
260
|
+
const rc = await tx.wait();
|
|
261
|
+
if (!rc) {
|
|
262
|
+
return { txHash, blockNumber: null, gasUsed: null, status: null, reverted: false, logs: [], broadcast: true, error: 'receipt 为空 (还没打包?)' };
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
txHash, blockNumber: rc.blockNumber, gasUsed: rc.gasUsed.toString(),
|
|
266
|
+
status: Number(rc.status), reverted: Number(rc.status) === 0,
|
|
267
|
+
logs: rc.logs ?? [], broadcast: true,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
catch (e) {
|
|
271
|
+
// ethers v6: status 0 时 tx.wait() 会抛, 但 e.receipt 带着真 receipt
|
|
272
|
+
const rc = e?.receipt;
|
|
273
|
+
return {
|
|
274
|
+
txHash, blockNumber: rc?.blockNumber ?? null, gasUsed: rc ? rc.gasUsed.toString() : null,
|
|
275
|
+
status: rc ? Number(rc.status) : null, reverted: rc ? Number(rc.status) === 0 : false,
|
|
276
|
+
logs: rc?.logs ?? [], broadcast: true,
|
|
277
|
+
error: String(e?.shortMessage || e?.reason || e?.message || e).slice(0, 300),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async createEscrowV2(p, signer) {
|
|
282
|
+
return this.send((c) => c.createEscrowV2(p.taskKey, p.agent, p.amount, p.paymentAsset, p.termsHash, p.quoteHash, p.inputHash, p.manifestHash, p.deadline, p.confirmationWindow, p.proofVersion), signer);
|
|
283
|
+
}
|
|
284
|
+
async submitProofV2(taskKey, resultHash, manifestHash, proofVersion, signer) {
|
|
285
|
+
return this.send((c) => c.submitProofV2(taskKey, resultHash, manifestHash, proofVersion), signer);
|
|
286
|
+
}
|
|
287
|
+
async releaseV2(taskKey, signer) {
|
|
288
|
+
return this.send((c) => c.releaseV2(taskKey), signer);
|
|
289
|
+
}
|
|
290
|
+
async claimAfterTimeoutV2(taskKey, signer) {
|
|
291
|
+
return this.send((c) => c.claimAfterTimeoutV2(taskKey), signer);
|
|
292
|
+
}
|
|
293
|
+
async disputeV2(taskKey, reasonHash, signer) {
|
|
294
|
+
return this.send((c) => c.disputeV2(taskKey, reasonHash), signer);
|
|
295
|
+
}
|
|
296
|
+
/** 静态调用 (不花 gas 地读 revert reason; F2 门槛类断言用) */
|
|
297
|
+
async staticCallClaimAfterTimeout(taskKey, from) {
|
|
298
|
+
try {
|
|
299
|
+
const data = this.iface.encodeFunctionData('claimAfterTimeoutV2', [taskKey]);
|
|
300
|
+
await this.provider.call({ to: this.escrowAddress, from, data });
|
|
301
|
+
return { ok: true };
|
|
302
|
+
}
|
|
303
|
+
catch (e) {
|
|
304
|
+
return { ok: false, reason: e?.reason || e?.shortMessage || String(e?.message || e).slice(0, 200) };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// ── 5 个 v2 事件监听 ──────────────────────────────────────────────────────
|
|
308
|
+
/** 只认本合约发出的、topic 匹配的日志 (别的合约发出的一律不算) */
|
|
309
|
+
escrowLogsOf(outcome) {
|
|
310
|
+
return (outcome.logs || []).filter((l) => String(l.address).toLowerCase() === this.escrowAddress.toLowerCase());
|
|
311
|
+
}
|
|
312
|
+
/** 把一笔 receipt 的日志里属于本合约的、指定 v2 事件的解码出来 */
|
|
313
|
+
decodeV2Events(outcome, eventName) {
|
|
314
|
+
const out = [];
|
|
315
|
+
for (const log of this.escrowLogsOf(outcome)) {
|
|
316
|
+
let parsed;
|
|
317
|
+
try {
|
|
318
|
+
parsed = this.iface.parseLog({ topics: [...log.topics], data: log.data });
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (!parsed)
|
|
324
|
+
continue;
|
|
325
|
+
if (eventName && parsed.name !== eventName)
|
|
326
|
+
continue;
|
|
327
|
+
const args = {};
|
|
328
|
+
parsed.fragment.inputs.forEach((inp, i) => { args[inp.name] = parsed.args[i]; });
|
|
329
|
+
out.push({ name: parsed.name, args, log });
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
/** 用**我们自己的 Interface** 从原始日志解码 (不依赖 ethers 传参形状) */
|
|
334
|
+
decodeLogArgs(eventName, log) {
|
|
335
|
+
const frag = this.iface.getEvent(eventName);
|
|
336
|
+
if (!frag || !log?.topics)
|
|
337
|
+
return null;
|
|
338
|
+
try {
|
|
339
|
+
const decoded = this.iface.decodeEventLog(frag, log.data, [...log.topics]);
|
|
340
|
+
const args = {};
|
|
341
|
+
frag.inputs.forEach((inp, i) => { args[inp.name] = decoded[i]; });
|
|
342
|
+
return args;
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* 监听某个 v2 事件。可给 taskKey 过滤 (链上 indexed 字段, 在回调里核对)。
|
|
350
|
+
* 返回取消订阅函数 —— 调用方必须能干净地摘掉监听 (否则重启会叠加)。
|
|
351
|
+
*
|
|
352
|
+
* 实现说明 (踩过的坑): 把 `contract.filters.X(...)` 这种 Filter 对象交给
|
|
353
|
+
* `contract.on()` 时, ethers v6 会把 fragment 解析成 null, 于是回调只收到一个
|
|
354
|
+
* ContractEventPayload (位置参数全空)。所以这里用**事件名字符串**注册, 自己按
|
|
355
|
+
* topic0/taskKey 过滤, 并从原始 log 用本地 Interface 解码 —— 参数形状永远一致。
|
|
356
|
+
*/
|
|
357
|
+
onV2Event(eventName, handler, filter) {
|
|
358
|
+
const contract = this.readContract;
|
|
359
|
+
const frag = this.iface.getEvent(eventName);
|
|
360
|
+
if (!frag)
|
|
361
|
+
throw new Error(`AgentEscrow ABI 里没有事件 ${eventName}`);
|
|
362
|
+
const topic0 = String(frag.topicHash).toLowerCase();
|
|
363
|
+
const wantKey = filter?.taskKey ? String(filter.taskKey).toLowerCase() : null;
|
|
364
|
+
const selfAddr = this.escrowAddress.toLowerCase();
|
|
365
|
+
const listener = (...a) => {
|
|
366
|
+
const payload = a[a.length - 1];
|
|
367
|
+
const log = payload?.log ?? (payload?.topics ? payload : undefined);
|
|
368
|
+
// 只认本合约 + 本事件 topic0 (别的合约/别的事件一律不算)
|
|
369
|
+
if (log?.address && String(log.address).toLowerCase() !== selfAddr)
|
|
370
|
+
return;
|
|
371
|
+
if (log?.topics?.[0] && String(log.topics[0]).toLowerCase() !== topic0)
|
|
372
|
+
return;
|
|
373
|
+
// taskKey 过滤 (indexed topic1)
|
|
374
|
+
if (wantKey && log?.topics?.[1] && String(log.topics[1]).toLowerCase() !== wantKey)
|
|
375
|
+
return;
|
|
376
|
+
let args = log ? this.decodeLogArgs(eventName, log) : null;
|
|
377
|
+
if (!args) {
|
|
378
|
+
args = {};
|
|
379
|
+
frag.inputs.forEach((inp, i) => { args[inp.name] = a[i]; });
|
|
380
|
+
if (wantKey && String(args.taskKey || '').toLowerCase() !== wantKey)
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
handler(args, log);
|
|
385
|
+
}
|
|
386
|
+
catch { /* 监听回调不许炸主流程 */ }
|
|
387
|
+
};
|
|
388
|
+
contract.on(eventName, listener); // ethers v6: 返回 Promise, 订阅失败不抛同步异常
|
|
389
|
+
const swallow = (r) => { if (r && typeof r.catch === 'function')
|
|
390
|
+
r.catch(() => { }); };
|
|
391
|
+
return () => { try {
|
|
392
|
+
swallow(contract.off(eventName, listener));
|
|
393
|
+
}
|
|
394
|
+
catch { /* 已摘 */ } };
|
|
395
|
+
}
|
|
396
|
+
onEscrowCreatedV2(h, f) { return this.onV2Event('EscrowCreatedV2', h, f); }
|
|
397
|
+
onProofSubmittedV2(h, f) { return this.onV2Event('ProofSubmittedV2', h, f); }
|
|
398
|
+
onReleasedV2(h, f) { return this.onV2Event('ReleasedV2', h, f); }
|
|
399
|
+
onRefundedV2(h, f) { return this.onV2Event('RefundedV2', h, f); }
|
|
400
|
+
onDisputedV2(h, f) { return this.onV2Event('DisputedV2', h, f); }
|
|
401
|
+
/** 等某个 v2 事件出现 (真等链上, 不猜); 超时 → 明确报超时, 不假装收到 */
|
|
402
|
+
waitForV2Event(eventName, opts = {}) {
|
|
403
|
+
const timeoutMs = opts.timeoutMs ?? 60_000;
|
|
404
|
+
return new Promise((resolve) => {
|
|
405
|
+
let done = false;
|
|
406
|
+
const off = this.onV2Event(eventName, (args, raw) => {
|
|
407
|
+
if (done)
|
|
408
|
+
return;
|
|
409
|
+
if (opts.predicate && !opts.predicate(args))
|
|
410
|
+
return;
|
|
411
|
+
done = true;
|
|
412
|
+
off();
|
|
413
|
+
clearTimeout(timer);
|
|
414
|
+
resolve({ args, raw });
|
|
415
|
+
}, { taskKey: opts.taskKey });
|
|
416
|
+
const timer = setTimeout(() => { if (done)
|
|
417
|
+
return; done = true; off(); resolve(null); }, timeoutMs);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
/** 摘掉所有监听 (重启/收尾用) */
|
|
421
|
+
removeAllListeners() {
|
|
422
|
+
const swallow = (r) => { if (r && typeof r.catch === 'function')
|
|
423
|
+
r.catch(() => { }); };
|
|
424
|
+
try {
|
|
425
|
+
swallow(this.readContract.removeAllListeners());
|
|
426
|
+
}
|
|
427
|
+
catch { /* noop */ }
|
|
428
|
+
try {
|
|
429
|
+
swallow(this.provider.removeAllListeners?.());
|
|
430
|
+
}
|
|
431
|
+
catch { /* noop */ }
|
|
432
|
+
}
|
|
433
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/agents/chain/index.ts — 链桥模块出口 (P3: Bolloon 链桥 / P5: 链上索引器)
|
|
3
|
+
*
|
|
4
|
+
* 七件事的入口:
|
|
5
|
+
* · chain-config — 链配置 / 确认数门槛 / 钱包路径 (env → 本地安全配置 → 报错)
|
|
6
|
+
* · escrow-client — AgentEscrow v2 真链读写 + 5 个 v2 事件监听 (ethers v6)
|
|
7
|
+
* · chain-settlement — 「到底结算了没有」的唯一判定 (修 F5)
|
|
8
|
+
* · chain-wallet — 链上签名的唯一放行闸 + 签名审计
|
|
9
|
+
* · chain-state-store — 落盘 / 重启恢复 / 重组对账
|
|
10
|
+
* · chain-indexer — P5: v2 事件索引器 (分页扫 / 增量 / 去重 / 重组回退 / 全量重建)
|
|
11
|
+
* · chain-index-query — P5: 只读查询 (时间线 / cursor 增量 / 统计 / 索引高度)
|
|
12
|
+
*/
|
|
13
|
+
export * from './chain-config.js';
|
|
14
|
+
export * from './escrow-client.js';
|
|
15
|
+
export * from './chain-settlement.js';
|
|
16
|
+
export * from './chain-wallet.js';
|
|
17
|
+
export * from './chain-state-store.js';
|
|
18
|
+
export * from './onchain-trade.js';
|
|
19
|
+
export * from './chain-indexer.js';
|
|
20
|
+
export * from './chain-index-query.js';
|