@antseed/node 0.1.0 → 0.1.2

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 (140) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +7 -5
  3. package/dist/discovery/http-metadata-resolver.d.ts +6 -0
  4. package/dist/discovery/http-metadata-resolver.d.ts.map +1 -1
  5. package/dist/discovery/http-metadata-resolver.js +32 -4
  6. package/dist/discovery/http-metadata-resolver.js.map +1 -1
  7. package/dist/discovery/peer-lookup.d.ts +1 -0
  8. package/dist/discovery/peer-lookup.d.ts.map +1 -1
  9. package/dist/discovery/peer-lookup.js +10 -25
  10. package/dist/discovery/peer-lookup.js.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/interfaces/seller-provider.d.ts +13 -1
  16. package/dist/interfaces/seller-provider.d.ts.map +1 -1
  17. package/dist/node.d.ts +13 -3
  18. package/dist/node.d.ts.map +1 -1
  19. package/dist/node.js +146 -21
  20. package/dist/node.js.map +1 -1
  21. package/dist/proxy/proxy-mux.d.ts +3 -1
  22. package/dist/proxy/proxy-mux.d.ts.map +1 -1
  23. package/dist/proxy/proxy-mux.js +9 -5
  24. package/dist/proxy/proxy-mux.js.map +1 -1
  25. package/dist/types/http.d.ts +1 -0
  26. package/dist/types/http.d.ts.map +1 -1
  27. package/dist/types/http.js +1 -1
  28. package/dist/types/http.js.map +1 -1
  29. package/package.json +14 -10
  30. package/contracts/AntseedEscrow.sol +0 -310
  31. package/contracts/MockUSDC.sol +0 -64
  32. package/contracts/README.md +0 -102
  33. package/src/config/encryption.test.ts +0 -49
  34. package/src/config/encryption.ts +0 -53
  35. package/src/config/plugin-config-manager.test.ts +0 -92
  36. package/src/config/plugin-config-manager.ts +0 -153
  37. package/src/config/plugin-loader.ts +0 -90
  38. package/src/discovery/announcer.ts +0 -169
  39. package/src/discovery/bootstrap.ts +0 -57
  40. package/src/discovery/default-metadata-resolver.ts +0 -18
  41. package/src/discovery/dht-health.ts +0 -136
  42. package/src/discovery/dht-node.ts +0 -191
  43. package/src/discovery/http-metadata-resolver.ts +0 -47
  44. package/src/discovery/index.ts +0 -15
  45. package/src/discovery/metadata-codec.ts +0 -453
  46. package/src/discovery/metadata-resolver.ts +0 -7
  47. package/src/discovery/metadata-server.ts +0 -73
  48. package/src/discovery/metadata-validator.ts +0 -172
  49. package/src/discovery/peer-lookup.ts +0 -122
  50. package/src/discovery/peer-metadata.ts +0 -34
  51. package/src/discovery/peer-selector.ts +0 -134
  52. package/src/discovery/profile-manager.ts +0 -131
  53. package/src/discovery/profile-search.ts +0 -100
  54. package/src/discovery/reputation-verifier.ts +0 -54
  55. package/src/index.ts +0 -61
  56. package/src/interfaces/buyer-router.ts +0 -21
  57. package/src/interfaces/plugin.ts +0 -36
  58. package/src/interfaces/seller-provider.ts +0 -81
  59. package/src/metering/index.ts +0 -6
  60. package/src/metering/receipt-generator.ts +0 -105
  61. package/src/metering/receipt-verifier.ts +0 -102
  62. package/src/metering/session-tracker.ts +0 -145
  63. package/src/metering/storage.ts +0 -600
  64. package/src/metering/token-counter.ts +0 -127
  65. package/src/metering/usage-aggregator.ts +0 -236
  66. package/src/node.ts +0 -1698
  67. package/src/p2p/connection-auth.ts +0 -152
  68. package/src/p2p/connection-manager.ts +0 -916
  69. package/src/p2p/handshake.ts +0 -162
  70. package/src/p2p/ice-config.ts +0 -59
  71. package/src/p2p/identity.ts +0 -110
  72. package/src/p2p/index.ts +0 -11
  73. package/src/p2p/keepalive.ts +0 -118
  74. package/src/p2p/message-protocol.ts +0 -171
  75. package/src/p2p/nat-traversal.ts +0 -169
  76. package/src/p2p/payment-codec.ts +0 -165
  77. package/src/p2p/payment-mux.ts +0 -153
  78. package/src/p2p/reconnect.ts +0 -117
  79. package/src/payments/balance-manager.ts +0 -77
  80. package/src/payments/buyer-payment-manager.ts +0 -414
  81. package/src/payments/disputes.ts +0 -72
  82. package/src/payments/evm/escrow-client.ts +0 -263
  83. package/src/payments/evm/keypair.ts +0 -31
  84. package/src/payments/evm/signatures.ts +0 -103
  85. package/src/payments/evm/wallet.ts +0 -42
  86. package/src/payments/index.ts +0 -50
  87. package/src/payments/settlement.ts +0 -40
  88. package/src/payments/types.ts +0 -79
  89. package/src/proxy/index.ts +0 -3
  90. package/src/proxy/provider-detection.ts +0 -78
  91. package/src/proxy/proxy-mux.ts +0 -173
  92. package/src/proxy/request-codec.ts +0 -294
  93. package/src/reputation/index.ts +0 -6
  94. package/src/reputation/rating-manager.ts +0 -118
  95. package/src/reputation/report-manager.ts +0 -91
  96. package/src/reputation/trust-engine.ts +0 -120
  97. package/src/reputation/trust-score.ts +0 -74
  98. package/src/reputation/uptime-tracker.ts +0 -155
  99. package/src/routing/default-router.ts +0 -75
  100. package/src/types/bittorrent-dht.d.ts +0 -19
  101. package/src/types/buyer.ts +0 -37
  102. package/src/types/capability.ts +0 -34
  103. package/src/types/connection.ts +0 -29
  104. package/src/types/http.ts +0 -20
  105. package/src/types/index.ts +0 -14
  106. package/src/types/metering.ts +0 -175
  107. package/src/types/nat-api.d.ts +0 -29
  108. package/src/types/peer-profile.ts +0 -25
  109. package/src/types/peer.ts +0 -62
  110. package/src/types/plugin-config.ts +0 -31
  111. package/src/types/protocol.ts +0 -162
  112. package/src/types/provider.ts +0 -40
  113. package/src/types/rating.ts +0 -23
  114. package/src/types/report.ts +0 -30
  115. package/src/types/seller.ts +0 -38
  116. package/src/types/staking.ts +0 -23
  117. package/src/utils/debug.ts +0 -30
  118. package/src/utils/hex.ts +0 -14
  119. package/tests/balance-manager.test.ts +0 -156
  120. package/tests/bootstrap.test.ts +0 -108
  121. package/tests/buyer-payment-manager.test.ts +0 -358
  122. package/tests/connection-auth.test.ts +0 -87
  123. package/tests/default-router.test.ts +0 -148
  124. package/tests/evm-keypair.test.ts +0 -173
  125. package/tests/identity.test.ts +0 -133
  126. package/tests/message-protocol.test.ts +0 -212
  127. package/tests/metadata-codec.test.ts +0 -165
  128. package/tests/metadata-validator.test.ts +0 -261
  129. package/tests/metering-storage.test.ts +0 -244
  130. package/tests/payment-codec.test.ts +0 -95
  131. package/tests/payment-mux.test.ts +0 -191
  132. package/tests/peer-selector.test.ts +0 -184
  133. package/tests/provider-detection.test.ts +0 -107
  134. package/tests/proxy-mux-security.test.ts +0 -38
  135. package/tests/receipt.test.ts +0 -215
  136. package/tests/reputation-integration.test.ts +0 -195
  137. package/tests/request-codec.test.ts +0 -144
  138. package/tests/token-counter.test.ts +0 -122
  139. package/tsconfig.json +0 -9
  140. package/vitest.config.ts +0 -7
package/src/node.ts DELETED
@@ -1,1698 +0,0 @@
1
- import { EventEmitter } from "node:events";
2
- import { createHash, randomUUID } from "node:crypto";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
-
6
- import type { Identity } from "./p2p/identity.js";
7
- import { loadOrCreateIdentity } from "./p2p/identity.js";
8
- import type { PeerId } from "./types/peer.js";
9
- import type { PeerInfo } from "./types/peer.js";
10
- import type { SerializedHttpRequest, SerializedHttpResponse } from "./types/http.js";
11
- import type { ConnectionConfig } from "./types/connection.js";
12
- import type { MeteringEvent, SessionMetrics } from "./types/metering.js";
13
- import { MeteringStorage } from "./metering/storage.js";
14
- import { ReceiptGenerator } from "./metering/receipt-generator.js";
15
- import { ConnectionState } from "./types/connection.js";
16
- import {
17
- DHTNode,
18
- DEFAULT_DHT_CONFIG,
19
- type DHTNodeConfig,
20
- } from "./discovery/dht-node.js";
21
- import { toBootstrapConfig, OFFICIAL_BOOTSTRAP_NODES } from "./discovery/bootstrap.js";
22
- import {
23
- ConnectionManager,
24
- PeerConnection,
25
- } from "./p2p/connection-manager.js";
26
- import {
27
- PeerAnnouncer,
28
- type AnnouncerConfig,
29
- } from "./discovery/announcer.js";
30
- import {
31
- PeerLookup,
32
- DEFAULT_LOOKUP_CONFIG,
33
- type LookupConfig,
34
- type LookupResult,
35
- } from "./discovery/peer-lookup.js";
36
- import { HttpMetadataResolver } from "./discovery/http-metadata-resolver.js";
37
- import { ProxyMux } from "./proxy/proxy-mux.js";
38
- import { PaymentMux } from "./p2p/payment-mux.js";
39
- import { FrameDecoder } from "./p2p/message-protocol.js";
40
- import type { Provider, TaskRequest, TaskEvent, SkillRequest, SkillResponse } from "./interfaces/seller-provider.js";
41
- import type { Router } from "./interfaces/buyer-router.js";
42
- import { NatTraversal } from "./p2p/nat-traversal.js";
43
- import { signUtf8Ed25519 } from "./p2p/identity.js";
44
- import { verifyMessage, getBytes } from "ethers";
45
- import {
46
- BalanceManager,
47
- type PaymentConfig,
48
- type PaymentMethod,
49
- BaseEscrowClient,
50
- identityToEvmWallet,
51
- buildLockMessageHash,
52
- buildReceiptMessage,
53
- buildAckMessage,
54
- signMessageEd25519,
55
- verifyMessageEd25519,
56
- } from "./payments/index.js";
57
- import type {
58
- SessionLockAuthPayload,
59
- BuyerAckPayload,
60
- SessionEndPayload,
61
- TopUpAuthPayload,
62
- } from "./types/protocol.js";
63
- import { hexToBytes, bytesToHex } from "./utils/hex.js";
64
- import { debugLog, debugWarn } from "./utils/debug.js";
65
- import { BuyerPaymentManager, type BuyerPaymentConfig } from "./payments/buyer-payment-manager.js";
66
- import { identityToEvmAddress } from "./payments/evm/keypair.js";
67
-
68
- export type { Provider, TaskRequest, TaskEvent, SkillRequest, SkillResponse };
69
- export type { Router };
70
- export type { BuyerPaymentConfig };
71
-
72
- export interface NodePaymentsConfig {
73
- /** Enable seller-side payment channels and automatic settlement. */
74
- enabled?: boolean;
75
- /** Payment method used for settlement. Default: "crypto" */
76
- paymentMethod?: PaymentMethod;
77
- /** Platform fee rate in [0,1]. Default: 0.05 */
78
- platformFeeRate?: number;
79
- /** Idle time before a session is finalized and settled. Default: 30000ms */
80
- settlementIdleMs?: number;
81
- /** Default escrow amount in USDC units. Default: "1" */
82
- defaultEscrowAmountUSDC?: string;
83
- /** Optional seller wallet address for auto-funded escrow deposit. */
84
- sellerWalletAddress?: string;
85
- /** Settlement backend configuration (crypto). */
86
- paymentConfig?: PaymentConfig | null;
87
- /** Base JSON-RPC URL (e.g. http://127.0.0.1:8545 for anvil) */
88
- rpcUrl?: string;
89
- /** Deployed AntseedEscrow contract address */
90
- contractAddress?: string;
91
- /** USDC token contract address */
92
- usdcAddress?: string;
93
- }
94
-
95
- export interface NodeConfig {
96
- role: 'seller' | 'buyer';
97
- dataDir?: string; // Default: ~/.antseed
98
- dhtPort?: number; // Default: 6881 for seller, 0 for buyer
99
- signalingPort?: number; // Default: 6882 for seller
100
- bootstrapNodes?: Array<{ host: string; port: number }>;
101
- requestTimeoutMs?: number; // Default: 30000
102
- /** Allow private/loopback IPs in DHT lookups. Default: false. Set true for local testing. */
103
- allowPrivateIPs?: boolean;
104
- /** Optional seller-side payment runtime wiring. */
105
- payments?: NodePaymentsConfig;
106
- }
107
-
108
- interface SellerSessionState {
109
- sessionId: string;
110
- sessionIdBytes: Uint8Array;
111
- startedAt: number;
112
- lastActivityAt: number;
113
- totalRequests: number;
114
- totalTokens: number;
115
- totalLatencyMs: number;
116
- totalCostCents: number;
117
- provider: string;
118
-
119
- // --- Bilateral payment state ---
120
- lockCommitted: boolean;
121
- lockedAmount: bigint;
122
- runningTotal: bigint;
123
- ackedRequestCount: number;
124
- lastAckedTotal: bigint;
125
- awaitingAck: boolean;
126
- buyerEvmAddress: string | null;
127
-
128
- // Legacy fields
129
- channelId?: string;
130
- settling?: boolean;
131
- }
132
-
133
- export interface SellerSessionSnapshot {
134
- sessionId: string;
135
- buyerPeerId: string;
136
- provider: string;
137
- startedAt: number;
138
- lastActivityAt: number;
139
- totalRequests: number;
140
- totalTokens: number;
141
- avgLatencyMs: number;
142
- settling: boolean;
143
- lockCommitted: boolean;
144
- lockedAmountUSDC: string;
145
- runningTotalUSDC: string;
146
- ackedRequestCount: number;
147
- }
148
-
149
- export class AntseedNode extends EventEmitter {
150
- private _config: NodeConfig;
151
- private _identity: Identity | null = null;
152
- private _dht: DHTNode | null = null;
153
- private _connectionManager: ConnectionManager | null = null;
154
- private _providers: Provider[] = [];
155
- private _router: Router | null = null;
156
- private _started = false;
157
- private _announcer: PeerAnnouncer | null = null;
158
- private _peerLookup: PeerLookup | null = null;
159
- private _muxes = new Map<PeerId, ProxyMux>();
160
- private _decoders = new Map<PeerId, FrameDecoder>();
161
- private _nat: NatTraversal | null = null;
162
- private _metering: MeteringStorage | null = null;
163
- private _receiptGenerator: ReceiptGenerator | null = null;
164
- private _balanceManager: BalanceManager | null = null;
165
- private _escrowClient: BaseEscrowClient | null = null;
166
- private _paymentMuxes = new Map<PeerId, PaymentMux>();
167
- /** Per-buyer session tracking: buyerPeerId → seller session state */
168
- private _sessions = new Map<string, SellerSessionState>();
169
- private _settlementTimers = new Map<string, ReturnType<typeof setTimeout>>();
170
- /** Buyer-side payment manager (initialized when buyer has payment config). */
171
- private _buyerPaymentManager: BuyerPaymentManager | null = null;
172
- /** Tracks which seller peers the buyer has already initiated a lock for. */
173
- private _buyerLockedPeers = new Set<string>();
174
-
175
- constructor(config: NodeConfig) {
176
- super();
177
- this._config = config;
178
- }
179
-
180
- get peerId(): string | null {
181
- return this._identity?.peerId ?? null;
182
- }
183
-
184
- get identity(): Identity | null {
185
- return this._identity;
186
- }
187
-
188
- registerProvider(provider: Provider): void {
189
- this._providers.push(provider);
190
- }
191
-
192
- setRouter(router: Router): void {
193
- this._router = router;
194
- }
195
-
196
- get router(): Router | null {
197
- return this._router;
198
- }
199
-
200
- /** Buyer-side payment manager (null if payments not enabled or not in buyer mode). */
201
- get buyerPaymentManager(): BuyerPaymentManager | null {
202
- return this._buyerPaymentManager;
203
- }
204
-
205
- /** Actual DHT port after binding (0 means not started). */
206
- get dhtPort(): number {
207
- return this._dht?.getPort() ?? 0;
208
- }
209
-
210
- /** Actual signaling/connection port after binding (0 means not started). */
211
- get signalingPort(): number {
212
- return this._connectionManager?.getListeningPort() ?? 0;
213
- }
214
-
215
- /**
216
- * Active seller sessions currently tracked in-memory.
217
- * Includes open sessions before they are finalized/settled.
218
- */
219
- getActiveSellerSessions(): SellerSessionSnapshot[] {
220
- const snapshots: SellerSessionSnapshot[] = [];
221
- for (const [buyerPeerId, session] of this._sessions.entries()) {
222
- snapshots.push({
223
- sessionId: session.sessionId,
224
- buyerPeerId,
225
- provider: session.provider,
226
- startedAt: session.startedAt,
227
- lastActivityAt: session.lastActivityAt,
228
- totalRequests: session.totalRequests,
229
- totalTokens: session.totalTokens,
230
- avgLatencyMs: session.totalRequests > 0 ? session.totalLatencyMs / session.totalRequests : 0,
231
- settling: Boolean(session.settling),
232
- lockCommitted: session.lockCommitted,
233
- lockedAmountUSDC: session.lockedAmount.toString(),
234
- runningTotalUSDC: session.runningTotal.toString(),
235
- ackedRequestCount: session.ackedRequestCount,
236
- });
237
- }
238
- return snapshots;
239
- }
240
-
241
- /** Number of active in-memory seller sessions that are not currently settling. */
242
- getActiveSellerSessionCount(): number {
243
- let count = 0;
244
- for (const session of this._sessions.values()) {
245
- if (!session.settling) {
246
- count += 1;
247
- }
248
- }
249
- return count;
250
- }
251
-
252
- async start(): Promise<void> {
253
- if (this._started) {
254
- throw new Error("Node already started");
255
- }
256
-
257
- const dataDir = this._config.dataDir ?? join(homedir(), ".antseed");
258
-
259
- // Load or create identity
260
- this._identity = await loadOrCreateIdentity(dataDir);
261
- debugLog(`[Node] Identity loaded: ${this._identity.peerId.slice(0, 12)}...`);
262
-
263
- // Determine bootstrap nodes
264
- const bootstrapNodes = this._config.bootstrapNodes ?? toBootstrapConfig(OFFICIAL_BOOTSTRAP_NODES);
265
- debugLog(`[Node] Starting as ${this._config.role} with ${bootstrapNodes.length} bootstrap node(s)`);
266
-
267
- if (this._config.role === "seller") {
268
- await this._startSeller(bootstrapNodes);
269
- } else {
270
- await this._startBuyer(bootstrapNodes);
271
- }
272
-
273
- this._started = true;
274
- debugLog(`[Node] Started successfully`);
275
- this.emit("started");
276
- }
277
-
278
- async stop(): Promise<void> {
279
- if (!this._started) {
280
- return;
281
- }
282
-
283
- // End all active buyer payment sessions before shutdown
284
- await this._endAllBuyerSessions();
285
-
286
- await this._finalizeAllSessions("node-stop");
287
-
288
- for (const timer of this._settlementTimers.values()) {
289
- clearTimeout(timer);
290
- }
291
- this._settlementTimers.clear();
292
-
293
- // Remove NAT port mappings
294
- if (this._nat) {
295
- await this._nat.cleanup();
296
- this._nat = null;
297
- }
298
-
299
- // Stop announcer
300
- if (this._announcer) {
301
- this._announcer.stopPeriodicAnnounce();
302
- this._announcer = null;
303
- }
304
-
305
- // Close all proxy muxes
306
- this._muxes.clear();
307
- this._paymentMuxes.clear();
308
- this._decoders.clear();
309
-
310
- // Close all connections
311
- if (this._connectionManager) {
312
- this._connectionManager.closeAll();
313
- this._connectionManager = null;
314
- }
315
-
316
- // Stop DHT
317
- if (this._dht) {
318
- await this._dht.stop();
319
- this._dht = null;
320
- }
321
-
322
- if (this._balanceManager) {
323
- try {
324
- const dataDir = this._config.dataDir ?? join(homedir(), ".antseed");
325
- await this._balanceManager.save(join(dataDir, "payments"));
326
- } catch (err) {
327
- debugWarn(`[Node] Failed to persist payment balances: ${err instanceof Error ? err.message : err}`);
328
- }
329
- }
330
-
331
- if (this._metering) {
332
- try {
333
- this._metering.close();
334
- } catch {
335
- // ignore close errors
336
- }
337
- this._metering = null;
338
- }
339
-
340
- this._peerLookup = null;
341
- this._receiptGenerator = null;
342
- this._balanceManager = null;
343
- this._escrowClient = null;
344
- this._buyerPaymentManager = null;
345
- this._buyerLockedPeers.clear();
346
- this._started = false;
347
- this.emit("stopped");
348
- }
349
-
350
- async discoverPeers(model?: string): Promise<PeerInfo[]> {
351
- if (!this._peerLookup) {
352
- throw new Error("Node not started or not in buyer mode");
353
- }
354
-
355
- // If a model is specified, search by model across known provider topics.
356
- // Otherwise search for a generic topic. The PeerLookup uses provider names,
357
- // so without a model we search broadly.
358
- const searchTerm = model ?? "*";
359
- debugLog(`[Node] Discovering peers (search: "${searchTerm}")...`);
360
- const results = await this._peerLookup.findSellers(searchTerm);
361
- debugLog(`[Node] DHT returned ${results.length} result(s)`);
362
-
363
- // Deduplicate by peerId (DHT can return the same peer from multiple topic lookups)
364
- const seen = new Set<string>();
365
- const peers: PeerInfo[] = [];
366
- for (const r of results) {
367
- const p = this._lookupResultToPeerInfo(r);
368
- if (!seen.has(p.peerId)) {
369
- seen.add(p.peerId);
370
- peers.push(p);
371
- }
372
- }
373
- // Optional reputation verification: replace claimed data with verified on-chain data
374
- if (this._escrowClient) {
375
- for (const p of peers) {
376
- if (p.evmAddress && p.onChainReputation !== undefined) {
377
- try {
378
- const rep = await this._escrowClient.getReputation(p.evmAddress);
379
- p.onChainReputation = rep.weightedAverage;
380
- p.onChainSessionCount = rep.sessionCount;
381
- p.onChainDisputeCount = rep.disputeCount;
382
- p.trustScore = rep.weightedAverage;
383
- } catch {
384
- // Use claimed data if verification fails
385
- }
386
- }
387
- }
388
- }
389
-
390
- for (const p of peers) {
391
- debugLog(`[Node] peer ${p.peerId.slice(0, 12)}... providers=[${p.providers.join(",")}] addr=${p.publicAddress ?? "?"}`);
392
- }
393
- return peers;
394
- }
395
-
396
- async sendRequest(peer: PeerInfo, req: SerializedHttpRequest): Promise<SerializedHttpResponse> {
397
- if (!req.requestId || typeof req.requestId !== "string") {
398
- throw new Error("requestId must be a non-empty string");
399
- }
400
- if (!this._connectionManager || !this._identity) {
401
- throw new Error("Node not started");
402
- }
403
-
404
- debugLog(`[Node] sendRequest ${req.method} ${req.path} → peer ${peer.peerId.slice(0, 12)}... (reqId=${req.requestId.slice(0, 8)})`);
405
-
406
- const conn = await this._getOrCreateConnection(peer);
407
- debugLog(`[Node] Connection to ${peer.peerId.slice(0, 12)}... state=${conn.state}`);
408
- const mux = this._getOrCreateMux(peer.peerId, conn);
409
-
410
- // Buyer-side: initiate lock and wait for confirmation on first request to a new peer
411
- if (this._buyerPaymentManager && !this._buyerLockedPeers.has(peer.peerId)) {
412
- await this._initiateBuyerLock(peer, conn);
413
- }
414
-
415
- const startTime = Date.now();
416
- return new Promise<SerializedHttpResponse>((resolve, reject) => {
417
- const timeoutMs = this._config.requestTimeoutMs ?? 30_000;
418
- const timeout = setTimeout(() => {
419
- debugWarn(`[Node] Request ${req.requestId.slice(0, 8)} timed out after ${timeoutMs}ms`);
420
- mux.cancelProxyRequest(req.requestId);
421
- reject(new Error(`Request ${req.requestId} timed out`));
422
- }, timeoutMs);
423
-
424
- mux.sendProxyRequest(
425
- req,
426
- (response: SerializedHttpResponse) => {
427
- clearTimeout(timeout);
428
- debugLog(`[Node] Response for ${req.requestId.slice(0, 8)}: status=${response.statusCode} (${Date.now() - startTime}ms, ${response.body.length}b)`);
429
- resolve(response);
430
- },
431
- (_chunk) => {
432
- // Chunks are handled by the response handler for now
433
- },
434
- );
435
- });
436
- }
437
-
438
- async *sendTask(peer: PeerInfo, task: TaskRequest): AsyncIterable<TaskEvent> {
439
- const req: SerializedHttpRequest = {
440
- requestId: task.taskId,
441
- method: "POST",
442
- path: "/v1/task",
443
- headers: {
444
- "content-type": "application/json",
445
- "x-antseed-capability": "task",
446
- },
447
- body: new TextEncoder().encode(JSON.stringify(task)),
448
- };
449
-
450
- const response = await this.sendRequest(peer, req);
451
- const bodyText = new TextDecoder().decode(response.body);
452
- const parsed = JSON.parse(bodyText) as TaskEvent | TaskEvent[];
453
- if (Array.isArray(parsed)) {
454
- for (const event of parsed) {
455
- yield event;
456
- }
457
- } else {
458
- yield parsed;
459
- }
460
- }
461
-
462
- async sendSkill(peer: PeerInfo, skill: SkillRequest): Promise<SkillResponse> {
463
- const req: SerializedHttpRequest = {
464
- requestId: skill.skillId,
465
- method: "POST",
466
- path: "/v1/skill",
467
- headers: {
468
- "content-type": "application/json",
469
- "x-antseed-capability": "skill",
470
- },
471
- body: new TextEncoder().encode(JSON.stringify(skill)),
472
- };
473
-
474
- const response = await this.sendRequest(peer, req);
475
- const bodyText = new TextDecoder().decode(response.body);
476
- return JSON.parse(bodyText) as SkillResponse;
477
- }
478
-
479
- private _createDHTConfig(port: number, bootstrapNodes: Array<{ host: string; port: number }>): DHTNodeConfig {
480
- return {
481
- peerId: this._identity!.peerId,
482
- port,
483
- bootstrapNodes,
484
- reannounceIntervalMs: DEFAULT_DHT_CONFIG.reannounceIntervalMs,
485
- operationTimeoutMs: DEFAULT_DHT_CONFIG.operationTimeoutMs,
486
- allowPrivateIPs: this._config.allowPrivateIPs,
487
- };
488
- }
489
-
490
- private _wireConnection(conn: PeerConnection, peerId: PeerId): void {
491
- const decoder = new FrameDecoder();
492
- conn.on("message", (data: Uint8Array) => {
493
- const frames = decoder.feed(data);
494
- const proxyMux = this._muxes.get(peerId);
495
- const paymentMux = this._paymentMuxes.get(peerId);
496
- for (const frame of frames) {
497
- if (paymentMux && PaymentMux.isPaymentMessage(frame.type)) {
498
- paymentMux.handleFrame(frame).catch((err) => {
499
- const message = err instanceof Error ? err.message : String(err);
500
- debugWarn(`[Node] Failed to handle payment frame from ${peerId.slice(0, 12)}...: ${message}`);
501
- });
502
- } else if (proxyMux) {
503
- proxyMux.handleFrame(frame).catch((err) => {
504
- const message = err instanceof Error ? err.message : String(err);
505
- debugWarn(`[Node] Failed to handle frame from ${peerId.slice(0, 12)}...: ${message}`);
506
- conn.fail(err instanceof Error ? err : new Error(message));
507
- });
508
- }
509
- }
510
- });
511
-
512
- this._decoders.set(peerId, decoder);
513
-
514
- conn.on("stateChange", (state: ConnectionState) => {
515
- if (state === ConnectionState.Closed || state === ConnectionState.Failed) {
516
- this._muxes.delete(peerId);
517
- this._paymentMuxes.delete(peerId);
518
- this._decoders.delete(peerId);
519
- // Handle buyer disconnect (ghost scenario)
520
- void this._finalizeSession(peerId, "disconnect");
521
- }
522
- });
523
- }
524
-
525
- private async _startSeller(bootstrapNodes: Array<{ host: string; port: number }>): Promise<void> {
526
- const identity = this._identity!;
527
- const dhtPort = this._config.dhtPort ?? 6881;
528
- const signalingPort = this._config.signalingPort ?? 6882;
529
- debugLog(`[Node] Starting seller — DHT port=${dhtPort}, signaling port=${signalingPort}`);
530
-
531
- // Initialize metering storage
532
- const dataDir = this._config.dataDir ?? join(homedir(), ".antseed");
533
- try {
534
- this._metering = new MeteringStorage(join(dataDir, "metering.db"));
535
- debugLog("[Node] Metering storage initialized");
536
- } catch (err) {
537
- debugWarn(`[Node] Metering storage unavailable: ${err instanceof Error ? err.message : err}`);
538
- }
539
-
540
- if (this._metering) {
541
- this._receiptGenerator = new ReceiptGenerator({
542
- peerId: identity.peerId,
543
- sign: (message: string) => signUtf8Ed25519(identity.privateKey, message),
544
- });
545
- }
546
-
547
- await this._initializePayments(dataDir);
548
-
549
- // Start DHT
550
- this._dht = new DHTNode(this._createDHTConfig(dhtPort, bootstrapNodes));
551
- await this._dht.start();
552
-
553
- // Create ConnectionManager and start listening
554
- this._connectionManager = new ConnectionManager();
555
- this._connectionManager.setLocalIdentity(identity);
556
- await this._connectionManager.startListening({
557
- peerId: identity.peerId,
558
- port: signalingPort,
559
- host: "0.0.0.0",
560
- });
561
-
562
- // Resolve actual bound port (important when port 0 is used for OS-assigned)
563
- const actualSignalingPort = this._connectionManager.getListeningPort() ?? signalingPort;
564
- const actualDhtPort = this._dht.getPort();
565
-
566
- // NAT traversal: automatically map ports via UPnP/NAT-PMP
567
- this._nat = new NatTraversal();
568
- const natResult = await this._nat.mapPorts([
569
- { port: actualSignalingPort, protocol: "TCP" },
570
- { port: actualDhtPort, protocol: "UDP" },
571
- ]);
572
-
573
- if (natResult.success) {
574
- this.emit("nat:mapped", natResult);
575
- } else {
576
- debugWarn("[NAT] UPnP/NAT-PMP mapping failed — seller may not be reachable from the internet");
577
- debugWarn("[NAT] Ensure port forwarding is configured manually, or peers on the same LAN can still connect");
578
- this.emit("nat:failed");
579
- }
580
-
581
- // Set up announcer for providers
582
- if (this._providers.length > 0) {
583
- const announcerConfig: AnnouncerConfig = {
584
- identity,
585
- dht: this._dht,
586
- providers: this._providers.map((p) => ({
587
- provider: p.name,
588
- models: p.models,
589
- maxConcurrency: p.maxConcurrency,
590
- })),
591
- region: "unknown",
592
- pricing: new Map(
593
- this._providers.map((p) => [
594
- p.name,
595
- {
596
- defaults: {
597
- inputUsdPerMillion: p.pricing.defaults.inputUsdPerMillion,
598
- outputUsdPerMillion: p.pricing.defaults.outputUsdPerMillion,
599
- },
600
- ...(p.pricing.models ? { models: { ...p.pricing.models } } : {}),
601
- },
602
- ]),
603
- ),
604
- reannounceIntervalMs: DEFAULT_DHT_CONFIG.reannounceIntervalMs,
605
- signalingPort: actualSignalingPort,
606
- };
607
- this._announcer = new PeerAnnouncer(announcerConfig);
608
- this._announcer.startPeriodicAnnounce();
609
-
610
- // Serve metadata on the signaling port (HTTP requests are auto-detected)
611
- this._connectionManager!.setMetadataProvider(
612
- () => this._announcer?.getLatestMetadata() ?? null,
613
- );
614
- }
615
-
616
- // Listen for incoming connections
617
- this._connectionManager.on("connection", (conn: PeerConnection) => {
618
- this._handleIncomingConnection(conn);
619
- });
620
-
621
- debugLog(`[Node] Seller ready — announcing ${this._providers.length} provider(s)`);
622
- }
623
-
624
- private async _startBuyer(bootstrapNodes: Array<{ host: string; port: number }>): Promise<void> {
625
- const identity = this._identity!;
626
- const dhtPort = this._config.dhtPort ?? 0;
627
- debugLog(`[Node] Starting buyer — DHT port=${dhtPort}`);
628
-
629
- // Start DHT with ephemeral port
630
- this._dht = new DHTNode(this._createDHTConfig(dhtPort, bootstrapNodes));
631
- await this._dht.start();
632
-
633
- // Create ConnectionManager for outbound connections
634
- this._connectionManager = new ConnectionManager();
635
- this._connectionManager.setLocalIdentity(identity);
636
-
637
- // Create PeerLookup with HttpMetadataResolver
638
- const metadataResolver = new HttpMetadataResolver();
639
- const lookupConfig: LookupConfig = {
640
- dht: this._dht,
641
- metadataResolver,
642
- requireValidSignature: DEFAULT_LOOKUP_CONFIG.requireValidSignature,
643
- allowStaleMetadata: DEFAULT_LOOKUP_CONFIG.allowStaleMetadata,
644
- maxAnnouncementAgeMs: DEFAULT_LOOKUP_CONFIG.maxAnnouncementAgeMs,
645
- maxResults: DEFAULT_LOOKUP_CONFIG.maxResults,
646
- };
647
- this._peerLookup = new PeerLookup(lookupConfig);
648
-
649
- // Initialize buyer-side payment manager if payments config is provided
650
- const payments = this._config.payments;
651
- if (payments?.enabled && payments.rpcUrl && payments.contractAddress && payments.usdcAddress) {
652
- const buyerPaymentConfig: BuyerPaymentConfig = {
653
- defaultLockAmountUSDC: payments.defaultEscrowAmountUSDC ?? "1000000",
654
- rpcUrl: payments.rpcUrl,
655
- contractAddress: payments.contractAddress,
656
- usdcAddress: payments.usdcAddress,
657
- };
658
- this._buyerPaymentManager = new BuyerPaymentManager(identity, buyerPaymentConfig);
659
- debugLog(`[Node] Buyer payment manager initialized (wallet=${identityToEvmAddress(identity).slice(0, 10)}...)`);
660
- }
661
-
662
- debugLog(`[Node] Buyer ready — DHT running on port ${this._dht!.getPort()}`);
663
- }
664
-
665
- private _handleIncomingConnection(conn: PeerConnection): void {
666
- debugLog(`[Node] Incoming connection from ${conn.remotePeerId.slice(0, 12)}...`);
667
- const buyerPeerId = conn.remotePeerId;
668
- const mux = new ProxyMux(conn);
669
-
670
- // Create PaymentMux alongside ProxyMux (seller-side)
671
- const paymentMux = new PaymentMux(conn);
672
- paymentMux.onSessionLockAuth((payload) => {
673
- void this._handleSessionLockAuth(buyerPeerId, payload, paymentMux);
674
- });
675
- paymentMux.onBuyerAck((payload) => {
676
- void this._handleBuyerAck(buyerPeerId, payload);
677
- });
678
- paymentMux.onSessionEnd((payload) => {
679
- void this._handleSessionEnd(buyerPeerId, payload);
680
- });
681
- paymentMux.onTopUpAuth((payload) => {
682
- void this._handleTopUpAuth(buyerPeerId, payload);
683
- });
684
- this._paymentMuxes.set(buyerPeerId, paymentMux);
685
-
686
- // Register the ProxyMux request handler that routes to providers
687
- mux.onProxyRequest(async (request: SerializedHttpRequest) => {
688
- debugLog(`[Node] Seller received request: ${request.method} ${request.path} (reqId=${request.requestId.slice(0, 8)})`);
689
-
690
- // Reject with 402 if lock not committed and escrow client is configured
691
- const session = this._sessions.get(buyerPeerId);
692
- if (this._escrowClient && (!session || !session.lockCommitted)) {
693
- debugWarn(`[Node] Rejecting request from ${buyerPeerId.slice(0, 12)}... — lock not committed`);
694
- mux.sendProxyResponse({
695
- requestId: request.requestId,
696
- statusCode: 402,
697
- headers: { "content-type": "text/plain" },
698
- body: new TextEncoder().encode("Payment required: session lock not committed"),
699
- });
700
- return;
701
- }
702
-
703
- const provider = this._providers.find((p) =>
704
- p.models.some((m) => request.path.includes(m)) || this._providers.length === 1
705
- );
706
-
707
- if (!provider) {
708
- debugWarn(`[Node] No matching provider for ${request.path}`);
709
- mux.sendProxyResponse({
710
- requestId: request.requestId,
711
- statusCode: 502,
712
- headers: { "content-type": "text/plain" },
713
- body: new TextEncoder().encode("No matching provider"),
714
- });
715
- return;
716
- }
717
-
718
- // Track active seller session at request start so runtime state reflects
719
- // in-flight work immediately (not only after metering persistence).
720
- this._getOrCreateSellerSession(buyerPeerId, provider.name);
721
-
722
- debugLog(`[Node] Routing to provider "${provider.name}"`);
723
- const startTime = Date.now();
724
- let statusCode = 500;
725
- let responseBody: Uint8Array = new Uint8Array(0);
726
- try {
727
- const response = await this._executeProviderRequest(provider, request);
728
- statusCode = response.statusCode;
729
- responseBody = response.body;
730
- debugLog(`[Node] Provider responded: status=${statusCode} (${Date.now() - startTime}ms, ${responseBody.length}b)`);
731
- mux.sendProxyResponse(response);
732
- } catch (err) {
733
- const message = err instanceof Error ? err.message : "Internal error";
734
- debugWarn(`[Node] Provider error after ${Date.now() - startTime}ms: ${message}`);
735
- responseBody = new TextEncoder().encode(message);
736
- mux.sendProxyResponse({
737
- requestId: request.requestId,
738
- statusCode: 500,
739
- headers: { "content-type": "text/plain" },
740
- body: responseBody,
741
- });
742
- }
743
-
744
- // Record metering
745
- const latencyMs = Date.now() - startTime;
746
- const requestPricing = this._resolveProviderPricing(provider, request);
747
- await this._recordMetering(
748
- buyerPeerId,
749
- provider.name,
750
- requestPricing,
751
- request,
752
- statusCode,
753
- latencyMs,
754
- request.body.length,
755
- responseBody.length,
756
- );
757
-
758
- // Generate bilateral receipt after each request if lock committed (Task 3)
759
- const currentSession = this._sessions.get(buyerPeerId);
760
- if (currentSession?.lockCommitted) {
761
- await this._sendBilateralReceipt(buyerPeerId, currentSession, requestPricing, responseBody, paymentMux);
762
- }
763
- });
764
-
765
- this._muxes.set(buyerPeerId, mux);
766
- this._wireConnection(conn, buyerPeerId);
767
- this.emit("connection", conn);
768
- }
769
-
770
- private async _executeProviderRequest(
771
- provider: Provider,
772
- request: SerializedHttpRequest,
773
- ): Promise<SerializedHttpResponse> {
774
- const capability = request.headers["x-antseed-capability"]?.toLowerCase();
775
- const isTask = capability === "task" || request.path === "/v1/task";
776
- const isSkill = capability === "skill" || request.path === "/v1/skill";
777
-
778
- if (isSkill) {
779
- if (!provider.handleSkill) {
780
- return {
781
- requestId: request.requestId,
782
- statusCode: 501,
783
- headers: { "content-type": "application/json" },
784
- body: new TextEncoder().encode(JSON.stringify({ error: "Provider does not support skill capability" })),
785
- };
786
- }
787
-
788
- const parsed = this._parseJsonBody(request.body);
789
- if (!parsed || typeof parsed !== "object") {
790
- return {
791
- requestId: request.requestId,
792
- statusCode: 400,
793
- headers: { "content-type": "application/json" },
794
- body: new TextEncoder().encode(JSON.stringify({ error: "Invalid skill payload" })),
795
- };
796
- }
797
-
798
- const raw = parsed as Record<string, unknown>;
799
- const skillReq: SkillRequest = {
800
- skillId: typeof raw["skillId"] === "string" ? raw["skillId"] : request.requestId,
801
- capability: typeof raw["capability"] === "string" ? raw["capability"] as SkillRequest["capability"] : "skill",
802
- input: raw["input"] ?? {},
803
- inputSchema: (raw["inputSchema"] && typeof raw["inputSchema"] === "object")
804
- ? raw["inputSchema"] as Record<string, unknown>
805
- : undefined,
806
- };
807
-
808
- const skillResponse = await provider.handleSkill(skillReq);
809
- return {
810
- requestId: request.requestId,
811
- statusCode: 200,
812
- headers: { "content-type": "application/json" },
813
- body: new TextEncoder().encode(JSON.stringify(skillResponse)),
814
- };
815
- }
816
-
817
- if (isTask) {
818
- if (!provider.handleTask) {
819
- return {
820
- requestId: request.requestId,
821
- statusCode: 501,
822
- headers: { "content-type": "application/json" },
823
- body: new TextEncoder().encode(JSON.stringify({ error: "Provider does not support task capability" })),
824
- };
825
- }
826
-
827
- const parsed = this._parseJsonBody(request.body);
828
- if (!parsed || typeof parsed !== "object") {
829
- return {
830
- requestId: request.requestId,
831
- statusCode: 400,
832
- headers: { "content-type": "application/json" },
833
- body: new TextEncoder().encode(JSON.stringify({ error: "Invalid task payload" })),
834
- };
835
- }
836
-
837
- const raw = parsed as Record<string, unknown>;
838
- const taskReq: TaskRequest = {
839
- taskId: typeof raw["taskId"] === "string" ? raw["taskId"] : request.requestId,
840
- capability: typeof raw["capability"] === "string" ? raw["capability"] as TaskRequest["capability"] : "agent",
841
- input: raw["input"] ?? {},
842
- metadata: (raw["metadata"] && typeof raw["metadata"] === "object")
843
- ? raw["metadata"] as Record<string, unknown>
844
- : undefined,
845
- };
846
-
847
- const events: TaskEvent[] = [];
848
- for await (const event of provider.handleTask(taskReq)) {
849
- events.push(event);
850
- }
851
-
852
- const payload: TaskEvent | TaskEvent[] = events.length <= 1
853
- ? (events[0] ?? {
854
- taskId: taskReq.taskId,
855
- type: "final",
856
- data: {},
857
- timestamp: Date.now(),
858
- })
859
- : events;
860
-
861
- return {
862
- requestId: request.requestId,
863
- statusCode: 200,
864
- headers: { "content-type": "application/json" },
865
- body: new TextEncoder().encode(JSON.stringify(payload)),
866
- };
867
- }
868
-
869
- return provider.handleRequest(request);
870
- }
871
-
872
- private _parseJsonBody(body: Uint8Array): unknown | null {
873
- try {
874
- return JSON.parse(new TextDecoder().decode(body)) as unknown;
875
- } catch {
876
- return null;
877
- }
878
- }
879
-
880
- private _extractRequestedModel(request: SerializedHttpRequest): string | null {
881
- const contentType = request.headers["content-type"] ?? request.headers["Content-Type"] ?? "";
882
- if (!contentType.toLowerCase().includes("application/json")) {
883
- return null;
884
- }
885
- const parsed = this._parseJsonBody(request.body);
886
- if (!parsed || typeof parsed !== "object") {
887
- return null;
888
- }
889
- const model = (parsed as Record<string, unknown>)["model"];
890
- if (typeof model !== "string" || model.trim().length === 0) {
891
- return null;
892
- }
893
- return model.trim();
894
- }
895
-
896
- private _resolveProviderPricing(
897
- provider: Provider,
898
- request: SerializedHttpRequest,
899
- ): { inputUsdPerMillion: number; outputUsdPerMillion: number } {
900
- const requestedModel = this._extractRequestedModel(request);
901
- if (requestedModel) {
902
- const modelPricing = provider.pricing.models?.[requestedModel];
903
- if (modelPricing) {
904
- return modelPricing;
905
- }
906
- }
907
- return provider.pricing.defaults;
908
- }
909
-
910
- private _getOrCreateSellerSession(
911
- buyerPeerId: string,
912
- providerName: string,
913
- ): SellerSessionState | null {
914
- if (!this._identity) {
915
- return null;
916
- }
917
-
918
- let session = this._sessions.get(buyerPeerId);
919
- if (!session) {
920
- const now = Date.now();
921
- const sessionId = randomUUID();
922
- // Generate 32-byte sessionIdBytes from UUID for on-chain use
923
- const sessionIdBytes = createHash("sha256").update(sessionId).digest();
924
- session = {
925
- sessionId,
926
- sessionIdBytes: new Uint8Array(sessionIdBytes),
927
- startedAt: now,
928
- lastActivityAt: now,
929
- totalRequests: 0,
930
- totalTokens: 0,
931
- totalLatencyMs: 0,
932
- totalCostCents: 0,
933
- provider: providerName,
934
- lockCommitted: false,
935
- lockedAmount: 0n,
936
- runningTotal: 0n,
937
- ackedRequestCount: 0,
938
- lastAckedTotal: 0n,
939
- awaitingAck: false,
940
- buyerEvmAddress: null,
941
- };
942
- this._sessions.set(buyerPeerId, session);
943
- }
944
-
945
- session.provider = providerName;
946
- session.lastActivityAt = Date.now();
947
- this._emitSellerSessionUpdated(buyerPeerId, session);
948
-
949
- return session;
950
- }
951
-
952
- private _emitSellerSessionUpdated(buyerPeerId: string, session: SellerSessionState): void {
953
- this.emit("session:updated", {
954
- buyerPeerId,
955
- sessionId: session.sessionId,
956
- provider: session.provider,
957
- startedAt: session.startedAt,
958
- lastActivityAt: session.lastActivityAt,
959
- totalRequests: session.totalRequests,
960
- totalTokens: session.totalTokens,
961
- avgLatencyMs: session.totalRequests > 0 ? session.totalLatencyMs / session.totalRequests : 0,
962
- settling: Boolean(session.settling),
963
- });
964
- }
965
-
966
- /** Estimate tokens from byte lengths (rough: ~4 chars per token). */
967
- private _estimateTokens(inputBytes: number, outputBytes: number) {
968
- const inputTokens = Math.max(1, Math.round(inputBytes / 4));
969
- const outputTokens = Math.max(1, Math.round(outputBytes / 4));
970
- return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
971
- }
972
-
973
- private async _recordMetering(
974
- buyerPeerId: string,
975
- providerName: string,
976
- providerPricingUsdPerMillion: { inputUsdPerMillion: number; outputUsdPerMillion: number },
977
- request: SerializedHttpRequest,
978
- statusCode: number,
979
- latencyMs: number,
980
- inputBytes: number,
981
- outputBytes: number,
982
- ): Promise<void> {
983
- if (!this._identity) return;
984
-
985
- const sellerPeerId = this._identity.peerId;
986
- const isSSE = request.headers["accept"]?.includes("text/event-stream") ?? false;
987
- const tokens = this._estimateTokens(inputBytes, outputBytes);
988
-
989
- // Get or create session for this buyer
990
- const session = this._getOrCreateSellerSession(buyerPeerId, providerName);
991
- if (!session) return;
992
-
993
- session.totalRequests++;
994
- session.totalTokens += tokens.totalTokens;
995
- session.totalLatencyMs += latencyMs;
996
- session.provider = providerName;
997
- session.lastActivityAt = Date.now();
998
- this._emitSellerSessionUpdated(buyerPeerId, session);
999
-
1000
- const metering = this._metering;
1001
- if (!metering) {
1002
- this._scheduleSettlementTimer(buyerPeerId);
1003
- return;
1004
- }
1005
-
1006
- // Record metering event
1007
- const event: MeteringEvent = {
1008
- eventId: randomUUID(),
1009
- sessionId: session.sessionId,
1010
- timestamp: Date.now(),
1011
- provider: providerName,
1012
- sellerPeerId,
1013
- buyerPeerId,
1014
- tokens: { ...tokens, method: "content-length", confidence: "low" },
1015
- latencyMs,
1016
- statusCode,
1017
- wasStreaming: isSSE,
1018
- };
1019
-
1020
- try {
1021
- metering.insertEvent(event);
1022
- } catch (err) {
1023
- debugWarn(`[Node] Failed to record metering event: ${err instanceof Error ? err.message : err}`);
1024
- }
1025
-
1026
- if (this._receiptGenerator) {
1027
- const estimatedCostUsd =
1028
- (tokens.inputTokens * providerPricingUsdPerMillion.inputUsdPerMillion +
1029
- tokens.outputTokens * providerPricingUsdPerMillion.outputUsdPerMillion) /
1030
- 1_000_000;
1031
- const effectiveUsdPerThousandTokens =
1032
- tokens.totalTokens > 0 ? (estimatedCostUsd / tokens.totalTokens) * 1000 : 0;
1033
- // Receipt unit pricing uses USD cents per 1,000 tokens.
1034
- const unitPriceCentsPerThousandTokens = Math.max(0, effectiveUsdPerThousandTokens * 100);
1035
- const receipt = this._receiptGenerator.generate(
1036
- session.sessionId,
1037
- event.eventId,
1038
- providerName,
1039
- buyerPeerId,
1040
- event.tokens,
1041
- unitPriceCentsPerThousandTokens,
1042
- );
1043
- try {
1044
- metering.insertReceipt(receipt);
1045
- session.totalCostCents += receipt.costCents;
1046
- } catch (err) {
1047
- debugWarn(`[Node] Failed to record usage receipt: ${err instanceof Error ? err.message : err}`);
1048
- }
1049
- }
1050
-
1051
- // Upsert session
1052
- const sessionMetrics: SessionMetrics = {
1053
- sessionId: session.sessionId,
1054
- sellerPeerId,
1055
- buyerPeerId,
1056
- provider: providerName,
1057
- startedAt: session.startedAt,
1058
- endedAt: null,
1059
- totalRequests: session.totalRequests,
1060
- totalTokens: session.totalTokens,
1061
- totalCostCents: session.totalCostCents,
1062
- avgLatencyMs: session.totalLatencyMs / session.totalRequests,
1063
- peerSwitches: 0,
1064
- disputedReceipts: 0,
1065
- };
1066
-
1067
- try {
1068
- metering.upsertSession(sessionMetrics);
1069
- } catch (err) {
1070
- debugWarn(`[Node] Failed to upsert session: ${err instanceof Error ? err.message : err}`);
1071
- }
1072
-
1073
- this._scheduleSettlementTimer(buyerPeerId);
1074
- }
1075
-
1076
- private async _initializePayments(dataDir: string): Promise<void> {
1077
- const payments = this._config.payments;
1078
- if (!payments || !payments.enabled) {
1079
- return;
1080
- }
1081
-
1082
- // Initialize BaseEscrowClient if Base config is provided
1083
- if (payments.rpcUrl && payments.contractAddress && payments.usdcAddress) {
1084
- this._escrowClient = new BaseEscrowClient({
1085
- rpcUrl: payments.rpcUrl,
1086
- contractAddress: payments.contractAddress,
1087
- usdcAddress: payments.usdcAddress,
1088
- });
1089
- debugLog(`[Node] BaseEscrowClient initialized (contract=${payments.contractAddress.slice(0, 10)}...)`);
1090
- }
1091
-
1092
- if (!this._metering) {
1093
- debugWarn("[Node] Payments enabled but metering storage is unavailable; skipping balance manager wiring");
1094
- return;
1095
- }
1096
-
1097
- const paymentsDir = join(dataDir, "payments");
1098
- this._balanceManager = new BalanceManager();
1099
- await this._balanceManager.load(paymentsDir).catch((err) => {
1100
- debugWarn(`[Node] Failed to load payment balances: ${err instanceof Error ? err.message : err}`);
1101
- });
1102
- }
1103
-
1104
- private _scheduleSettlementTimer(buyerPeerId: string): void {
1105
- const existing = this._settlementTimers.get(buyerPeerId);
1106
- if (existing) {
1107
- clearTimeout(existing);
1108
- }
1109
-
1110
- const idleMs = this._config.payments?.settlementIdleMs ?? 30_000;
1111
- const timer = setTimeout(() => {
1112
- void this._finalizeSession(buyerPeerId, "idle-timeout");
1113
- }, idleMs);
1114
-
1115
- if (typeof (timer as { unref?: () => void }).unref === "function") {
1116
- (timer as { unref: () => void }).unref();
1117
- }
1118
-
1119
- this._settlementTimers.set(buyerPeerId, timer);
1120
- }
1121
-
1122
- private async _finalizeSession(buyerPeerId: string, reason: string): Promise<void> {
1123
- const session = this._sessions.get(buyerPeerId);
1124
- if (!session || session.settling) {
1125
- return;
1126
- }
1127
- session.settling = true;
1128
-
1129
- const timer = this._settlementTimers.get(buyerPeerId);
1130
- if (timer) {
1131
- clearTimeout(timer);
1132
- this._settlementTimers.delete(buyerPeerId);
1133
- }
1134
-
1135
- // Bilateral-aware disconnect handling (ghost scenario - Task 7)
1136
- if (session.lockCommitted && this._escrowClient && this._identity && reason === "disconnect") {
1137
- const sellerWallet = identityToEvmWallet(this._identity);
1138
- const sessionIdHex = "0x" + bytesToHex(session.sessionIdBytes);
1139
-
1140
- try {
1141
- if (session.lastAckedTotal > 0n) {
1142
- // Buyer acked some work — open dispute with last acked total
1143
- debugLog(`[Node] Ghost buyer — opening dispute with lastAckedTotal=${session.lastAckedTotal}`);
1144
- await this._escrowClient.openDispute(sellerWallet, sessionIdHex, session.lastAckedTotal);
1145
- } else if (session.runningTotal > 0n) {
1146
- // No acks but work was done — open dispute with running total
1147
- debugLog(`[Node] Ghost buyer — opening dispute with runningTotal=${session.runningTotal}`);
1148
- await this._escrowClient.openDispute(sellerWallet, sessionIdHex, session.runningTotal);
1149
- } else {
1150
- // No work done — lock expires after 1 hour automatically
1151
- debugLog(`[Node] Ghost buyer — no work done, lock will expire`);
1152
- }
1153
- } catch (err) {
1154
- debugWarn(`[Node] Failed to handle ghost buyer for session ${session.sessionId}: ${err instanceof Error ? err.message : err}`);
1155
- }
1156
-
1157
- this._sessions.delete(buyerPeerId);
1158
- this.emit("session:finalized", {
1159
- buyerPeerId,
1160
- sessionId: session.sessionId,
1161
- reason: "ghost-disconnect",
1162
- });
1163
- return;
1164
- }
1165
-
1166
- if (!this._metering || !this._identity) {
1167
- this._sessions.delete(buyerPeerId);
1168
- return;
1169
- }
1170
-
1171
- const now = Date.now();
1172
- const baseMetrics: SessionMetrics = {
1173
- sessionId: session.sessionId,
1174
- sellerPeerId: this._identity.peerId,
1175
- buyerPeerId,
1176
- provider: session.provider,
1177
- startedAt: session.startedAt,
1178
- endedAt: now,
1179
- totalRequests: session.totalRequests,
1180
- totalTokens: session.totalTokens,
1181
- totalCostCents: session.totalCostCents,
1182
- avgLatencyMs: session.totalRequests > 0 ? session.totalLatencyMs / session.totalRequests : 0,
1183
- peerSwitches: 0,
1184
- disputedReceipts: 0,
1185
- };
1186
-
1187
- try {
1188
- this._metering.upsertSession(baseMetrics);
1189
- this._sessions.delete(buyerPeerId);
1190
- this.emit("session:finalized", {
1191
- buyerPeerId,
1192
- sessionId: session.sessionId,
1193
- reason,
1194
- });
1195
- } catch (err) {
1196
- session.settling = false;
1197
- debugWarn(`[Node] Failed to finalize session ${session.sessionId}: ${err instanceof Error ? err.message : err}`);
1198
- const retry = setTimeout(() => {
1199
- void this._finalizeSession(buyerPeerId, "retry");
1200
- }, 10_000);
1201
- if (typeof (retry as { unref?: () => void }).unref === "function") {
1202
- (retry as { unref: () => void }).unref();
1203
- }
1204
- this._settlementTimers.set(buyerPeerId, retry);
1205
- }
1206
- }
1207
-
1208
- private async _finalizeAllSessions(reason: string): Promise<void> {
1209
- if (this._sessions.size === 0) return;
1210
- const buyers = [...this._sessions.keys()];
1211
- await Promise.allSettled(
1212
- buyers.map((buyerPeerId) => this._finalizeSession(buyerPeerId, reason)),
1213
- );
1214
- }
1215
-
1216
- private async _getOrCreateConnection(peer: PeerInfo): Promise<PeerConnection> {
1217
- if (!this._connectionManager || !this._identity) {
1218
- throw new Error("Node not started");
1219
- }
1220
-
1221
- const existing = this._connectionManager.getConnection(peer.peerId);
1222
- if (
1223
- existing &&
1224
- existing.state !== ConnectionState.Closed &&
1225
- existing.state !== ConnectionState.Failed
1226
- ) {
1227
- debugLog(`[Node] Reusing existing connection to ${peer.peerId.slice(0, 12)}... (state=${existing.state})`);
1228
- // If still connecting, wait for it to reach Open or Authenticated
1229
- if (existing.state === ConnectionState.Connecting) {
1230
- debugLog(`[Node] Waiting for connection to open...`);
1231
- await new Promise<void>((resolve, reject) => {
1232
- const onState = (state: ConnectionState): void => {
1233
- if (state === ConnectionState.Open || state === ConnectionState.Authenticated) {
1234
- existing.off("stateChange", onState);
1235
- resolve();
1236
- } else if (state === ConnectionState.Failed || state === ConnectionState.Closed) {
1237
- existing.off("stateChange", onState);
1238
- reject(new Error(`Connection to ${peer.peerId} failed`));
1239
- }
1240
- };
1241
- existing.on("stateChange", onState);
1242
- });
1243
- }
1244
- return existing;
1245
- }
1246
-
1247
- // Register the peer endpoint so ConnectionManager can resolve it
1248
- if (peer.publicAddress) {
1249
- const parts = peer.publicAddress.split(":");
1250
- const host = parts[0]!;
1251
- const port = parseInt(parts[1] ?? "6882", 10);
1252
- this._connectionManager.registerPeerEndpoint(peer.peerId, { host, port });
1253
- debugLog(`[Node] Connecting to ${peer.peerId.slice(0, 12)}... at ${host}:${port}`);
1254
- } else {
1255
- debugWarn(`[Node] Peer ${peer.peerId.slice(0, 12)}... has no public address`);
1256
- }
1257
-
1258
- const connConfig: ConnectionConfig = {
1259
- remotePeerId: peer.peerId,
1260
- isInitiator: true,
1261
- };
1262
-
1263
- const conn = this._connectionManager.createConnection(connConfig);
1264
-
1265
- // Wait for connection to open
1266
- await new Promise<void>((resolve, reject) => {
1267
- const onState = (state: ConnectionState): void => {
1268
- debugLog(`[Node] Connection state: ${state}`);
1269
- if (state === ConnectionState.Open || state === ConnectionState.Authenticated) {
1270
- conn.off("stateChange", onState);
1271
- resolve();
1272
- } else if (state === ConnectionState.Failed || state === ConnectionState.Closed) {
1273
- conn.off("stateChange", onState);
1274
- reject(new Error(`Connection to ${peer.peerId} failed`));
1275
- }
1276
- };
1277
- conn.on("stateChange", onState);
1278
- });
1279
-
1280
- debugLog(`[Node] Connected to ${peer.peerId.slice(0, 12)}...`);
1281
- this._wireConnection(conn, peer.peerId);
1282
- return conn;
1283
- }
1284
-
1285
- private _getOrCreateMux(peerId: PeerId, conn: PeerConnection): ProxyMux {
1286
- const existing = this._muxes.get(peerId);
1287
- if (existing) {
1288
- return existing;
1289
- }
1290
-
1291
- const mux = new ProxyMux(conn);
1292
- this._muxes.set(peerId, mux);
1293
- return mux;
1294
- }
1295
-
1296
- // ── Seller-side bilateral payment handlers ─────────────────────
1297
-
1298
- /**
1299
- * Handle SessionLockAuth from buyer (Task 2).
1300
- * Recovers buyer address, commits lock on-chain, initializes bilateral state.
1301
- */
1302
- private async _handleSessionLockAuth(
1303
- buyerPeerId: string,
1304
- payload: SessionLockAuthPayload,
1305
- paymentMux: PaymentMux,
1306
- ): Promise<void> {
1307
- if (!this._identity || !this._escrowClient) {
1308
- paymentMux.sendSessionLockReject({
1309
- sessionId: payload.sessionId,
1310
- reason: "Escrow client not configured",
1311
- });
1312
- return;
1313
- }
1314
-
1315
- try {
1316
- const sellerWallet = identityToEvmWallet(this._identity);
1317
- const lockedAmount = BigInt(payload.lockedAmount);
1318
-
1319
- // Recover buyer address from ECDSA signature
1320
- const lockMsgHash = buildLockMessageHash(payload.sessionId, sellerWallet.address, lockedAmount);
1321
- const buyerEvmAddress = verifyMessage(getBytes(lockMsgHash), payload.buyerSig);
1322
-
1323
- // Submit commit_lock on-chain
1324
- const txHash = await this._escrowClient.commitLock(
1325
- sellerWallet,
1326
- buyerEvmAddress,
1327
- payload.sessionId,
1328
- lockedAmount,
1329
- payload.buyerSig,
1330
- );
1331
-
1332
- // Initialize or update bilateral session state
1333
- let session: SellerSessionState | null | undefined = this._sessions.get(buyerPeerId);
1334
- if (!session) {
1335
- session = this._getOrCreateSellerSession(buyerPeerId, this._providers[0]?.name ?? "unknown");
1336
- }
1337
- if (session) {
1338
- // Override sessionId with the one from the lock auth (buyer-chosen)
1339
- session.sessionId = payload.sessionId;
1340
- session.sessionIdBytes = hexToBytes(payload.sessionId.replace(/^0x/, ""));
1341
- session.lockCommitted = true;
1342
- session.lockedAmount = lockedAmount;
1343
- session.runningTotal = 0n;
1344
- session.ackedRequestCount = 0;
1345
- session.lastAckedTotal = 0n;
1346
- session.awaitingAck = false;
1347
- session.buyerEvmAddress = buyerEvmAddress;
1348
- }
1349
-
1350
- debugLog(`[Node] Lock committed for buyer ${buyerPeerId.slice(0, 12)}... amount=${lockedAmount} tx=${txHash.slice(0, 12)}...`);
1351
-
1352
- paymentMux.sendSessionLockConfirm({
1353
- sessionId: payload.sessionId,
1354
- txSignature: txHash,
1355
- });
1356
- } catch (err) {
1357
- const reason = err instanceof Error ? err.message : String(err);
1358
- debugWarn(`[Node] Failed to commit lock for ${buyerPeerId.slice(0, 12)}...: ${reason}`);
1359
- paymentMux.sendSessionLockReject({
1360
- sessionId: payload.sessionId,
1361
- reason,
1362
- });
1363
- }
1364
- }
1365
-
1366
- /**
1367
- * Generate and send a bilateral receipt after processing a request (Task 3).
1368
- */
1369
- private async _sendBilateralReceipt(
1370
- _buyerPeerId: string,
1371
- session: SellerSessionState,
1372
- providerPricingUsdPerMillion: { inputUsdPerMillion: number; outputUsdPerMillion: number },
1373
- responseBody: Uint8Array,
1374
- paymentMux: PaymentMux,
1375
- ): Promise<void> {
1376
- if (!this._identity) return;
1377
-
1378
- // Calculate incremental cost in USDC base units (6 decimals)
1379
- // Estimate tokens from response body size
1380
- const tokens = this._estimateTokens(0, responseBody.length);
1381
- const costUSD =
1382
- (tokens.inputTokens * providerPricingUsdPerMillion.inputUsdPerMillion +
1383
- tokens.outputTokens * providerPricingUsdPerMillion.outputUsdPerMillion) /
1384
- 1_000_000;
1385
- const costBaseUnits = BigInt(Math.round(costUSD * 1_000_000));
1386
-
1387
- // Update running total
1388
- session.runningTotal += costBaseUnits;
1389
-
1390
- // SHA-256 hash of response body for proof of work
1391
- const responseHash = createHash("sha256").update(responseBody).digest();
1392
-
1393
- // Build receipt message and sign with Ed25519
1394
- const receiptMsg = buildReceiptMessage(
1395
- session.sessionIdBytes,
1396
- session.runningTotal,
1397
- session.totalRequests,
1398
- new Uint8Array(responseHash),
1399
- );
1400
- const sellerSig = await signMessageEd25519(this._identity, receiptMsg);
1401
-
1402
- paymentMux.sendSellerReceipt({
1403
- sessionId: session.sessionId,
1404
- runningTotal: session.runningTotal.toString(),
1405
- requestCount: session.totalRequests,
1406
- responseHash: bytesToHex(new Uint8Array(responseHash)),
1407
- sellerSig: bytesToHex(sellerSig),
1408
- });
1409
-
1410
- session.awaitingAck = true;
1411
-
1412
- // Send TopUpRequest if running total > 80% of locked amount
1413
- if (session.lockedAmount > 0n && session.runningTotal * 100n > session.lockedAmount * 80n) {
1414
- const additionalAmount = session.lockedAmount; // Request same amount again
1415
- paymentMux.sendTopUpRequest({
1416
- sessionId: session.sessionId,
1417
- additionalAmount: additionalAmount.toString(),
1418
- currentRunningTotal: session.runningTotal.toString(),
1419
- currentLockedAmount: session.lockedAmount.toString(),
1420
- });
1421
- debugLog(`[Node] TopUpRequest sent for session ${session.sessionId.slice(0, 8)}... (running=${session.runningTotal}, locked=${session.lockedAmount})`);
1422
- }
1423
- }
1424
-
1425
- /**
1426
- * Handle BuyerAck (Task 4).
1427
- * Verifies buyer's Ed25519 ack signature and updates session state.
1428
- */
1429
- private async _handleBuyerAck(buyerPeerId: string, payload: BuyerAckPayload): Promise<void> {
1430
- const session = this._sessions.get(buyerPeerId);
1431
- if (!session || !session.lockCommitted) {
1432
- debugWarn(`[Node] Received BuyerAck for unknown/uncommitted session from ${buyerPeerId.slice(0, 12)}...`);
1433
- return;
1434
- }
1435
-
1436
- try {
1437
- // Verify buyer's Ed25519 ack signature
1438
- const buyerPublicKey = hexToBytes(buyerPeerId);
1439
- const ackMsg = buildAckMessage(
1440
- session.sessionIdBytes,
1441
- BigInt(payload.runningTotal),
1442
- payload.requestCount,
1443
- );
1444
- const sigBytes = hexToBytes(payload.buyerSig);
1445
- const valid = await verifyMessageEd25519(buyerPublicKey, sigBytes, ackMsg);
1446
-
1447
- if (!valid) {
1448
- debugWarn(`[Node] Invalid BuyerAck signature from ${buyerPeerId.slice(0, 12)}...`);
1449
- return;
1450
- }
1451
-
1452
- session.ackedRequestCount = payload.requestCount;
1453
- session.lastAckedTotal = BigInt(payload.runningTotal);
1454
- session.awaitingAck = false;
1455
-
1456
- debugLog(`[Node] BuyerAck received: requestCount=${payload.requestCount} runningTotal=${payload.runningTotal}`);
1457
- } catch (err) {
1458
- debugWarn(`[Node] Failed to process BuyerAck: ${err instanceof Error ? err.message : err}`);
1459
- }
1460
- }
1461
-
1462
- /**
1463
- * Handle SessionEnd from buyer (Task 5).
1464
- * Submits settlement on-chain and cleans up.
1465
- */
1466
- private async _handleSessionEnd(buyerPeerId: string, payload: SessionEndPayload): Promise<void> {
1467
- const session = this._sessions.get(buyerPeerId);
1468
- if (!session || !session.lockCommitted) {
1469
- debugWarn(`[Node] Received SessionEnd for unknown/uncommitted session from ${buyerPeerId.slice(0, 12)}...`);
1470
- return;
1471
- }
1472
-
1473
- if (!this._identity || !this._escrowClient) {
1474
- debugWarn(`[Node] Cannot process SessionEnd — escrow client not available`);
1475
- return;
1476
- }
1477
-
1478
- try {
1479
- const sellerWallet = identityToEvmWallet(this._identity);
1480
- const sessionIdHex = "0x" + bytesToHex(session.sessionIdBytes);
1481
-
1482
- // Submit settlement on-chain with buyer's ECDSA signature and score
1483
- const txHash = await this._escrowClient.settle(
1484
- sellerWallet,
1485
- sessionIdHex,
1486
- BigInt(payload.runningTotal),
1487
- payload.score,
1488
- payload.buyerSig,
1489
- );
1490
-
1491
- debugLog(`[Node] Session settled on-chain: ${session.sessionId.slice(0, 8)}... tx=${txHash.slice(0, 12)}... score=${payload.score}`);
1492
-
1493
- // Clean up session
1494
- this._sessions.delete(buyerPeerId);
1495
- const timer = this._settlementTimers.get(buyerPeerId);
1496
- if (timer) {
1497
- clearTimeout(timer);
1498
- this._settlementTimers.delete(buyerPeerId);
1499
- }
1500
-
1501
- this.emit("session:settled", {
1502
- buyerPeerId,
1503
- sessionId: session.sessionId,
1504
- runningTotal: payload.runningTotal,
1505
- score: payload.score,
1506
- txHash,
1507
- });
1508
- } catch (err) {
1509
- debugWarn(`[Node] Failed to settle session ${session.sessionId}: ${err instanceof Error ? err.message : err}`);
1510
- }
1511
- }
1512
-
1513
- /**
1514
- * Handle TopUpAuth from buyer (Task 6).
1515
- * Calls extendLock on-chain and updates session.
1516
- */
1517
- private async _handleTopUpAuth(buyerPeerId: string, payload: TopUpAuthPayload): Promise<void> {
1518
- const session = this._sessions.get(buyerPeerId);
1519
- if (!session || !session.lockCommitted) {
1520
- debugWarn(`[Node] Received TopUpAuth for unknown/uncommitted session from ${buyerPeerId.slice(0, 12)}...`);
1521
- return;
1522
- }
1523
-
1524
- if (!this._identity || !this._escrowClient) {
1525
- debugWarn(`[Node] Cannot process TopUpAuth — escrow client not available`);
1526
- return;
1527
- }
1528
-
1529
- try {
1530
- const sellerWallet = identityToEvmWallet(this._identity);
1531
- const sessionIdHex = "0x" + bytesToHex(session.sessionIdBytes);
1532
- const additionalAmount = BigInt(payload.additionalAmount);
1533
-
1534
- const txHash = await this._escrowClient.extendLock(
1535
- sellerWallet,
1536
- sessionIdHex,
1537
- additionalAmount,
1538
- payload.buyerSig,
1539
- );
1540
-
1541
- session.lockedAmount += additionalAmount;
1542
- debugLog(`[Node] TopUp committed: session=${session.sessionId.slice(0, 8)}... additional=${additionalAmount} newTotal=${session.lockedAmount} tx=${txHash.slice(0, 12)}...`);
1543
- } catch (err) {
1544
- debugWarn(`[Node] Failed to extend lock for session ${session.sessionId}: ${err instanceof Error ? err.message : err}`);
1545
- }
1546
- }
1547
-
1548
- // ── Buyer-side payment helpers ─────────────────────────────────
1549
-
1550
- /**
1551
- * Create a PaymentMux for a buyer-side outbound connection and register
1552
- * buyer-side handlers (lock confirm, lock reject, seller receipt, top-up request).
1553
- */
1554
- private _getOrCreateBuyerPaymentMux(peerId: PeerId, conn: PeerConnection): PaymentMux {
1555
- const existing = this._paymentMuxes.get(peerId);
1556
- if (existing) return existing;
1557
-
1558
- const pmux = new PaymentMux(conn);
1559
- this._paymentMuxes.set(peerId, pmux);
1560
-
1561
- const bpm = this._buyerPaymentManager;
1562
- if (!bpm) return pmux;
1563
-
1564
- pmux.onSessionLockConfirm((payload) => {
1565
- bpm.handleLockConfirm(peerId, payload);
1566
- });
1567
-
1568
- pmux.onSessionLockReject((payload) => {
1569
- bpm.handleLockReject(peerId, payload);
1570
- });
1571
-
1572
- pmux.onSellerReceipt((receipt) => {
1573
- void bpm.handleSellerReceipt(peerId, receipt, pmux);
1574
- });
1575
-
1576
- pmux.onTopUpRequest((request) => {
1577
- void bpm.handleTopUpRequest(peerId, request, pmux);
1578
- });
1579
-
1580
- return pmux;
1581
- }
1582
-
1583
- /**
1584
- * Initiate a lock with a seller peer. Creates PaymentMux, signs lock auth,
1585
- * and waits for confirmation before returning.
1586
- */
1587
- private async _initiateBuyerLock(peer: PeerInfo, conn: PeerConnection): Promise<void> {
1588
- const bpm = this._buyerPaymentManager;
1589
- if (!bpm) return;
1590
-
1591
- // Mark as locked so we don't re-initiate
1592
- this._buyerLockedPeers.add(peer.peerId);
1593
-
1594
- const pmux = this._getOrCreateBuyerPaymentMux(peer.peerId, conn);
1595
-
1596
- // Determine seller EVM address — prefer from peer metadata
1597
- const sellerEvmAddress = peer.evmAddress ?? "";
1598
- if (!sellerEvmAddress) {
1599
- debugWarn(`[Node] Seller ${peer.peerId.slice(0, 12)}... has no EVM address; skipping lock initiation`);
1600
- return;
1601
- }
1602
-
1603
- try {
1604
- await bpm.initiateLock(peer.peerId, sellerEvmAddress, pmux);
1605
- debugLog(`[Node] Lock initiated for seller ${peer.peerId.slice(0, 12)}..., waiting for confirmation...`);
1606
-
1607
- // Wait for lock confirmation (polls every 200ms, 30s timeout)
1608
- await this._waitForLockConfirmation(peer.peerId);
1609
- debugLog(`[Node] Lock confirmed for seller ${peer.peerId.slice(0, 12)}...`);
1610
- } catch (err) {
1611
- debugWarn(`[Node] Lock initiation/confirmation failed for ${peer.peerId.slice(0, 12)}...: ${err instanceof Error ? err.message : err}`);
1612
- // Remove from locked set so next request can retry
1613
- this._buyerLockedPeers.delete(peer.peerId);
1614
- }
1615
- }
1616
-
1617
- /**
1618
- * Poll until the lock for a seller is confirmed or rejected.
1619
- * Polls every 200ms with a 30-second timeout.
1620
- */
1621
- private async _waitForLockConfirmation(sellerPeerId: string): Promise<void> {
1622
- const bpm = this._buyerPaymentManager;
1623
- if (!bpm) return;
1624
-
1625
- const pollIntervalMs = 200;
1626
- const timeoutMs = 30_000;
1627
- const deadline = Date.now() + timeoutMs;
1628
-
1629
- while (Date.now() < deadline) {
1630
- if (bpm.isLockConfirmed(sellerPeerId)) {
1631
- return;
1632
- }
1633
- if (bpm.isLockRejected(sellerPeerId)) {
1634
- throw new Error(`Lock rejected by seller ${sellerPeerId.slice(0, 12)}...`);
1635
- }
1636
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
1637
- }
1638
-
1639
- throw new Error(`Lock confirmation timed out for seller ${sellerPeerId.slice(0, 12)}... (${timeoutMs}ms)`);
1640
- }
1641
-
1642
- /**
1643
- * End all active buyer payment sessions (called during shutdown).
1644
- */
1645
- private async _endAllBuyerSessions(): Promise<void> {
1646
- const bpm = this._buyerPaymentManager;
1647
- if (!bpm) return;
1648
-
1649
- const sessions = bpm.getActiveSessions();
1650
- if (sessions.length === 0) return;
1651
-
1652
- debugLog(`[Node] Ending ${sessions.length} buyer payment session(s)...`);
1653
- await Promise.allSettled(
1654
- sessions.map((session) => {
1655
- const pmux = this._paymentMuxes.get(session.sellerPeerId as PeerId);
1656
- if (pmux) {
1657
- return bpm.endSession(session.sellerPeerId, pmux, 80);
1658
- }
1659
- return Promise.resolve();
1660
- }),
1661
- );
1662
- }
1663
-
1664
- private _lookupResultToPeerInfo(result: LookupResult): PeerInfo {
1665
- const providers = result.metadata.providers.map((p) => p.provider);
1666
- const firstProvider = result.metadata.providers[0];
1667
- const providerPricingEntries = Object.fromEntries(
1668
- result.metadata.providers.map((p) => [
1669
- p.provider,
1670
- {
1671
- defaults: {
1672
- inputUsdPerMillion: p.defaultPricing.inputUsdPerMillion,
1673
- outputUsdPerMillion: p.defaultPricing.outputUsdPerMillion,
1674
- },
1675
- ...(p.modelPricing ? { models: { ...p.modelPricing } } : {}),
1676
- },
1677
- ]),
1678
- );
1679
- const hasProviderPricing = Object.keys(providerPricingEntries).length > 0;
1680
-
1681
- return {
1682
- peerId: result.metadata.peerId,
1683
- lastSeen: result.metadata.timestamp,
1684
- providers,
1685
- publicAddress: `${result.host}:${result.port}`,
1686
- ...(hasProviderPricing ? { providerPricing: providerPricingEntries } : {}),
1687
- defaultInputUsdPerMillion: firstProvider?.defaultPricing.inputUsdPerMillion,
1688
- defaultOutputUsdPerMillion: firstProvider?.defaultPricing.outputUsdPerMillion,
1689
- maxConcurrency: firstProvider?.maxConcurrency,
1690
- currentLoad: firstProvider?.currentLoad,
1691
- evmAddress: result.metadata.evmAddress,
1692
- onChainReputation: result.metadata.onChainReputation,
1693
- onChainSessionCount: result.metadata.onChainSessionCount,
1694
- onChainDisputeCount: result.metadata.onChainDisputeCount,
1695
- trustScore: result.metadata.onChainReputation,
1696
- };
1697
- }
1698
- }