@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bridge.d.ts +46 -0
  3. package/dist/bridge.d.ts.map +1 -0
  4. package/dist/bridge.js +542 -0
  5. package/dist/chain.d.ts +496 -0
  6. package/dist/chain.d.ts.map +1 -0
  7. package/dist/chain.js +763 -0
  8. package/dist/credit.d.ts +35 -0
  9. package/dist/credit.d.ts.map +1 -0
  10. package/dist/credit.js +112 -0
  11. package/dist/dial.d.ts +43 -0
  12. package/dist/dial.d.ts.map +1 -0
  13. package/dist/dial.js +266 -0
  14. package/dist/directory.d.ts +39 -0
  15. package/dist/directory.d.ts.map +1 -0
  16. package/dist/directory.js +149 -0
  17. package/dist/discovery.d.ts +20 -0
  18. package/dist/discovery.d.ts.map +1 -0
  19. package/dist/discovery.js +58 -0
  20. package/dist/hello.d.ts +14 -0
  21. package/dist/hello.d.ts.map +1 -0
  22. package/dist/hello.js +42 -0
  23. package/dist/index.d.ts +32 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +32 -0
  26. package/dist/keys.d.ts +30 -0
  27. package/dist/keys.d.ts.map +1 -0
  28. package/dist/keys.js +126 -0
  29. package/dist/llmBridge.d.ts +34 -0
  30. package/dist/llmBridge.d.ts.map +1 -0
  31. package/dist/llmBridge.js +151 -0
  32. package/dist/lp.d.ts +31 -0
  33. package/dist/lp.d.ts.map +1 -0
  34. package/dist/lp.js +116 -0
  35. package/dist/modeManager.d.ts +30 -0
  36. package/dist/modeManager.d.ts.map +1 -0
  37. package/dist/modeManager.js +81 -0
  38. package/dist/netconfig.d.ts +11 -0
  39. package/dist/netconfig.d.ts.map +1 -0
  40. package/dist/netconfig.js +39 -0
  41. package/dist/offer.d.ts +70 -0
  42. package/dist/offer.d.ts.map +1 -0
  43. package/dist/offer.js +84 -0
  44. package/dist/ollama.d.ts +53 -0
  45. package/dist/ollama.d.ts.map +1 -0
  46. package/dist/ollama.js +162 -0
  47. package/dist/p2pNode.d.ts +50 -0
  48. package/dist/p2pNode.d.ts.map +1 -0
  49. package/dist/p2pNode.js +450 -0
  50. package/dist/paywall.d.ts +30 -0
  51. package/dist/paywall.d.ts.map +1 -0
  52. package/dist/paywall.js +84 -0
  53. package/dist/publishOptIn.d.ts +15 -0
  54. package/dist/publishOptIn.d.ts.map +1 -0
  55. package/dist/publishOptIn.js +34 -0
  56. package/dist/receipts.d.ts +61 -0
  57. package/dist/receipts.d.ts.map +1 -0
  58. package/dist/receipts.js +135 -0
  59. package/dist/registry.d.ts +19 -0
  60. package/dist/registry.d.ts.map +1 -0
  61. package/dist/registry.js +50 -0
  62. package/dist/rpc.d.ts +24 -0
  63. package/dist/rpc.d.ts.map +1 -0
  64. package/dist/rpc.js +143 -0
  65. package/dist/status.d.ts +56 -0
  66. package/dist/status.d.ts.map +1 -0
  67. package/dist/status.js +354 -0
  68. package/dist/userSession.d.ts +64 -0
  69. package/dist/userSession.d.ts.map +1 -0
  70. package/dist/userSession.js +125 -0
  71. package/dist/workerSession.d.ts +67 -0
  72. package/dist/workerSession.d.ts.map +1 -0
  73. package/dist/workerSession.js +65 -0
  74. package/package.json +56 -3
  75. 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