@genkaku/protocol 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/dist/bridge.d.ts +46 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +542 -0
- package/dist/chain.d.ts +496 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +763 -0
- package/dist/credit.d.ts +35 -0
- package/dist/credit.d.ts.map +1 -0
- package/dist/credit.js +112 -0
- package/dist/dial.d.ts +43 -0
- package/dist/dial.d.ts.map +1 -0
- package/dist/dial.js +266 -0
- package/dist/directory.d.ts +39 -0
- package/dist/directory.d.ts.map +1 -0
- package/dist/directory.js +149 -0
- package/dist/discovery.d.ts +20 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +58 -0
- package/dist/hello.d.ts +14 -0
- package/dist/hello.d.ts.map +1 -0
- package/dist/hello.js +42 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/keys.d.ts +30 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +126 -0
- package/dist/llmBridge.d.ts +34 -0
- package/dist/llmBridge.d.ts.map +1 -0
- package/dist/llmBridge.js +151 -0
- package/dist/lp.d.ts +31 -0
- package/dist/lp.d.ts.map +1 -0
- package/dist/lp.js +116 -0
- package/dist/modeManager.d.ts +30 -0
- package/dist/modeManager.d.ts.map +1 -0
- package/dist/modeManager.js +81 -0
- package/dist/netconfig.d.ts +11 -0
- package/dist/netconfig.d.ts.map +1 -0
- package/dist/netconfig.js +39 -0
- package/dist/offer.d.ts +70 -0
- package/dist/offer.d.ts.map +1 -0
- package/dist/offer.js +84 -0
- package/dist/ollama.d.ts +53 -0
- package/dist/ollama.d.ts.map +1 -0
- package/dist/ollama.js +162 -0
- package/dist/p2pNode.d.ts +50 -0
- package/dist/p2pNode.d.ts.map +1 -0
- package/dist/p2pNode.js +450 -0
- package/dist/paywall.d.ts +30 -0
- package/dist/paywall.d.ts.map +1 -0
- package/dist/paywall.js +84 -0
- package/dist/publishOptIn.d.ts +15 -0
- package/dist/publishOptIn.d.ts.map +1 -0
- package/dist/publishOptIn.js +34 -0
- package/dist/receipts.d.ts +61 -0
- package/dist/receipts.d.ts.map +1 -0
- package/dist/receipts.js +135 -0
- package/dist/registry.d.ts +19 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +50 -0
- package/dist/rpc.d.ts +24 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +143 -0
- package/dist/status.d.ts +56 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +354 -0
- package/dist/userSession.d.ts +64 -0
- package/dist/userSession.d.ts.map +1 -0
- package/dist/userSession.js +125 -0
- package/dist/workerSession.d.ts +67 -0
- package/dist/workerSession.d.ts.map +1 -0
- package/dist/workerSession.js +65 -0
- package/package.json +56 -3
- package/README.md +0 -3
package/dist/chain.js
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
// GENKAKU: settlement layer bindings (P4.4) — JobEscrow v0.2 on Base Sepolia.
|
|
2
|
+
// Writes are testnet-only until P6: env 'base' (mainnet) write paths throw.
|
|
3
|
+
// Gas is paid by the calling wallet (user → open/settle; worker → claim,
|
|
4
|
+
// self-paid per the locked design in contracts/README.md).
|
|
5
|
+
//
|
|
6
|
+
// Every digest goes through the packed hashing the escrow's `_verify`
|
|
7
|
+
// recovers against, so wire-held receipts verify bit-for-bit on chain.
|
|
8
|
+
// ABI DTOs are `as const` so viem's contract-call typing narrows exactly.
|
|
9
|
+
import { createPublicClient, createWalletClient, decodeEventLog, encodePacked, keccak256, maxUint256, toBytes, } from 'viem';
|
|
10
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
11
|
+
import { baseSepolia } from 'viem/chains';
|
|
12
|
+
import { CONTRACTS } from "./netconfig.js";
|
|
13
|
+
import { rpcTransport } from "./rpc.js";
|
|
14
|
+
import { digestForHex, signDigest } from "./receipts.js";
|
|
15
|
+
export const ESCROW_ABI = [
|
|
16
|
+
{
|
|
17
|
+
name: 'openSession',
|
|
18
|
+
type: 'function',
|
|
19
|
+
stateMutability: 'nonpayable',
|
|
20
|
+
inputs: [
|
|
21
|
+
{ name: 'worker', type: 'address' },
|
|
22
|
+
{ name: 'amount', type: 'uint128' },
|
|
23
|
+
],
|
|
24
|
+
outputs: [{ name: 'id', type: 'uint256' }],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'settle',
|
|
28
|
+
type: 'function',
|
|
29
|
+
stateMutability: 'nonpayable',
|
|
30
|
+
inputs: [
|
|
31
|
+
{ name: 'id', type: 'uint256' },
|
|
32
|
+
{ name: 'receiptHash', type: 'bytes32' },
|
|
33
|
+
{ name: 'spend', type: 'uint128' },
|
|
34
|
+
{ name: 'workerSig', type: 'bytes' },
|
|
35
|
+
],
|
|
36
|
+
outputs: [],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'claim',
|
|
40
|
+
type: 'function',
|
|
41
|
+
stateMutability: 'nonpayable',
|
|
42
|
+
inputs: [],
|
|
43
|
+
outputs: [],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'refund',
|
|
47
|
+
type: 'function',
|
|
48
|
+
stateMutability: 'nonpayable',
|
|
49
|
+
inputs: [{ name: 'id', type: 'uint256' }],
|
|
50
|
+
outputs: [],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'sessions',
|
|
54
|
+
type: 'function',
|
|
55
|
+
stateMutability: 'view',
|
|
56
|
+
inputs: [{ name: 'id', type: 'uint256' }],
|
|
57
|
+
outputs: [
|
|
58
|
+
{ name: 'user', type: 'address' },
|
|
59
|
+
{ name: 'worker', type: 'address' },
|
|
60
|
+
{ name: 'amount', type: 'uint128' },
|
|
61
|
+
{ name: 'openedAt', type: 'uint64' },
|
|
62
|
+
{ name: 'settled', type: 'bool' },
|
|
63
|
+
{ name: 'refunded', type: 'bool' },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'accrued',
|
|
68
|
+
type: 'function',
|
|
69
|
+
stateMutability: 'view',
|
|
70
|
+
inputs: [{ name: 'worker', type: 'address' }],
|
|
71
|
+
outputs: [{ name: 'balance', type: 'uint128' }],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'SessionOpened',
|
|
75
|
+
type: 'event',
|
|
76
|
+
anonymous: false,
|
|
77
|
+
inputs: [
|
|
78
|
+
{ name: 'id', type: 'uint256', indexed: true },
|
|
79
|
+
{ name: 'user', type: 'address', indexed: true },
|
|
80
|
+
{ name: 'worker', type: 'address', indexed: true },
|
|
81
|
+
{ name: 'amount', type: 'uint128', indexed: false },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'SessionSettled',
|
|
86
|
+
type: 'event',
|
|
87
|
+
anonymous: false,
|
|
88
|
+
inputs: [
|
|
89
|
+
{ name: 'id', type: 'uint256', indexed: true },
|
|
90
|
+
{ name: 'worker', type: 'address', indexed: true },
|
|
91
|
+
{ name: 'user', type: 'address', indexed: true },
|
|
92
|
+
{ name: 'spend', type: 'uint128', indexed: false },
|
|
93
|
+
{ name: 'fee', type: 'uint128', indexed: false },
|
|
94
|
+
{ name: 'leftover', type: 'uint128', indexed: false },
|
|
95
|
+
{ name: 'receiptHash', type: 'bytes32', indexed: false },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'SessionRefunded',
|
|
100
|
+
type: 'event',
|
|
101
|
+
anonymous: false,
|
|
102
|
+
inputs: [
|
|
103
|
+
{ name: 'id', type: 'uint256', indexed: true },
|
|
104
|
+
{ name: 'user', type: 'address', indexed: true },
|
|
105
|
+
{ name: 'amount', type: 'uint128', indexed: false },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
/** Read the escrow record a client claims to have funded. The paywall (P4.9d)
|
|
110
|
+
* refuses to serve unless this returns a live session that pays THIS worker. */
|
|
111
|
+
export async function readEscrowSession(publicC, env, id) {
|
|
112
|
+
if (CONTRACTS[env].escrow === '')
|
|
113
|
+
return null;
|
|
114
|
+
const row = await publicC.readContract({
|
|
115
|
+
address: CONTRACTS[env].escrow,
|
|
116
|
+
abi: ESCROW_ABI,
|
|
117
|
+
functionName: 'sessions',
|
|
118
|
+
args: [id],
|
|
119
|
+
authorizationList: undefined,
|
|
120
|
+
});
|
|
121
|
+
const [user, worker, amount, openedAt, settled, refunded] = row;
|
|
122
|
+
if (user === '0x0000000000000000000000000000000000000000')
|
|
123
|
+
return null;
|
|
124
|
+
return { id, user, worker, amount, openedAt: Number(openedAt), settled, refunded };
|
|
125
|
+
}
|
|
126
|
+
/** WorkerRegistry v0.2 (P4.9a) — wallet ⇄ PeerId binding + on-chain directory.
|
|
127
|
+
* Register/advertise writes are worker-key-gas (self-paid); reads are the
|
|
128
|
+
* data-plane compare. The locator lives ONLY in the `WorkerLocated` log, so
|
|
129
|
+
* the directory is rebuilt by scanning logs — never by storage reads. */
|
|
130
|
+
export const REGISTRY_ABI = [
|
|
131
|
+
{
|
|
132
|
+
name: 'register',
|
|
133
|
+
type: 'function',
|
|
134
|
+
stateMutability: 'nonpayable',
|
|
135
|
+
inputs: [
|
|
136
|
+
{ name: 'peerId', type: 'bytes32' },
|
|
137
|
+
{ name: 'policyHash', type: 'bytes32' },
|
|
138
|
+
],
|
|
139
|
+
outputs: [],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'registerWithLocator',
|
|
143
|
+
type: 'function',
|
|
144
|
+
stateMutability: 'nonpayable',
|
|
145
|
+
inputs: [
|
|
146
|
+
{ name: 'peerId', type: 'bytes32' },
|
|
147
|
+
{ name: 'policyHash', type: 'bytes32' },
|
|
148
|
+
{ name: 'locator', type: 'string' },
|
|
149
|
+
],
|
|
150
|
+
outputs: [],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'advertise',
|
|
154
|
+
type: 'function',
|
|
155
|
+
stateMutability: 'nonpayable',
|
|
156
|
+
inputs: [{ name: 'locator', type: 'string' }],
|
|
157
|
+
outputs: [],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'selfDeactivate',
|
|
161
|
+
type: 'function',
|
|
162
|
+
stateMutability: 'nonpayable',
|
|
163
|
+
inputs: [],
|
|
164
|
+
outputs: [],
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'MAX_LOCATOR_BYTES',
|
|
168
|
+
type: 'function',
|
|
169
|
+
stateMutability: 'view',
|
|
170
|
+
inputs: [],
|
|
171
|
+
outputs: [{ name: '', type: 'uint256' }],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'WorkerLocated',
|
|
175
|
+
type: 'event',
|
|
176
|
+
inputs: [
|
|
177
|
+
{ name: 'wallet', type: 'address', indexed: true },
|
|
178
|
+
{ name: 'peerId', type: 'bytes32', indexed: true },
|
|
179
|
+
{ name: 'locator', type: 'string', indexed: false },
|
|
180
|
+
{ name: 'updatedAt', type: 'uint64', indexed: false },
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'WorkerDeactivated',
|
|
185
|
+
type: 'event',
|
|
186
|
+
inputs: [
|
|
187
|
+
{ name: 'wallet', type: 'address', indexed: true },
|
|
188
|
+
{ name: 'reason', type: 'string', indexed: false },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'workers',
|
|
193
|
+
type: 'function',
|
|
194
|
+
stateMutability: 'view',
|
|
195
|
+
inputs: [{ name: '', type: 'address' }],
|
|
196
|
+
outputs: [
|
|
197
|
+
{ name: 'peerId', type: 'bytes32' },
|
|
198
|
+
{ name: 'policyHash', type: 'bytes32' },
|
|
199
|
+
{ name: 'registeredAt', type: 'uint64' },
|
|
200
|
+
{ name: 'lastActiveAt', type: 'uint64' },
|
|
201
|
+
{ name: 'active', type: 'bool' },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'isActive',
|
|
206
|
+
type: 'function',
|
|
207
|
+
stateMutability: 'view',
|
|
208
|
+
inputs: [{ name: 'wallet', type: 'address' }],
|
|
209
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
export const USDC_ABI = [{
|
|
213
|
+
name: 'approve',
|
|
214
|
+
type: 'function',
|
|
215
|
+
stateMutability: 'nonpayable',
|
|
216
|
+
inputs: [
|
|
217
|
+
{ name: 'spender', type: 'address' },
|
|
218
|
+
{ name: 'amount', type: 'uint256' },
|
|
219
|
+
],
|
|
220
|
+
outputs: [{ name: '', type: 'bool' }],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'allowance',
|
|
224
|
+
type: 'function',
|
|
225
|
+
stateMutability: 'view',
|
|
226
|
+
inputs: [
|
|
227
|
+
{ name: 'owner', type: 'address' },
|
|
228
|
+
{ name: 'spender', type: 'address' },
|
|
229
|
+
],
|
|
230
|
+
outputs: [{ name: '', type: 'uint256' }],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'balanceOf',
|
|
234
|
+
type: 'function',
|
|
235
|
+
stateMutability: 'view',
|
|
236
|
+
inputs: [{ name: 'owner', type: 'address' }],
|
|
237
|
+
outputs: [{ name: '', type: 'uint256' }],
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
function viemChain(env) {
|
|
241
|
+
// P6 will add the mainnet chain object. Until then the ONLY wired chain is
|
|
242
|
+
// Base Sepolia — refusing keeps the chain type narrow for viem's contract
|
|
243
|
+
// typing and makes the mainnet write guard structural, not call-site.
|
|
244
|
+
if (env !== 'base-sepolia') {
|
|
245
|
+
throw new Error('[genkaku:chain] mainnet uses canonical USDC and a Safe treasury — locked for P6');
|
|
246
|
+
}
|
|
247
|
+
return baseSepolia;
|
|
248
|
+
}
|
|
249
|
+
/** Public reads. The transport is a failover list (P4.9b): a rate-limited or
|
|
250
|
+
* dead endpoint rolls to the next one without surfacing anything to the user. */
|
|
251
|
+
/** Explicit return type: viem's inferred client type points into its own
|
|
252
|
+
* package-internal modules, which cannot be re-exported from a declaration. */
|
|
253
|
+
export function publicClientFor(env) {
|
|
254
|
+
return createPublicClient({ chain: viemChain(env), transport: rpcTransport(env) });
|
|
255
|
+
}
|
|
256
|
+
/** Local-account wallet client — signs and pays gas itself. Writes go through
|
|
257
|
+
* the same failover list, so a flaky endpoint cannot strand a registration. */
|
|
258
|
+
/** Explicit return type, same reason as {@link publicClientFor}. */
|
|
259
|
+
export function walletClientFor(env, key) {
|
|
260
|
+
return createWalletClient({
|
|
261
|
+
chain: viemChain(env),
|
|
262
|
+
transport: rpcTransport(env),
|
|
263
|
+
account: privateKeyToAccount(key),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// ── digests ────────────────────────────────────────────────────────────────
|
|
267
|
+
/** The worker's settle co-sign digest: keccak256(abi.encodePacked(id,
|
|
268
|
+
* receiptHash, spend)) — an EIP-191 signature over this is what settle's
|
|
269
|
+
* `_verify` checks against the session's WORKER address. */
|
|
270
|
+
export function settleDigest(escrowId, receiptHash, spend) {
|
|
271
|
+
// `spend` is a uint128 in JobEscrow, and abi.encodePacked uses the TYPE's
|
|
272
|
+
// width: packing it as uint256 shifted every byte after it, so the contract
|
|
273
|
+
// recovered a different signer (BadSignature) on every real settlement.
|
|
274
|
+
return toBytes(keccak256(encodePacked(['uint256', 'bytes32', 'uint128'], [escrowId, receiptHash, spend])));
|
|
275
|
+
}
|
|
276
|
+
/** The on-chain `receiptHash` argument: the digest of the worker-held max
|
|
277
|
+
* cumulative receipt (bit-for-bit what both data-plane sides verify). */
|
|
278
|
+
export function receiptHashFor(receipt) {
|
|
279
|
+
return digestForHex(receipt);
|
|
280
|
+
}
|
|
281
|
+
// ── read ───────────────────────────────────────────────────────────────────
|
|
282
|
+
export async function readAccrued(publicC, env, worker) {
|
|
283
|
+
if (CONTRACTS[env].escrow === '') {
|
|
284
|
+
return 0n;
|
|
285
|
+
}
|
|
286
|
+
return await publicC.readContract({
|
|
287
|
+
address: CONTRACTS[env].escrow,
|
|
288
|
+
abi: ESCROW_ABI,
|
|
289
|
+
functionName: 'accrued',
|
|
290
|
+
args: [worker],
|
|
291
|
+
// viem 2.57 types `authorizationList` as `any` but TypeScript 6.0.3
|
|
292
|
+
// promotes it to a REQUIRED position (TS5.9 keeps it optional — verified
|
|
293
|
+
// in /tmp/viem-typecheck). Explicit undefined restores the TS5 shape;
|
|
294
|
+
// it is ignored at runtime for non-EIP-7702 calls.
|
|
295
|
+
authorizationList: undefined,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Read the balances a user needs before opening an escrow: USDC to pay and ETH
|
|
300
|
+
* for gas. A chain without a deployed escrow still has canonical USDC.
|
|
301
|
+
* @param publicC - public client for the selected network.
|
|
302
|
+
* @param env - network whose USDC address is used.
|
|
303
|
+
* @param wallet - account to read.
|
|
304
|
+
* @returns gas and 6-decimal USDC balances.
|
|
305
|
+
*/
|
|
306
|
+
export async function readWalletBalances(publicC, env, wallet) {
|
|
307
|
+
const [eth, usdc] = await Promise.all([
|
|
308
|
+
publicC.getBalance({ address: wallet }),
|
|
309
|
+
publicC.readContract({
|
|
310
|
+
address: CONTRACTS[env].usdc,
|
|
311
|
+
abi: USDC_ABI,
|
|
312
|
+
functionName: 'balanceOf',
|
|
313
|
+
args: [wallet],
|
|
314
|
+
// Same viem 2.57 / TS 6.0 shape workaround as readAccrued above.
|
|
315
|
+
authorizationList: undefined,
|
|
316
|
+
}),
|
|
317
|
+
]);
|
|
318
|
+
return { eth, usdc };
|
|
319
|
+
}
|
|
320
|
+
/** The worker account's own wallet address (P4.5 binds peerId ⇄ wallet via
|
|
321
|
+
* WorkerRegistry; offers self-report it until then). */
|
|
322
|
+
export function walletAddress(key) {
|
|
323
|
+
return privateKeyToAccount(key).address;
|
|
324
|
+
}
|
|
325
|
+
// ── writes ─────────────────────────────────────────────────────────────────
|
|
326
|
+
/** Approve (only when the allowance is short) + open. The numeric escrow id
|
|
327
|
+
* is parsed from SessionOpened's OWN log — never from a counter probe (a
|
|
328
|
+
* concurrent open would desync the id). */
|
|
329
|
+
export async function openEscrow(publicC, walletC, env, worker, amount) {
|
|
330
|
+
const escrow = CONTRACTS[env].escrow;
|
|
331
|
+
const usdc = CONTRACTS[env].usdc;
|
|
332
|
+
if (!escrow) {
|
|
333
|
+
throw new Error(`[genkaku:chain] no escrow deployed for ${env}`);
|
|
334
|
+
}
|
|
335
|
+
const allowance = await publicC.readContract({
|
|
336
|
+
address: usdc,
|
|
337
|
+
abi: USDC_ABI,
|
|
338
|
+
functionName: 'allowance',
|
|
339
|
+
args: [walletC.account.address, escrow],
|
|
340
|
+
// viem 2.57 types `authorizationList` as `any` but TypeScript 6.0.3
|
|
341
|
+
// promotes it to a REQUIRED position (TS5.9 keeps it optional — verified
|
|
342
|
+
// in /tmp/viem-typecheck). Explicit undefined restores the TS5 shape;
|
|
343
|
+
// it is ignored at runtime for non-EIP-7702 calls.
|
|
344
|
+
authorizationList: undefined,
|
|
345
|
+
});
|
|
346
|
+
let approved = allowance;
|
|
347
|
+
if (allowance < amount) {
|
|
348
|
+
const approveHash = await walletC.writeContract({
|
|
349
|
+
address: usdc,
|
|
350
|
+
abi: USDC_ABI,
|
|
351
|
+
functionName: 'approve',
|
|
352
|
+
args: [escrow, maxUint256],
|
|
353
|
+
// viem 2.57 types `authorizationList` as `any` but TypeScript 6.0.3
|
|
354
|
+
// promotes it to a REQUIRED position (TS5.9 keeps it optional — verified
|
|
355
|
+
// in /tmp/viem-typecheck). Explicit undefined restores the TS5 shape;
|
|
356
|
+
// it is ignored at runtime for non-EIP-7702 calls.
|
|
357
|
+
authorizationList: undefined,
|
|
358
|
+
chain: viemChain(env),
|
|
359
|
+
account: walletC.account,
|
|
360
|
+
});
|
|
361
|
+
await publicC.waitForTransactionReceipt({ hash: approveHash });
|
|
362
|
+
approved = maxUint256;
|
|
363
|
+
}
|
|
364
|
+
const openHash = await walletC.writeContract({
|
|
365
|
+
address: escrow,
|
|
366
|
+
abi: ESCROW_ABI,
|
|
367
|
+
functionName: 'openSession',
|
|
368
|
+
args: [worker, amount],
|
|
369
|
+
// viem 2.57 types `authorizationList` as `any` but TypeScript 6.0.3
|
|
370
|
+
// promotes it to a REQUIRED position (TS5.9 keeps it optional — verified
|
|
371
|
+
// in /tmp/viem-typecheck). Explicit undefined restores the TS5 shape;
|
|
372
|
+
// it is ignored at runtime for non-EIP-7702 calls.
|
|
373
|
+
authorizationList: undefined,
|
|
374
|
+
chain: viemChain(env),
|
|
375
|
+
account: walletC.account,
|
|
376
|
+
});
|
|
377
|
+
const rc = await publicC.waitForTransactionReceipt({ hash: openHash });
|
|
378
|
+
const openLog = rc.logs.find(l => l.address.toLowerCase() === escrow.toLowerCase());
|
|
379
|
+
if (openLog == null) {
|
|
380
|
+
throw new Error('[genkaku:chain] open: no SessionOpened log in the open tx');
|
|
381
|
+
}
|
|
382
|
+
// TS6: viem's Log type drops `topics` (same TS6-only narrowing quirk as
|
|
383
|
+
// authorizationList). Bridge through unknown; the log is filtered to the
|
|
384
|
+
// escrow address above, and SessionOpened's args are runtime-guaranteed
|
|
385
|
+
// by the ABI.
|
|
386
|
+
const decoded = decodeEventLog({
|
|
387
|
+
abi: ESCROW_ABI,
|
|
388
|
+
data: openLog.data,
|
|
389
|
+
topics: openLog.topics,
|
|
390
|
+
});
|
|
391
|
+
if (decoded.eventName !== 'SessionOpened') {
|
|
392
|
+
throw new Error(`[genkaku:chain] open: unexpected event ${decoded.eventName}`);
|
|
393
|
+
}
|
|
394
|
+
const id = decoded.args.id;
|
|
395
|
+
if (id == null) {
|
|
396
|
+
throw new Error('[genkaku:chain] open: SessionOpened log without an id');
|
|
397
|
+
}
|
|
398
|
+
return { escrowId: id, approved };
|
|
399
|
+
}
|
|
400
|
+
export async function settleSession(walletC, env, args) {
|
|
401
|
+
return await walletC.writeContract({
|
|
402
|
+
address: CONTRACTS[env].escrow,
|
|
403
|
+
abi: ESCROW_ABI,
|
|
404
|
+
functionName: 'settle',
|
|
405
|
+
args: [args.escrowId, args.receiptHash, args.spend, args.workerSig],
|
|
406
|
+
// viem 2.57 types `authorizationList` as `any` but TypeScript 6.0.3
|
|
407
|
+
// promotes it to a REQUIRED position (TS5.9 keeps it optional — verified
|
|
408
|
+
// in /tmp/viem-typecheck). Explicit undefined restores the TS5 shape;
|
|
409
|
+
// it is ignored at runtime for non-EIP-7702 calls.
|
|
410
|
+
authorizationList: undefined,
|
|
411
|
+
chain: viemChain(env),
|
|
412
|
+
account: walletC.account,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/** Reclaim an unsettled escrow. A short session can bill zero started minutes;
|
|
416
|
+
* without this the funds would sit in the contract until the user found a
|
|
417
|
+
* manual refund, which a non-technical user never will. */
|
|
418
|
+
export async function refundSession(walletC, env, escrowId) {
|
|
419
|
+
return await walletC.writeContract({
|
|
420
|
+
address: CONTRACTS[env].escrow,
|
|
421
|
+
abi: ESCROW_ABI,
|
|
422
|
+
functionName: 'refund',
|
|
423
|
+
args: [escrowId],
|
|
424
|
+
authorizationList: undefined,
|
|
425
|
+
chain: viemChain(env),
|
|
426
|
+
account: walletC.account,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
export async function claimAccrual(walletC, env) {
|
|
430
|
+
return await walletC.writeContract({
|
|
431
|
+
address: CONTRACTS[env].escrow,
|
|
432
|
+
abi: ESCROW_ABI,
|
|
433
|
+
functionName: 'claim',
|
|
434
|
+
args: [],
|
|
435
|
+
// viem 2.57 types `authorizationList` as `any` but TypeScript 6.0.3
|
|
436
|
+
// promotes it to a REQUIRED position (TS5.9 keeps it optional — verified
|
|
437
|
+
// in /tmp/viem-typecheck). Explicit undefined restores the TS5 shape;
|
|
438
|
+
// it is ignored at runtime for non-EIP-7702 calls.
|
|
439
|
+
authorizationList: undefined,
|
|
440
|
+
chain: viemChain(env),
|
|
441
|
+
account: walletC.account,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
// ── worker registry (P4.5: wallet ⇄ PeerId binding) ──────────────────────
|
|
445
|
+
/** The chain view of one worker's registry record (null when never active). */
|
|
446
|
+
export async function readWorker(publicC, env, wallet) {
|
|
447
|
+
if (CONTRACTS[env].registry === '') {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
const w = await publicC.readContract({
|
|
451
|
+
address: CONTRACTS[env].registry,
|
|
452
|
+
abi: REGISTRY_ABI,
|
|
453
|
+
functionName: 'workers',
|
|
454
|
+
args: [wallet],
|
|
455
|
+
// viem 2.57 types `authorizationList` as `any` but TypeScript 6.0.3
|
|
456
|
+
// promotes it to a REQUIRED position (see openEscrow's comment).
|
|
457
|
+
authorizationList: undefined,
|
|
458
|
+
});
|
|
459
|
+
// viem returns the struct as this tuple: peerId, policyHash, registeredAt,
|
|
460
|
+
// lastActiveAt, active.
|
|
461
|
+
const [peerId, policyHash, , , active] = w;
|
|
462
|
+
return active ? { active, peerId, policyHash } : null;
|
|
463
|
+
}
|
|
464
|
+
/** Register (or re-pin) the CALLER's wallet to its PeerId digest + policy.
|
|
465
|
+
* The caller is the worker wallet itself (msg.sender == the binding) — the
|
|
466
|
+
* desktop pays gas from the worker key. Testnet-only (viemChain refuses
|
|
467
|
+
* mainnet until P6). */
|
|
468
|
+
export async function registerWorker(walletC, env, args) {
|
|
469
|
+
if (CONTRACTS[env].registry === '') {
|
|
470
|
+
throw new Error(`[genkaku:chain] no registry deployed for ${env}`);
|
|
471
|
+
}
|
|
472
|
+
const txHash = await walletC.writeContract({
|
|
473
|
+
address: CONTRACTS[env].registry,
|
|
474
|
+
abi: REGISTRY_ABI,
|
|
475
|
+
functionName: 'register',
|
|
476
|
+
args: [args.peerIdDigest, args.policyHash],
|
|
477
|
+
// viem 2.57 types `authorizationList` as `any` but TypeScript 6.0.3
|
|
478
|
+
// promotes it to a REQUIRED position (see openEscrow's comment).
|
|
479
|
+
authorizationList: undefined,
|
|
480
|
+
chain: viemChain(env),
|
|
481
|
+
account: walletC.account,
|
|
482
|
+
});
|
|
483
|
+
await publicClientFor(env).waitForTransactionReceipt({ hash: txHash });
|
|
484
|
+
return { txHash };
|
|
485
|
+
}
|
|
486
|
+
/** First block that can hold a v0.2 directory entry (the scan floor). */
|
|
487
|
+
export const REGISTRY_DEPLOY_BLOCK = {
|
|
488
|
+
'base-sepolia': 55900292n, // DeployRegistryV2, 2026-11-08
|
|
489
|
+
base: 0n, // set at mainnet launch
|
|
490
|
+
};
|
|
491
|
+
/** First block that can hold a v0.2 escrow session (same deployment batch). */
|
|
492
|
+
export const ESCROW_DEPLOY_BLOCK = {
|
|
493
|
+
'base-sepolia': 55900292n,
|
|
494
|
+
base: 0n,
|
|
495
|
+
};
|
|
496
|
+
/** The contract's locator ceiling (mirrors MAX_LOCATOR_BYTES). */
|
|
497
|
+
export const MAX_LOCATOR_BYTES = 256;
|
|
498
|
+
/** Four addresses is enough for LAN + public + relay without paying for more. */
|
|
499
|
+
export const MAX_LOCATOR_ADDRS = 4;
|
|
500
|
+
/** Build the on-chain locator from the addresses libp2p actually announces.
|
|
501
|
+
* Loopback is dropped (useless to a remote peer) and the list is capped so it
|
|
502
|
+
* always fits MAX_LOCATOR_BYTES. Public addrs come first: a client that can
|
|
503
|
+
* only use one should try the routable one. Separator is `;` — a multiaddr
|
|
504
|
+
* never contains it, so parsing stays unambiguous. */
|
|
505
|
+
export function locatorOf(addrs, maxBytes = MAX_LOCATOR_BYTES, maxCount = MAX_LOCATOR_ADDRS) {
|
|
506
|
+
const clean = [...new Set(addrs)]
|
|
507
|
+
.filter(a => a.startsWith('/'))
|
|
508
|
+
.filter(a => !a.startsWith('/ip4/127.') && !a.startsWith('/ip6/::1'))
|
|
509
|
+
.map(a => a.trim())
|
|
510
|
+
.filter(a => a.length > 0);
|
|
511
|
+
const rank = (a) => {
|
|
512
|
+
if (a.includes('/p2p-circuit/'))
|
|
513
|
+
return 2;
|
|
514
|
+
if (/^\/ip4\/(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(a))
|
|
515
|
+
return 1;
|
|
516
|
+
if (/^\/ip6\/(fc|fd|fe80)/.test(a))
|
|
517
|
+
return 1;
|
|
518
|
+
return 0;
|
|
519
|
+
};
|
|
520
|
+
const ordered = clean
|
|
521
|
+
.map((addr, index) => ({ addr, index, rank: rank(addr) }))
|
|
522
|
+
.sort((a, b) => a.rank - b.rank || a.index - b.index)
|
|
523
|
+
.map(x => x.addr);
|
|
524
|
+
const picked = [];
|
|
525
|
+
let bytes = 0;
|
|
526
|
+
for (const addr of ordered) {
|
|
527
|
+
if (picked.length >= maxCount)
|
|
528
|
+
break;
|
|
529
|
+
const extra = addr.length + (picked.length > 0 ? 1 : 0);
|
|
530
|
+
if (bytes + extra > maxBytes)
|
|
531
|
+
continue;
|
|
532
|
+
picked.push(addr);
|
|
533
|
+
bytes += extra;
|
|
534
|
+
}
|
|
535
|
+
return picked.join(';');
|
|
536
|
+
}
|
|
537
|
+
/** True when this address is reachable from the public internet. LAN, loopback,
|
|
538
|
+
* link-local and CGNAT (100.64/10) ranges are local-only; a relay address is
|
|
539
|
+
* signaling, not a data path (P4.9g). A user-supplied DNS name is trusted. */
|
|
540
|
+
export function isPublicAddr(addr) {
|
|
541
|
+
if (!addr.startsWith('/'))
|
|
542
|
+
return false;
|
|
543
|
+
if (addr.includes('/p2p-circuit/'))
|
|
544
|
+
return false;
|
|
545
|
+
const v4 = /^\/ip4\/(\d+)\.(\d+)\.(\d+)\.(\d+)\//.exec(addr);
|
|
546
|
+
if (v4 != null) {
|
|
547
|
+
const a = Number(v4[1]);
|
|
548
|
+
const b = Number(v4[2]);
|
|
549
|
+
if (a === 0 || a === 10 || a === 127)
|
|
550
|
+
return false;
|
|
551
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
552
|
+
return false;
|
|
553
|
+
if (a === 192 && b === 168)
|
|
554
|
+
return false;
|
|
555
|
+
if (a === 169 && b === 254)
|
|
556
|
+
return false;
|
|
557
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
558
|
+
return false;
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
const v6 = /^\/ip6\/([0-9a-f:]+)\//i.exec(addr);
|
|
562
|
+
if (v6 != null) {
|
|
563
|
+
const host = v6[1].toLowerCase();
|
|
564
|
+
if (host === '::1')
|
|
565
|
+
return false;
|
|
566
|
+
if (host.startsWith('fc') || host.startsWith('fd') || host.startsWith('fe80'))
|
|
567
|
+
return false;
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
return /^\/dns(4|6|addr)\//.test(addr);
|
|
571
|
+
}
|
|
572
|
+
/** Does this peer announce any address the global directory can use? Without
|
|
573
|
+
* one, on-chain presence only advertises a dead end (P4.9g §39). */
|
|
574
|
+
export function hasPublicAddr(addrs) {
|
|
575
|
+
return addrs.some(isPublicAddr);
|
|
576
|
+
}
|
|
577
|
+
/** Split a locator back into multiaddrs (tolerates `;`, newline, whitespace). */
|
|
578
|
+
export function parseLocator(locator) {
|
|
579
|
+
return locator
|
|
580
|
+
.split(/[;\n]+/)
|
|
581
|
+
.map(s => s.trim())
|
|
582
|
+
.filter(s => s.startsWith('/'));
|
|
583
|
+
}
|
|
584
|
+
/** Register (or re-pin) AND publish the locator in one transaction. */
|
|
585
|
+
export async function registerWorkerWithLocator(walletC, env, args) {
|
|
586
|
+
if (CONTRACTS[env].registry === '') {
|
|
587
|
+
throw new Error(`[genkaku:chain] no registry deployed for ${env}`);
|
|
588
|
+
}
|
|
589
|
+
const txHash = await walletC.writeContract({
|
|
590
|
+
address: CONTRACTS[env].registry,
|
|
591
|
+
abi: REGISTRY_ABI,
|
|
592
|
+
functionName: 'registerWithLocator',
|
|
593
|
+
args: [args.peerIdDigest, args.policyHash, args.locator],
|
|
594
|
+
authorizationList: undefined,
|
|
595
|
+
chain: viemChain(env),
|
|
596
|
+
account: walletC.account,
|
|
597
|
+
});
|
|
598
|
+
await publicClientFor(env).waitForTransactionReceipt({ hash: txHash });
|
|
599
|
+
return txHash;
|
|
600
|
+
}
|
|
601
|
+
/** Publish a new locator for an already-registered worker. Only call this when
|
|
602
|
+
* the announced address set actually changed — each call is a chain write. */
|
|
603
|
+
export async function advertiseWorker(walletC, env, locator) {
|
|
604
|
+
if (CONTRACTS[env].registry === '') {
|
|
605
|
+
throw new Error(`[genkaku:chain] no registry deployed for ${env}`);
|
|
606
|
+
}
|
|
607
|
+
const txHash = await walletC.writeContract({
|
|
608
|
+
address: CONTRACTS[env].registry,
|
|
609
|
+
abi: REGISTRY_ABI,
|
|
610
|
+
functionName: 'advertise',
|
|
611
|
+
args: [locator],
|
|
612
|
+
authorizationList: undefined,
|
|
613
|
+
chain: viemChain(env),
|
|
614
|
+
account: walletC.account,
|
|
615
|
+
});
|
|
616
|
+
await publicClientFor(env).waitForTransactionReceipt({ hash: txHash });
|
|
617
|
+
return txHash;
|
|
618
|
+
}
|
|
619
|
+
/** Remove this wallet from the live directory. Used when a worker loses the
|
|
620
|
+
* public address it advertised — a stale entry is worse than none. */
|
|
621
|
+
export async function deactivateWorker(walletC, env) {
|
|
622
|
+
if (CONTRACTS[env].registry === '') {
|
|
623
|
+
throw new Error(`[genkaku:chain] no registry deployed for ${env}`);
|
|
624
|
+
}
|
|
625
|
+
const txHash = await walletC.writeContract({
|
|
626
|
+
address: CONTRACTS[env].registry,
|
|
627
|
+
abi: REGISTRY_ABI,
|
|
628
|
+
functionName: 'selfDeactivate',
|
|
629
|
+
args: [],
|
|
630
|
+
authorizationList: undefined,
|
|
631
|
+
chain: viemChain(env),
|
|
632
|
+
account: walletC.account,
|
|
633
|
+
});
|
|
634
|
+
await publicClientFor(env).waitForTransactionReceipt({ hash: txHash });
|
|
635
|
+
return txHash;
|
|
636
|
+
}
|
|
637
|
+
function registryEvent(name) {
|
|
638
|
+
const found = REGISTRY_ABI.find(entry => entry.type === 'event' && entry.name === name);
|
|
639
|
+
if (found == null || found.type !== 'event') {
|
|
640
|
+
throw new Error('[genkaku:chain] registry ABI is missing event ' + name);
|
|
641
|
+
}
|
|
642
|
+
return found;
|
|
643
|
+
}
|
|
644
|
+
/** Walk a block range, halving it whenever the RPC refuses the span.
|
|
645
|
+
* Public Base endpoints disagree wildly on `eth_getLogs` limits (some reject
|
|
646
|
+
* even a single block), so the scan floor is discovered at runtime instead of
|
|
647
|
+
* assumed. */
|
|
648
|
+
export async function collectLogs(publicC, address, event, fromBlock, toBlock, args, depth = 0) {
|
|
649
|
+
try {
|
|
650
|
+
// viem's getLogs overloads narrow on the literal event object; the event
|
|
651
|
+
// here is chosen at runtime from REGISTRY_ABI, so the generic cannot be
|
|
652
|
+
// inferred and the call is widened once, here.
|
|
653
|
+
const client = publicC;
|
|
654
|
+
return await client.getLogs({ address, event, args, fromBlock, toBlock });
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
if (fromBlock >= toBlock || depth > 24)
|
|
658
|
+
throw error;
|
|
659
|
+
const mid = fromBlock + (toBlock - fromBlock) / 2n;
|
|
660
|
+
const [left, right] = await Promise.all([
|
|
661
|
+
collectLogs(publicC, address, event, fromBlock, mid, args, depth + 1),
|
|
662
|
+
collectLogs(publicC, address, event, mid + 1n, toBlock, args, depth + 1),
|
|
663
|
+
]);
|
|
664
|
+
return [...left, ...right];
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/** Rebuild the worker directory from chain logs — the whole point of P4.9a.
|
|
668
|
+
* Returns only entries still live: a wallet whose LAST directory-relevant
|
|
669
|
+
* event is `WorkerDeactivated` is dropped (the tombstone stays in history,
|
|
670
|
+
* which is exactly what an auditor needs). */
|
|
671
|
+
export async function readWorkerLocators(publicC, env, opts = {}) {
|
|
672
|
+
const registry = CONTRACTS[env].registry;
|
|
673
|
+
if (CONTRACTS[env].registry === '')
|
|
674
|
+
return [];
|
|
675
|
+
const latest = opts.toBlock ?? (await publicC.getBlockNumber());
|
|
676
|
+
const floor = opts.fromBlock ?? REGISTRY_DEPLOY_BLOCK[env];
|
|
677
|
+
if (latest < floor)
|
|
678
|
+
return [];
|
|
679
|
+
const chunkSize = opts.chunkSize ?? 50000n;
|
|
680
|
+
const from = floor;
|
|
681
|
+
const to = latest;
|
|
682
|
+
const locatedEvent = registryEvent('WorkerLocated');
|
|
683
|
+
const deactivatedEvent = registryEvent('WorkerDeactivated');
|
|
684
|
+
const located = [];
|
|
685
|
+
const tombstone = new Map();
|
|
686
|
+
for (let start = from; start <= to; start += chunkSize) {
|
|
687
|
+
const end = start + chunkSize - 1n > to ? to : start + chunkSize - 1n;
|
|
688
|
+
const walletFilter = opts.wallet != null ? { wallet: opts.wallet } : undefined;
|
|
689
|
+
const [locatedLogs, deadLogs] = await Promise.all([
|
|
690
|
+
collectLogs(publicC, registry, locatedEvent, start, end, walletFilter),
|
|
691
|
+
collectLogs(publicC, registry, deactivatedEvent, start, end, walletFilter),
|
|
692
|
+
]);
|
|
693
|
+
for (const log of locatedLogs) {
|
|
694
|
+
const a = log.args;
|
|
695
|
+
located.push({
|
|
696
|
+
wallet: a.wallet,
|
|
697
|
+
peerId: a.peerId,
|
|
698
|
+
locator: a.locator,
|
|
699
|
+
updatedAt: Number(a.updatedAt),
|
|
700
|
+
blockNumber: log.blockNumber ?? 0n,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
for (const log of deadLogs) {
|
|
704
|
+
const a = log.args;
|
|
705
|
+
const key = a.wallet.toLowerCase();
|
|
706
|
+
const block = log.blockNumber ?? 0n;
|
|
707
|
+
const prev = tombstone.get(key);
|
|
708
|
+
if (prev == null || block > prev)
|
|
709
|
+
tombstone.set(key, block);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// newest locator per wallet, then drop wallets whose last event was a death
|
|
713
|
+
const newest = new Map();
|
|
714
|
+
for (const entry of located) {
|
|
715
|
+
const key = entry.wallet.toLowerCase();
|
|
716
|
+
const prev = newest.get(key);
|
|
717
|
+
if (prev == null || entry.blockNumber > prev.blockNumber
|
|
718
|
+
|| (entry.blockNumber === prev.blockNumber && entry.updatedAt >= prev.updatedAt)) {
|
|
719
|
+
newest.set(key, entry);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const out = [];
|
|
723
|
+
for (const [key, entry] of newest) {
|
|
724
|
+
const deadAt = tombstone.get(key);
|
|
725
|
+
if (deadAt != null && deadAt > entry.blockNumber)
|
|
726
|
+
continue;
|
|
727
|
+
out.push({
|
|
728
|
+
wallet: entry.wallet,
|
|
729
|
+
peerId: entry.peerId,
|
|
730
|
+
locator: entry.locator,
|
|
731
|
+
addrs: parseLocator(entry.locator),
|
|
732
|
+
updatedAt: entry.updatedAt,
|
|
733
|
+
blockNumber: entry.blockNumber,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return out.sort((a, b) => (a.blockNumber === b.blockNumber ? 0 : a.blockNumber < b.blockNumber ? 1 : -1));
|
|
737
|
+
}
|
|
738
|
+
/** Wallets that tombstoned themselves in a block range. Incremental readers
|
|
739
|
+
* need these separately: `readWorkerLocators` only reports live entries for
|
|
740
|
+
* the range it scanned, so a cached entry must be evicted by a later death. */
|
|
741
|
+
export async function readWorkerDeactivations(publicC, env, opts) {
|
|
742
|
+
const registry = CONTRACTS[env].registry;
|
|
743
|
+
if (CONTRACTS[env].registry === '')
|
|
744
|
+
return [];
|
|
745
|
+
const to = opts.toBlock ?? (await publicC.getBlockNumber());
|
|
746
|
+
const chunkSize = opts.chunkSize ?? 50000n;
|
|
747
|
+
const event = registryEvent('WorkerDeactivated');
|
|
748
|
+
const out = [];
|
|
749
|
+
for (let start = opts.fromBlock; start <= to; start += chunkSize) {
|
|
750
|
+
const end = start + chunkSize - 1n > to ? to : start + chunkSize - 1n;
|
|
751
|
+
const logs = await collectLogs(publicC, registry, event, start, end);
|
|
752
|
+
for (const log of logs) {
|
|
753
|
+
const a = log.args;
|
|
754
|
+
out.push({ wallet: a.wallet, blockNumber: log.blockNumber ?? 0n });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return out;
|
|
758
|
+
}
|
|
759
|
+
/** Receipt signing re-export (single crypto door for callers). */
|
|
760
|
+
export async function signReceiptPayload(key, receipt) {
|
|
761
|
+
return await signDigest(key, toBytes(digestForHex(receipt)));
|
|
762
|
+
}
|
|
763
|
+
//# sourceMappingURL=chain.js.map
|