@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.
- package/LICENSE +674 -0
- package/README.md +7 -5
- package/dist/discovery/http-metadata-resolver.d.ts +6 -0
- package/dist/discovery/http-metadata-resolver.d.ts.map +1 -1
- package/dist/discovery/http-metadata-resolver.js +32 -4
- package/dist/discovery/http-metadata-resolver.js.map +1 -1
- package/dist/discovery/peer-lookup.d.ts +1 -0
- package/dist/discovery/peer-lookup.d.ts.map +1 -1
- package/dist/discovery/peer-lookup.js +10 -25
- package/dist/discovery/peer-lookup.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/seller-provider.d.ts +13 -1
- package/dist/interfaces/seller-provider.d.ts.map +1 -1
- package/dist/node.d.ts +13 -3
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +146 -21
- package/dist/node.js.map +1 -1
- package/dist/proxy/proxy-mux.d.ts +3 -1
- package/dist/proxy/proxy-mux.d.ts.map +1 -1
- package/dist/proxy/proxy-mux.js +9 -5
- package/dist/proxy/proxy-mux.js.map +1 -1
- package/dist/types/http.d.ts +1 -0
- package/dist/types/http.d.ts.map +1 -1
- package/dist/types/http.js +1 -1
- package/dist/types/http.js.map +1 -1
- package/package.json +14 -10
- package/contracts/AntseedEscrow.sol +0 -310
- package/contracts/MockUSDC.sol +0 -64
- package/contracts/README.md +0 -102
- package/src/config/encryption.test.ts +0 -49
- package/src/config/encryption.ts +0 -53
- package/src/config/plugin-config-manager.test.ts +0 -92
- package/src/config/plugin-config-manager.ts +0 -153
- package/src/config/plugin-loader.ts +0 -90
- package/src/discovery/announcer.ts +0 -169
- package/src/discovery/bootstrap.ts +0 -57
- package/src/discovery/default-metadata-resolver.ts +0 -18
- package/src/discovery/dht-health.ts +0 -136
- package/src/discovery/dht-node.ts +0 -191
- package/src/discovery/http-metadata-resolver.ts +0 -47
- package/src/discovery/index.ts +0 -15
- package/src/discovery/metadata-codec.ts +0 -453
- package/src/discovery/metadata-resolver.ts +0 -7
- package/src/discovery/metadata-server.ts +0 -73
- package/src/discovery/metadata-validator.ts +0 -172
- package/src/discovery/peer-lookup.ts +0 -122
- package/src/discovery/peer-metadata.ts +0 -34
- package/src/discovery/peer-selector.ts +0 -134
- package/src/discovery/profile-manager.ts +0 -131
- package/src/discovery/profile-search.ts +0 -100
- package/src/discovery/reputation-verifier.ts +0 -54
- package/src/index.ts +0 -61
- package/src/interfaces/buyer-router.ts +0 -21
- package/src/interfaces/plugin.ts +0 -36
- package/src/interfaces/seller-provider.ts +0 -81
- package/src/metering/index.ts +0 -6
- package/src/metering/receipt-generator.ts +0 -105
- package/src/metering/receipt-verifier.ts +0 -102
- package/src/metering/session-tracker.ts +0 -145
- package/src/metering/storage.ts +0 -600
- package/src/metering/token-counter.ts +0 -127
- package/src/metering/usage-aggregator.ts +0 -236
- package/src/node.ts +0 -1698
- package/src/p2p/connection-auth.ts +0 -152
- package/src/p2p/connection-manager.ts +0 -916
- package/src/p2p/handshake.ts +0 -162
- package/src/p2p/ice-config.ts +0 -59
- package/src/p2p/identity.ts +0 -110
- package/src/p2p/index.ts +0 -11
- package/src/p2p/keepalive.ts +0 -118
- package/src/p2p/message-protocol.ts +0 -171
- package/src/p2p/nat-traversal.ts +0 -169
- package/src/p2p/payment-codec.ts +0 -165
- package/src/p2p/payment-mux.ts +0 -153
- package/src/p2p/reconnect.ts +0 -117
- package/src/payments/balance-manager.ts +0 -77
- package/src/payments/buyer-payment-manager.ts +0 -414
- package/src/payments/disputes.ts +0 -72
- package/src/payments/evm/escrow-client.ts +0 -263
- package/src/payments/evm/keypair.ts +0 -31
- package/src/payments/evm/signatures.ts +0 -103
- package/src/payments/evm/wallet.ts +0 -42
- package/src/payments/index.ts +0 -50
- package/src/payments/settlement.ts +0 -40
- package/src/payments/types.ts +0 -79
- package/src/proxy/index.ts +0 -3
- package/src/proxy/provider-detection.ts +0 -78
- package/src/proxy/proxy-mux.ts +0 -173
- package/src/proxy/request-codec.ts +0 -294
- package/src/reputation/index.ts +0 -6
- package/src/reputation/rating-manager.ts +0 -118
- package/src/reputation/report-manager.ts +0 -91
- package/src/reputation/trust-engine.ts +0 -120
- package/src/reputation/trust-score.ts +0 -74
- package/src/reputation/uptime-tracker.ts +0 -155
- package/src/routing/default-router.ts +0 -75
- package/src/types/bittorrent-dht.d.ts +0 -19
- package/src/types/buyer.ts +0 -37
- package/src/types/capability.ts +0 -34
- package/src/types/connection.ts +0 -29
- package/src/types/http.ts +0 -20
- package/src/types/index.ts +0 -14
- package/src/types/metering.ts +0 -175
- package/src/types/nat-api.d.ts +0 -29
- package/src/types/peer-profile.ts +0 -25
- package/src/types/peer.ts +0 -62
- package/src/types/plugin-config.ts +0 -31
- package/src/types/protocol.ts +0 -162
- package/src/types/provider.ts +0 -40
- package/src/types/rating.ts +0 -23
- package/src/types/report.ts +0 -30
- package/src/types/seller.ts +0 -38
- package/src/types/staking.ts +0 -23
- package/src/utils/debug.ts +0 -30
- package/src/utils/hex.ts +0 -14
- package/tests/balance-manager.test.ts +0 -156
- package/tests/bootstrap.test.ts +0 -108
- package/tests/buyer-payment-manager.test.ts +0 -358
- package/tests/connection-auth.test.ts +0 -87
- package/tests/default-router.test.ts +0 -148
- package/tests/evm-keypair.test.ts +0 -173
- package/tests/identity.test.ts +0 -133
- package/tests/message-protocol.test.ts +0 -212
- package/tests/metadata-codec.test.ts +0 -165
- package/tests/metadata-validator.test.ts +0 -261
- package/tests/metering-storage.test.ts +0 -244
- package/tests/payment-codec.test.ts +0 -95
- package/tests/payment-mux.test.ts +0 -191
- package/tests/peer-selector.test.ts +0 -184
- package/tests/provider-detection.test.ts +0 -107
- package/tests/proxy-mux-security.test.ts +0 -38
- package/tests/receipt.test.ts +0 -215
- package/tests/reputation-integration.test.ts +0 -195
- package/tests/request-codec.test.ts +0 -144
- package/tests/token-counter.test.ts +0 -122
- package/tsconfig.json +0 -9
- 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
|
-
}
|