@jack-kernel/sdk 1.0.0 → 1.2.1

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 (91) hide show
  1. package/dist/cjs/index.js +125 -2
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/lifi/chain-map.js +39 -0
  4. package/dist/cjs/lifi/chain-map.js.map +1 -0
  5. package/dist/cjs/lifi/fallback.js +135 -0
  6. package/dist/cjs/lifi/fallback.js.map +1 -0
  7. package/dist/cjs/lifi/index.js +34 -0
  8. package/dist/cjs/lifi/index.js.map +1 -0
  9. package/dist/cjs/lifi/lifi-provider.js +496 -0
  10. package/dist/cjs/lifi/lifi-provider.js.map +1 -0
  11. package/dist/cjs/lifi/token-map.js +75 -0
  12. package/dist/cjs/lifi/token-map.js.map +1 -0
  13. package/dist/cjs/lifi/types.js +3 -0
  14. package/dist/cjs/lifi/types.js.map +1 -0
  15. package/dist/cjs/lifi/utils.js +45 -0
  16. package/dist/cjs/lifi/utils.js.map +1 -0
  17. package/dist/cjs/yellow/channel-state-manager.js +167 -0
  18. package/dist/cjs/yellow/channel-state-manager.js.map +1 -0
  19. package/dist/cjs/yellow/clear-node-connection.js +390 -0
  20. package/dist/cjs/yellow/clear-node-connection.js.map +1 -0
  21. package/dist/cjs/yellow/event-mapper.js +254 -0
  22. package/dist/cjs/yellow/event-mapper.js.map +1 -0
  23. package/dist/cjs/yellow/serialization.js +130 -0
  24. package/dist/cjs/yellow/serialization.js.map +1 -0
  25. package/dist/cjs/yellow/session-key-manager.js +308 -0
  26. package/dist/cjs/yellow/session-key-manager.js.map +1 -0
  27. package/dist/cjs/yellow/types.js +12 -0
  28. package/dist/cjs/yellow/types.js.map +1 -0
  29. package/dist/cjs/yellow/yellow-provider.js +1545 -0
  30. package/dist/cjs/yellow/yellow-provider.js.map +1 -0
  31. package/dist/esm/index.js +102 -1
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/esm/lifi/chain-map.js +35 -0
  34. package/dist/esm/lifi/chain-map.js.map +1 -0
  35. package/dist/esm/lifi/fallback.js +128 -0
  36. package/dist/esm/lifi/fallback.js.map +1 -0
  37. package/dist/esm/lifi/index.js +19 -0
  38. package/dist/esm/lifi/index.js.map +1 -0
  39. package/dist/esm/lifi/lifi-provider.js +492 -0
  40. package/dist/esm/lifi/lifi-provider.js.map +1 -0
  41. package/dist/esm/lifi/token-map.js +71 -0
  42. package/dist/esm/lifi/token-map.js.map +1 -0
  43. package/dist/esm/lifi/types.js +2 -0
  44. package/dist/esm/lifi/types.js.map +1 -0
  45. package/dist/esm/lifi/utils.js +41 -0
  46. package/dist/esm/lifi/utils.js.map +1 -0
  47. package/dist/esm/yellow/channel-state-manager.js +163 -0
  48. package/dist/esm/yellow/channel-state-manager.js.map +1 -0
  49. package/dist/esm/yellow/clear-node-connection.js +385 -0
  50. package/dist/esm/yellow/clear-node-connection.js.map +1 -0
  51. package/dist/esm/yellow/event-mapper.js +248 -0
  52. package/dist/esm/yellow/event-mapper.js.map +1 -0
  53. package/dist/esm/yellow/serialization.js +125 -0
  54. package/dist/esm/yellow/serialization.js.map +1 -0
  55. package/dist/esm/yellow/session-key-manager.js +302 -0
  56. package/dist/esm/yellow/session-key-manager.js.map +1 -0
  57. package/dist/esm/yellow/types.js +11 -0
  58. package/dist/esm/yellow/types.js.map +1 -0
  59. package/dist/esm/yellow/yellow-provider.js +1538 -0
  60. package/dist/esm/yellow/yellow-provider.js.map +1 -0
  61. package/dist/types/index.d.ts +104 -2
  62. package/dist/types/index.d.ts.map +1 -1
  63. package/dist/types/lifi/chain-map.d.ts +27 -0
  64. package/dist/types/lifi/chain-map.d.ts.map +1 -0
  65. package/dist/types/lifi/fallback.d.ts +58 -0
  66. package/dist/types/lifi/fallback.d.ts.map +1 -0
  67. package/dist/types/lifi/index.d.ts +18 -0
  68. package/dist/types/lifi/index.d.ts.map +1 -0
  69. package/dist/types/lifi/lifi-provider.d.ts +133 -0
  70. package/dist/types/lifi/lifi-provider.d.ts.map +1 -0
  71. package/dist/types/lifi/token-map.d.ts +34 -0
  72. package/dist/types/lifi/token-map.d.ts.map +1 -0
  73. package/dist/types/lifi/types.d.ts +52 -0
  74. package/dist/types/lifi/types.d.ts.map +1 -0
  75. package/dist/types/lifi/utils.d.ts +29 -0
  76. package/dist/types/lifi/utils.d.ts.map +1 -0
  77. package/dist/types/yellow/channel-state-manager.d.ts +106 -0
  78. package/dist/types/yellow/channel-state-manager.d.ts.map +1 -0
  79. package/dist/types/yellow/clear-node-connection.d.ts +202 -0
  80. package/dist/types/yellow/clear-node-connection.d.ts.map +1 -0
  81. package/dist/types/yellow/event-mapper.d.ts +74 -0
  82. package/dist/types/yellow/event-mapper.d.ts.map +1 -0
  83. package/dist/types/yellow/serialization.d.ts +52 -0
  84. package/dist/types/yellow/serialization.d.ts.map +1 -0
  85. package/dist/types/yellow/session-key-manager.d.ts +179 -0
  86. package/dist/types/yellow/session-key-manager.d.ts.map +1 -0
  87. package/dist/types/yellow/types.d.ts +177 -0
  88. package/dist/types/yellow/types.d.ts.map +1 -0
  89. package/dist/types/yellow/yellow-provider.d.ts +303 -0
  90. package/dist/types/yellow/yellow-provider.d.ts.map +1 -0
  91. package/package.json +4 -1
@@ -0,0 +1,1538 @@
1
+ /**
2
+ * YellowProvider - Main entry point for Yellow Network operations
3
+ *
4
+ * Orchestrates session management, channel lifecycle, clearing, and event mapping.
5
+ * Follows the same standalone-class pattern as LifiProvider.
6
+ *
7
+ * Key design decisions:
8
+ * - Constructor MAY throw if NitroliteClient initialization fails
9
+ * - All other public methods return result objects (never throw)
10
+ * - WebSocket connection management is encapsulated in ClearNodeConnection
11
+ * - Session key management is handled internally via SessionKeyManager
12
+ *
13
+ * Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.8, 10.1
14
+ */
15
+ import { createPublicClient, http } from 'viem';
16
+ import { ClearNodeConnection } from './clear-node-connection.js';
17
+ import { SessionKeyManager } from './session-key-manager.js';
18
+ import { ChannelStateManager } from './channel-state-manager.js';
19
+ import { toSerializableChannelState, toSerializableYellowQuote, } from './serialization.js';
20
+ // ============================================================================
21
+ // Error-to-ReasonCode Mapping
22
+ // ============================================================================
23
+ /**
24
+ * Centralized error-to-reason-code mapping for all YellowProvider methods.
25
+ *
26
+ * Classifies an unknown error into the appropriate YellowReasonCode based on
27
+ * the error message content. This ensures consistent error classification
28
+ * across all provider methods.
29
+ *
30
+ * Mapping rules (Requirements 13.1, 13.2, 13.3, 13.4, 13.5):
31
+ * - WebSocket errors → YELLOW_UNAVAILABLE
32
+ * - On-chain transaction reverts → YELLOW_TX_FAILED
33
+ * - Authentication failures → YELLOW_AUTH_FAILED
34
+ * - Message timeouts → YELLOW_TIMEOUT
35
+ * - ClearNode unavailable / all reconnection attempts exhausted → YELLOW_UNAVAILABLE
36
+ *
37
+ * @param error - The error to classify (may be Error, string, or unknown)
38
+ * @returns The appropriate YellowReasonCode for the error
39
+ */
40
+ export function mapErrorToReasonCode(error) {
41
+ const message = error instanceof Error ? error.message : String(error);
42
+ const lowerMessage = message.toLowerCase();
43
+ // Timeout errors → YELLOW_TIMEOUT (Requirement 13.4)
44
+ if (lowerMessage.includes('timed out') ||
45
+ lowerMessage.includes('timeout') ||
46
+ lowerMessage.includes('timed_out')) {
47
+ return 'YELLOW_TIMEOUT';
48
+ }
49
+ // Authentication failures → YELLOW_AUTH_FAILED (Requirement 13.3)
50
+ if (lowerMessage.includes('auth') ||
51
+ lowerMessage.includes('authentication') ||
52
+ lowerMessage.includes('unauthorized') ||
53
+ lowerMessage.includes('eip-712 signing') ||
54
+ lowerMessage.includes('session expired') ||
55
+ lowerMessage.includes('session invalid')) {
56
+ return 'YELLOW_AUTH_FAILED';
57
+ }
58
+ // On-chain transaction reverts → YELLOW_TX_FAILED (Requirement 13.2)
59
+ if (lowerMessage.includes('revert') ||
60
+ lowerMessage.includes('transaction failed') ||
61
+ lowerMessage.includes('tx failed') ||
62
+ lowerMessage.includes('execution reverted') ||
63
+ lowerMessage.includes('on-chain') ||
64
+ lowerMessage.includes('onchain') ||
65
+ lowerMessage.includes('contract call')) {
66
+ return 'YELLOW_TX_FAILED';
67
+ }
68
+ // WebSocket errors → YELLOW_UNAVAILABLE (Requirement 13.1)
69
+ // Also covers ClearNode unavailable and reconnection exhaustion (Requirement 13.5)
70
+ if (lowerMessage.includes('websocket') ||
71
+ lowerMessage.includes('ws ') ||
72
+ lowerMessage.includes('connection') ||
73
+ lowerMessage.includes('reconnect') ||
74
+ lowerMessage.includes('disconnected') ||
75
+ lowerMessage.includes('not connected') ||
76
+ lowerMessage.includes('clearnode') ||
77
+ lowerMessage.includes('unavailable') ||
78
+ lowerMessage.includes('econnrefused') ||
79
+ lowerMessage.includes('socket hang up')) {
80
+ return 'YELLOW_UNAVAILABLE';
81
+ }
82
+ // Channel dispute errors
83
+ if (lowerMessage.includes('dispute')) {
84
+ return 'YELLOW_CHANNEL_DISPUTE';
85
+ }
86
+ // Insufficient balance errors
87
+ if (lowerMessage.includes('insufficient') &&
88
+ lowerMessage.includes('channel')) {
89
+ return 'INSUFFICIENT_CHANNEL_BALANCE';
90
+ }
91
+ if (lowerMessage.includes('insufficient') || lowerMessage.includes('balance')) {
92
+ return 'INSUFFICIENT_BALANCE';
93
+ }
94
+ // Default: treat unknown errors as provider unavailable
95
+ // This ensures the JACK_SDK can fall back to alternative providers (Requirement 13.5)
96
+ return 'YELLOW_UNAVAILABLE';
97
+ }
98
+ /**
99
+ * Extract a revert reason from an on-chain transaction error, if available.
100
+ *
101
+ * Attempts to parse the revert reason from common error formats produced by
102
+ * viem and other Ethereum libraries.
103
+ *
104
+ * @param error - The error to extract the revert reason from
105
+ * @returns The revert reason string, or undefined if not found
106
+ */
107
+ export function extractRevertReason(error) {
108
+ const message = error instanceof Error ? error.message : String(error);
109
+ // Match common revert reason patterns
110
+ // viem format: "execution reverted: <reason>"
111
+ const revertMatch = message.match(/execution reverted:\s*(.+?)(?:\n|$)/i);
112
+ if (revertMatch) {
113
+ return revertMatch[1].trim();
114
+ }
115
+ // Generic revert pattern: "revert <reason>" or "reverted with reason: <reason>"
116
+ const genericMatch = message.match(/revert(?:ed)?(?:\s+with\s+reason)?[:\s]+(.+?)(?:\n|$)/i);
117
+ if (genericMatch) {
118
+ return genericMatch[1].trim();
119
+ }
120
+ return undefined;
121
+ }
122
+ /**
123
+ * Stub NitroliteClient class.
124
+ *
125
+ * Provides the constructor interface expected by the real @erc7824/nitrolite package.
126
+ * Channel lifecycle methods (createChannel, resizeChannel, closeChannel) are stubs
127
+ * that will be replaced when the real package is installed.
128
+ */
129
+ export class NitroliteClient {
130
+ config;
131
+ constructor(config) {
132
+ this.config = config;
133
+ }
134
+ }
135
+ // ============================================================================
136
+ // Defaults
137
+ // ============================================================================
138
+ const DEFAULT_CLEAR_NODE_URL = 'wss://clearnet-sandbox.yellow.com/ws';
139
+ const DEFAULT_CHALLENGE_DURATION = 3600;
140
+ const DEFAULT_SESSION_EXPIRY = 3600;
141
+ const DEFAULT_MESSAGE_TIMEOUT = 30000;
142
+ const DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
143
+ const DEFAULT_RECONNECT_DELAY = 1000;
144
+ // ============================================================================
145
+ // YellowProvider
146
+ // ============================================================================
147
+ /**
148
+ * Main provider class for Yellow Network operations.
149
+ *
150
+ * Orchestrates session management, channel lifecycle, clearing, and event mapping.
151
+ * The constructor initializes the NitroliteClient with viem clients and contract addresses.
152
+ * The connect() method establishes the WebSocket and completes the auth handshake.
153
+ *
154
+ * Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.8, 10.1
155
+ */
156
+ export class YellowProvider {
157
+ // Internal components
158
+ nitroliteClient;
159
+ walletClient;
160
+ publicClient;
161
+ config;
162
+ connection = null;
163
+ sessionManager = null;
164
+ channelStateManager = null;
165
+ // Event emitter state
166
+ eventHandlers = new Map();
167
+ // Provider status
168
+ _status = 'disconnected';
169
+ /**
170
+ * Creates a new YellowProvider instance.
171
+ *
172
+ * Requirement 1.1: Initialize NitroliteClient with custody/adjudicator addresses and viem clients.
173
+ * Requirement 1.2: Default clearNodeUrl to "wss://clearnet-sandbox.yellow.com/ws".
174
+ * Requirement 1.3: Convert challengeDuration to BigInt when passing to NitroliteClient.
175
+ * Requirement 1.4: Default challengeDuration to 3600 seconds.
176
+ * Requirement 1.5: Throw descriptive error if NitroliteClient initialization fails.
177
+ * Requirement 1.8: Accept all YellowConfig fields.
178
+ *
179
+ * @param config - Yellow Network configuration
180
+ * @param walletClient - viem WalletClient for signing transactions and EIP-712 messages
181
+ * @throws Error if NitroliteClient initialization fails
182
+ */
183
+ constructor(config, walletClient) {
184
+ // Store wallet client
185
+ this.walletClient = walletClient;
186
+ // Apply defaults (Requirements 1.2, 1.4)
187
+ this.config = {
188
+ ...config,
189
+ clearNodeUrl: config.clearNodeUrl ?? DEFAULT_CLEAR_NODE_URL,
190
+ challengeDuration: config.challengeDuration ?? DEFAULT_CHALLENGE_DURATION,
191
+ sessionExpiry: config.sessionExpiry ?? DEFAULT_SESSION_EXPIRY,
192
+ messageTimeout: config.messageTimeout ?? DEFAULT_MESSAGE_TIMEOUT,
193
+ maxReconnectAttempts: config.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS,
194
+ reconnectDelay: config.reconnectDelay ?? DEFAULT_RECONNECT_DELAY,
195
+ };
196
+ // Create PublicClient from rpcUrl if provided, otherwise use a minimal one
197
+ if (config.rpcUrl) {
198
+ this.publicClient = createPublicClient({
199
+ transport: http(config.rpcUrl),
200
+ });
201
+ }
202
+ else {
203
+ // Create a minimal public client - operations requiring on-chain queries
204
+ // will need a valid rpcUrl to be provided
205
+ this.publicClient = createPublicClient({
206
+ transport: http(),
207
+ });
208
+ }
209
+ // Initialize NitroliteClient (Requirements 1.1, 1.3, 1.5)
210
+ try {
211
+ this.nitroliteClient = new NitroliteClient({
212
+ publicClient: this.publicClient,
213
+ walletClient: this.walletClient,
214
+ custodyAddress: this.config.custodyAddress,
215
+ adjudicatorAddress: this.config.adjudicatorAddress,
216
+ challengeDuration: BigInt(this.config.challengeDuration), // Requirement 1.3
217
+ });
218
+ }
219
+ catch (error) {
220
+ // Requirement 1.5: Throw descriptive error
221
+ const message = error instanceof Error ? error.message : String(error);
222
+ throw new Error(`Failed to initialize NitroliteClient: ${message}. ` +
223
+ `Check custody address (${this.config.custodyAddress}), ` +
224
+ `adjudicator address (${this.config.adjudicatorAddress}), ` +
225
+ `and chain ID (${this.config.chainId}).`);
226
+ }
227
+ }
228
+ /**
229
+ * Connect to ClearNode and authenticate.
230
+ *
231
+ * Establishes a WebSocket connection via ClearNodeConnection, then authenticates
232
+ * using SessionKeyManager with the configured session expiry.
233
+ *
234
+ * Requirement 10.1: Establish WebSocket connection and emit connected event.
235
+ *
236
+ * @returns Connection result with session address on success, or fallback on failure
237
+ */
238
+ async connect() {
239
+ try {
240
+ this._status = 'connecting';
241
+ // Create ClearNodeConnection
242
+ this.connection = new ClearNodeConnection(this.config.clearNodeUrl, {
243
+ maxReconnectAttempts: this.config.maxReconnectAttempts,
244
+ reconnectDelay: this.config.reconnectDelay,
245
+ messageTimeout: this.config.messageTimeout,
246
+ });
247
+ // Wire up connection events to our event emitter
248
+ this.connection.on('connected', () => {
249
+ this._status = 'connected';
250
+ this.emit('connected', undefined);
251
+ });
252
+ this.connection.on('disconnected', () => {
253
+ this._status = 'disconnected';
254
+ this.emit('disconnected', undefined);
255
+ });
256
+ // Establish WebSocket connection
257
+ await this.connection.connect();
258
+ // Create SessionKeyManager and authenticate
259
+ this.sessionManager = new SessionKeyManager(this.walletClient, this.connection);
260
+ const authParams = {
261
+ allowances: [],
262
+ expiresAt: Math.floor(Date.now() / 1000) + this.config.sessionExpiry,
263
+ scope: 'jack-kernel',
264
+ };
265
+ const sessionInfo = await this.sessionManager.authenticate(authParams);
266
+ // Create ChannelStateManager
267
+ this.channelStateManager = new ChannelStateManager(this.publicClient, this.config.custodyAddress);
268
+ this._status = 'connected';
269
+ return {
270
+ connected: true,
271
+ sessionAddress: sessionInfo.sessionAddress,
272
+ };
273
+ }
274
+ catch (error) {
275
+ this._status = 'error';
276
+ const message = error instanceof Error ? error.message : String(error);
277
+ // Use centralized error-to-reason-code mapping (Requirements 13.1, 13.3, 13.5)
278
+ const reasonCode = mapErrorToReasonCode(error);
279
+ this.emit('error', { message, reasonCode });
280
+ return {
281
+ connected: false,
282
+ fallback: {
283
+ enabled: true,
284
+ reasonCode,
285
+ message: `Failed to connect to ClearNode: ${message}`,
286
+ },
287
+ };
288
+ }
289
+ }
290
+ /**
291
+ * Disconnect from ClearNode and clean up all resources.
292
+ *
293
+ * Closes the WebSocket connection, invalidates the session, clears channel state,
294
+ * and removes all event handlers from internal components.
295
+ *
296
+ * Requirement 10.4: Close WebSocket and clean up all pending message handlers.
297
+ */
298
+ async disconnect() {
299
+ // Close WebSocket connection
300
+ if (this.connection) {
301
+ await this.connection.disconnect();
302
+ this.connection = null;
303
+ }
304
+ // Invalidate session
305
+ if (this.sessionManager) {
306
+ this.sessionManager.invalidate();
307
+ this.sessionManager = null;
308
+ }
309
+ // Clear channel state cache
310
+ if (this.channelStateManager) {
311
+ this.channelStateManager.clear();
312
+ this.channelStateManager = null;
313
+ }
314
+ this._status = 'disconnected';
315
+ }
316
+ /**
317
+ * Check if the provider is connected and authenticated.
318
+ */
319
+ get isConnected() {
320
+ return (this._status === 'connected' &&
321
+ this.connection !== null &&
322
+ this.connection.isConnected &&
323
+ this.sessionManager !== null &&
324
+ this.sessionManager.isAuthenticated);
325
+ }
326
+ /**
327
+ * Get the current provider status.
328
+ */
329
+ get status() {
330
+ return this._status;
331
+ }
332
+ /**
333
+ * Register an event listener for YellowProvider events.
334
+ *
335
+ * @param event - The event type to listen for
336
+ * @param handler - The handler function to call when the event fires
337
+ */
338
+ on(event, handler) {
339
+ let handlers = this.eventHandlers.get(event);
340
+ if (!handlers) {
341
+ handlers = new Set();
342
+ this.eventHandlers.set(event, handlers);
343
+ }
344
+ handlers.add(handler);
345
+ }
346
+ /**
347
+ * Remove an event listener.
348
+ *
349
+ * @param event - The event type to stop listening for
350
+ * @param handler - The handler function to remove
351
+ */
352
+ off(event, handler) {
353
+ const handlers = this.eventHandlers.get(event);
354
+ if (handlers) {
355
+ handlers.delete(handler);
356
+ if (handlers.size === 0) {
357
+ this.eventHandlers.delete(event);
358
+ }
359
+ }
360
+ }
361
+ // ============================================================================
362
+ // Channel lifecycle methods (task 6.2)
363
+ // ============================================================================
364
+ /**
365
+ * Create a new state channel.
366
+ *
367
+ * Two-phase operation:
368
+ * 1. Send create_channel message to ClearNode via sendAndWait
369
+ * 2. Submit channel creation on-chain via NitroliteClient (simulated)
370
+ *
371
+ * After success, updates the ChannelStateManager cache and emits channel_created.
372
+ *
373
+ * Requirements: 3.1, 3.2, 3.3, 3.4, 3.5
374
+ *
375
+ * @param params - Channel creation parameters (chainId, token)
376
+ * @returns YellowChannelResult with channelId, state, and txHash on success; fallback on failure
377
+ */
378
+ async createChannel(params) {
379
+ // Check connection state
380
+ if (!this.connection || !this.connection.isConnected) {
381
+ return {
382
+ fallback: {
383
+ enabled: true,
384
+ reasonCode: 'YELLOW_UNAVAILABLE',
385
+ message: 'Not connected to ClearNode',
386
+ },
387
+ };
388
+ }
389
+ // Ensure session is authenticated (auto-reauthenticate if expired)
390
+ if (!this.sessionManager) {
391
+ return {
392
+ fallback: {
393
+ enabled: true,
394
+ reasonCode: 'YELLOW_UNAVAILABLE',
395
+ message: 'Session manager not initialized - call connect() first',
396
+ },
397
+ };
398
+ }
399
+ try {
400
+ await this.sessionManager.ensureAuthenticated();
401
+ }
402
+ catch (error) {
403
+ const message = error instanceof Error ? error.message : String(error);
404
+ return {
405
+ fallback: {
406
+ enabled: true,
407
+ reasonCode: 'YELLOW_AUTH_FAILED',
408
+ message: `Authentication failed: ${message}`,
409
+ },
410
+ };
411
+ }
412
+ try {
413
+ // Phase 1: Send create_channel to ClearNode (Requirement 3.1)
414
+ const createMsg = JSON.stringify({
415
+ method: 'create_channel',
416
+ params: {
417
+ chainId: params.chainId,
418
+ token: params.token,
419
+ },
420
+ });
421
+ let clearNodeResponse;
422
+ try {
423
+ clearNodeResponse = await this.connection.sendAndWait(createMsg, 'create_channel', this.config.messageTimeout);
424
+ }
425
+ catch (error) {
426
+ // Requirement 3.5: Timeout reason code; Requirement 13.1: WebSocket errors
427
+ const message = error instanceof Error ? error.message : String(error);
428
+ const reasonCode = mapErrorToReasonCode(error);
429
+ return {
430
+ fallback: {
431
+ enabled: true,
432
+ reasonCode,
433
+ message: `ClearNode create_channel failed: ${message}`,
434
+ },
435
+ };
436
+ }
437
+ // Extract channel data from ClearNode response (Requirement 3.2)
438
+ const channelId = clearNodeResponse.data?.channelId
439
+ ?? clearNodeResponse.channelId
440
+ ?? `0x${Date.now().toString(16).padStart(64, '0')}`;
441
+ // Phase 2: Submit on-chain via NitroliteClient (simulated since NitroliteClient is a stub)
442
+ // In production, this would call this.nitroliteClient.createChannel(...)
443
+ let txHash;
444
+ try {
445
+ txHash = this.simulateOnChainTransaction('createChannel', channelId);
446
+ }
447
+ catch (error) {
448
+ // Requirement 3.4: On-chain transaction failure with revert reason (Requirement 13.2)
449
+ const message = error instanceof Error ? error.message : String(error);
450
+ const revertReason = extractRevertReason(error);
451
+ return {
452
+ channelId,
453
+ fallback: {
454
+ enabled: true,
455
+ reasonCode: 'YELLOW_TX_FAILED',
456
+ message: revertReason
457
+ ? `On-chain channel creation failed: ${revertReason}`
458
+ : `On-chain channel creation failed: ${message}`,
459
+ },
460
+ };
461
+ }
462
+ // Build ChannelState (Requirement 3.3)
463
+ // Apply toSerializableChannelState to ensure BigInt values from NitroliteClient
464
+ // are converted to strings for JSON compatibility (Requirements 11.1, 11.5)
465
+ const now = Math.floor(Date.now() / 1000);
466
+ const walletAddress = this.walletClient.account?.address ?? '0x0';
467
+ const channelState = toSerializableChannelState({
468
+ channelId,
469
+ status: 'ACTIVE',
470
+ chainId: params.chainId,
471
+ token: params.token,
472
+ allocations: clearNodeResponse.data?.allocations ?? [
473
+ {
474
+ destination: walletAddress,
475
+ token: params.token,
476
+ amount: '0',
477
+ },
478
+ ],
479
+ stateVersion: clearNodeResponse.data?.stateVersion ?? 1,
480
+ stateIntent: 'INITIALIZE',
481
+ stateHash: clearNodeResponse.data?.stateHash,
482
+ adjudicator: this.config.adjudicatorAddress,
483
+ challengePeriod: this.config.challengeDuration,
484
+ createdAt: now,
485
+ updatedAt: now,
486
+ });
487
+ // Update local cache
488
+ if (this.channelStateManager) {
489
+ this.channelStateManager.updateChannel(channelId, channelState);
490
+ }
491
+ // Emit event
492
+ this.emit('channel_created', { channelId, state: channelState, txHash });
493
+ return {
494
+ channelId,
495
+ state: channelState,
496
+ txHash,
497
+ };
498
+ }
499
+ catch (error) {
500
+ const message = error instanceof Error ? error.message : String(error);
501
+ const reasonCode = mapErrorToReasonCode(error);
502
+ return {
503
+ fallback: {
504
+ enabled: true,
505
+ reasonCode,
506
+ message: `Channel creation failed: ${message}`,
507
+ },
508
+ };
509
+ }
510
+ }
511
+ /**
512
+ * Resize a channel's allocations.
513
+ *
514
+ * Two-phase operation:
515
+ * 1. Send resize_channel message to ClearNode via sendAndWait
516
+ * 2. Submit resize on-chain via NitroliteClient (simulated)
517
+ *
518
+ * After success, updates the ChannelStateManager cache and emits channel_resized.
519
+ *
520
+ * Requirements: 4.1, 4.2, 4.3, 4.4, 4.5
521
+ *
522
+ * @param params - Resize parameters (channelId, allocateAmount, optional fundsDestination)
523
+ * @returns YellowChannelResult with updated state on success; fallback on failure
524
+ */
525
+ async resizeChannel(params) {
526
+ // Check connection state
527
+ if (!this.connection || !this.connection.isConnected) {
528
+ return {
529
+ fallback: {
530
+ enabled: true,
531
+ reasonCode: 'YELLOW_UNAVAILABLE',
532
+ message: 'Not connected to ClearNode',
533
+ },
534
+ };
535
+ }
536
+ // Ensure session is authenticated
537
+ if (!this.sessionManager) {
538
+ return {
539
+ fallback: {
540
+ enabled: true,
541
+ reasonCode: 'YELLOW_UNAVAILABLE',
542
+ message: 'Session manager not initialized - call connect() first',
543
+ },
544
+ };
545
+ }
546
+ try {
547
+ await this.sessionManager.ensureAuthenticated();
548
+ }
549
+ catch (error) {
550
+ const message = error instanceof Error ? error.message : String(error);
551
+ return {
552
+ fallback: {
553
+ enabled: true,
554
+ reasonCode: 'YELLOW_AUTH_FAILED',
555
+ message: `Authentication failed: ${message}`,
556
+ },
557
+ };
558
+ }
559
+ // Requirement 4.5: Check for insufficient balance
560
+ // Validate the allocateAmount is a valid non-negative value
561
+ try {
562
+ const amount = BigInt(params.allocateAmount);
563
+ if (amount < 0n) {
564
+ return {
565
+ channelId: params.channelId,
566
+ fallback: {
567
+ enabled: true,
568
+ reasonCode: 'INSUFFICIENT_BALANCE',
569
+ message: 'Resize allocation amount cannot be negative',
570
+ },
571
+ };
572
+ }
573
+ }
574
+ catch {
575
+ return {
576
+ channelId: params.channelId,
577
+ fallback: {
578
+ enabled: true,
579
+ reasonCode: 'INSUFFICIENT_BALANCE',
580
+ message: `Invalid allocation amount: ${params.allocateAmount}`,
581
+ },
582
+ };
583
+ }
584
+ try {
585
+ // Phase 1: Send resize_channel to ClearNode (Requirement 4.1)
586
+ const resizeMsg = JSON.stringify({
587
+ method: 'resize_channel',
588
+ params: {
589
+ channelId: params.channelId,
590
+ allocateAmount: params.allocateAmount,
591
+ fundsDestination: params.fundsDestination,
592
+ },
593
+ });
594
+ let clearNodeResponse;
595
+ try {
596
+ clearNodeResponse = await this.connection.sendAndWait(resizeMsg, 'resize_channel', this.config.messageTimeout);
597
+ }
598
+ catch (error) {
599
+ const message = error instanceof Error ? error.message : String(error);
600
+ // Check if ClearNode reported insufficient balance
601
+ if (message.toLowerCase().includes('insufficient') || message.toLowerCase().includes('balance')) {
602
+ return {
603
+ channelId: params.channelId,
604
+ fallback: {
605
+ enabled: true,
606
+ reasonCode: 'INSUFFICIENT_BALANCE',
607
+ message: `Insufficient balance for resize: ${message}`,
608
+ },
609
+ };
610
+ }
611
+ // Use centralized mapping for timeout/ws errors (Requirements 13.1, 13.4)
612
+ const reasonCode = mapErrorToReasonCode(error);
613
+ return {
614
+ channelId: params.channelId,
615
+ fallback: {
616
+ enabled: true,
617
+ reasonCode,
618
+ message: `ClearNode resize_channel failed: ${message}`,
619
+ },
620
+ };
621
+ }
622
+ // Check if ClearNode response indicates insufficient balance
623
+ if (clearNodeResponse.error?.includes('insufficient') || clearNodeResponse.error?.includes('balance')) {
624
+ return {
625
+ channelId: params.channelId,
626
+ fallback: {
627
+ enabled: true,
628
+ reasonCode: 'INSUFFICIENT_BALANCE',
629
+ message: `Insufficient balance for resize: ${clearNodeResponse.error}`,
630
+ },
631
+ };
632
+ }
633
+ // Phase 2: Submit on-chain resize via NitroliteClient (simulated) (Requirement 4.2)
634
+ let txHash;
635
+ try {
636
+ txHash = this.simulateOnChainTransaction('resizeChannel', params.channelId);
637
+ }
638
+ catch (error) {
639
+ // Requirement 4.4: On-chain resize failure with revert reason (Requirement 13.2)
640
+ const message = error instanceof Error ? error.message : String(error);
641
+ const revertReason = extractRevertReason(error);
642
+ return {
643
+ channelId: params.channelId,
644
+ fallback: {
645
+ enabled: true,
646
+ reasonCode: 'YELLOW_TX_FAILED',
647
+ message: revertReason
648
+ ? `On-chain channel resize failed: ${revertReason}`
649
+ : `On-chain channel resize failed: ${message}`,
650
+ },
651
+ };
652
+ }
653
+ // Build updated ChannelState (Requirement 4.3)
654
+ // Apply toSerializableChannelState to ensure BigInt values from NitroliteClient
655
+ // are converted to strings for JSON compatibility (Requirements 11.1, 11.5)
656
+ const now = Math.floor(Date.now() / 1000);
657
+ const existingState = this.channelStateManager?.getChannel(params.channelId);
658
+ const walletAddress = this.walletClient.account?.address ?? '0x0';
659
+ const updatedAllocations = clearNodeResponse.data?.allocations ?? [
660
+ {
661
+ destination: params.fundsDestination ?? walletAddress,
662
+ token: existingState?.token ?? '',
663
+ amount: params.allocateAmount,
664
+ },
665
+ ];
666
+ const channelState = toSerializableChannelState({
667
+ channelId: params.channelId,
668
+ status: existingState?.status ?? 'ACTIVE',
669
+ chainId: existingState?.chainId ?? this.config.chainId,
670
+ token: existingState?.token ?? '',
671
+ allocations: updatedAllocations,
672
+ stateVersion: (existingState?.stateVersion ?? 0) + 1,
673
+ stateIntent: 'RESIZE',
674
+ stateHash: clearNodeResponse.data?.stateHash,
675
+ adjudicator: this.config.adjudicatorAddress,
676
+ challengePeriod: this.config.challengeDuration,
677
+ createdAt: existingState?.createdAt ?? now,
678
+ updatedAt: now,
679
+ });
680
+ // Update local cache
681
+ if (this.channelStateManager) {
682
+ this.channelStateManager.updateChannel(params.channelId, channelState);
683
+ }
684
+ // Emit event
685
+ this.emit('channel_resized', { channelId: params.channelId, state: channelState, txHash });
686
+ return {
687
+ channelId: params.channelId,
688
+ state: channelState,
689
+ txHash,
690
+ };
691
+ }
692
+ catch (error) {
693
+ const message = error instanceof Error ? error.message : String(error);
694
+ const reasonCode = mapErrorToReasonCode(error);
695
+ return {
696
+ channelId: params.channelId,
697
+ fallback: {
698
+ enabled: true,
699
+ reasonCode,
700
+ message: `Channel resize failed: ${message}`,
701
+ },
702
+ };
703
+ }
704
+ }
705
+ /**
706
+ * Close a channel and optionally withdraw funds.
707
+ *
708
+ * Two-phase operation:
709
+ * 1. Send close_channel message to ClearNode via sendAndWait
710
+ * 2. Submit close on-chain via NitroliteClient (simulated)
711
+ * 3. Optionally call custody withdrawal for the channel's tokens
712
+ *
713
+ * After success, updates the ChannelStateManager cache and emits channel_closed.
714
+ *
715
+ * Requirements: 5.1, 5.2, 5.3, 5.4, 5.5
716
+ *
717
+ * @param params - Close parameters (channelId, optional withdraw flag defaulting to true)
718
+ * @returns YellowChannelResult with FINAL status on success; fallback on failure
719
+ */
720
+ async closeChannel(params) {
721
+ // Check connection state
722
+ if (!this.connection || !this.connection.isConnected) {
723
+ return {
724
+ fallback: {
725
+ enabled: true,
726
+ reasonCode: 'YELLOW_UNAVAILABLE',
727
+ message: 'Not connected to ClearNode',
728
+ },
729
+ };
730
+ }
731
+ // Ensure session is authenticated
732
+ if (!this.sessionManager) {
733
+ return {
734
+ fallback: {
735
+ enabled: true,
736
+ reasonCode: 'YELLOW_UNAVAILABLE',
737
+ message: 'Session manager not initialized - call connect() first',
738
+ },
739
+ };
740
+ }
741
+ try {
742
+ await this.sessionManager.ensureAuthenticated();
743
+ }
744
+ catch (error) {
745
+ const message = error instanceof Error ? error.message : String(error);
746
+ return {
747
+ fallback: {
748
+ enabled: true,
749
+ reasonCode: 'YELLOW_AUTH_FAILED',
750
+ message: `Authentication failed: ${message}`,
751
+ },
752
+ };
753
+ }
754
+ // Requirement 5.5: Check if channel is in DISPUTE status
755
+ const existingState = this.channelStateManager?.getChannel(params.channelId);
756
+ if (existingState?.status === 'DISPUTE') {
757
+ return {
758
+ channelId: params.channelId,
759
+ state: existingState,
760
+ fallback: {
761
+ enabled: true,
762
+ reasonCode: 'YELLOW_CHANNEL_DISPUTE',
763
+ message: 'Cannot close channel: channel is in DISPUTE status. Wait for dispute resolution before closing.',
764
+ },
765
+ };
766
+ }
767
+ try {
768
+ // Phase 1: Send close_channel to ClearNode (Requirement 5.1)
769
+ const closeMsg = JSON.stringify({
770
+ method: 'close_channel',
771
+ params: {
772
+ channelId: params.channelId,
773
+ },
774
+ });
775
+ let clearNodeResponse;
776
+ try {
777
+ clearNodeResponse = await this.connection.sendAndWait(closeMsg, 'close_channel', this.config.messageTimeout);
778
+ }
779
+ catch (error) {
780
+ // Use centralized mapping for timeout/ws errors (Requirements 13.1, 13.4)
781
+ const message = error instanceof Error ? error.message : String(error);
782
+ const reasonCode = mapErrorToReasonCode(error);
783
+ return {
784
+ channelId: params.channelId,
785
+ fallback: {
786
+ enabled: true,
787
+ reasonCode,
788
+ message: `ClearNode close_channel failed: ${message}`,
789
+ },
790
+ };
791
+ }
792
+ // Phase 2: Submit on-chain close via NitroliteClient (simulated) (Requirement 5.2)
793
+ let txHash;
794
+ try {
795
+ txHash = this.simulateOnChainTransaction('closeChannel', params.channelId);
796
+ }
797
+ catch (error) {
798
+ // On-chain close failure with revert reason (Requirement 13.2)
799
+ const message = error instanceof Error ? error.message : String(error);
800
+ const revertReason = extractRevertReason(error);
801
+ return {
802
+ channelId: params.channelId,
803
+ fallback: {
804
+ enabled: true,
805
+ reasonCode: 'YELLOW_TX_FAILED',
806
+ message: revertReason
807
+ ? `On-chain channel close failed: ${revertReason}`
808
+ : `On-chain channel close failed: ${message}`,
809
+ },
810
+ };
811
+ }
812
+ // Phase 3: Optionally call custody withdrawal (Requirement 5.4)
813
+ const shouldWithdraw = params.withdraw !== false; // default: true
814
+ if (shouldWithdraw) {
815
+ try {
816
+ this.simulateOnChainTransaction('withdraw', params.channelId);
817
+ }
818
+ catch {
819
+ // Withdrawal failure is non-fatal - the channel is still closed
820
+ // The user can retry withdrawal separately
821
+ }
822
+ }
823
+ // Build ChannelState with FINAL status (Requirement 5.3)
824
+ // Apply toSerializableChannelState to ensure BigInt values from NitroliteClient
825
+ // are converted to strings for JSON compatibility (Requirements 11.1, 11.5)
826
+ const now = Math.floor(Date.now() / 1000);
827
+ const walletAddress = this.walletClient.account?.address ?? '0x0';
828
+ const channelState = toSerializableChannelState({
829
+ channelId: params.channelId,
830
+ status: 'FINAL',
831
+ chainId: existingState?.chainId ?? this.config.chainId,
832
+ token: existingState?.token ?? '',
833
+ allocations: clearNodeResponse.data?.allocations ?? existingState?.allocations ?? [
834
+ {
835
+ destination: walletAddress,
836
+ token: existingState?.token ?? '',
837
+ amount: '0',
838
+ },
839
+ ],
840
+ stateVersion: (existingState?.stateVersion ?? 0) + 1,
841
+ stateIntent: 'FINALIZE',
842
+ stateHash: clearNodeResponse.data?.stateHash,
843
+ adjudicator: this.config.adjudicatorAddress,
844
+ challengePeriod: this.config.challengeDuration,
845
+ createdAt: existingState?.createdAt ?? now,
846
+ updatedAt: now,
847
+ });
848
+ // Update local cache
849
+ if (this.channelStateManager) {
850
+ this.channelStateManager.updateChannel(params.channelId, channelState);
851
+ }
852
+ // Emit event
853
+ this.emit('channel_closed', { channelId: params.channelId, state: channelState, txHash });
854
+ return {
855
+ channelId: params.channelId,
856
+ state: channelState,
857
+ txHash,
858
+ };
859
+ }
860
+ catch (error) {
861
+ const message = error instanceof Error ? error.message : String(error);
862
+ const reasonCode = mapErrorToReasonCode(error);
863
+ return {
864
+ channelId: params.channelId,
865
+ fallback: {
866
+ enabled: true,
867
+ reasonCode,
868
+ message: `Channel close failed: ${message}`,
869
+ },
870
+ };
871
+ }
872
+ }
873
+ /**
874
+ * Send an offchain transfer through an open state channel.
875
+ *
876
+ * Validates the transfer amount against the sender's channel allocation,
877
+ * sends a signed transfer message to ClearNode, and returns the updated allocations.
878
+ *
879
+ * Requirements: 6.1, 6.2, 6.3, 6.4
880
+ *
881
+ * @param params - Transfer parameters (destination, allocations with asset/amount)
882
+ * @returns YellowTransferResult with success and updated allocations, or fallback on failure
883
+ */
884
+ async transfer(params) {
885
+ // Check connection state
886
+ if (!this.connection || !this.connection.isConnected) {
887
+ return {
888
+ success: false,
889
+ fallback: {
890
+ enabled: true,
891
+ reasonCode: 'YELLOW_UNAVAILABLE',
892
+ message: 'Not connected to ClearNode',
893
+ },
894
+ };
895
+ }
896
+ // Ensure session is authenticated
897
+ if (!this.sessionManager) {
898
+ return {
899
+ success: false,
900
+ fallback: {
901
+ enabled: true,
902
+ reasonCode: 'YELLOW_UNAVAILABLE',
903
+ message: 'Session manager not initialized - call connect() first',
904
+ },
905
+ };
906
+ }
907
+ try {
908
+ await this.sessionManager.ensureAuthenticated();
909
+ }
910
+ catch (error) {
911
+ const message = error instanceof Error ? error.message : String(error);
912
+ return {
913
+ success: false,
914
+ fallback: {
915
+ enabled: true,
916
+ reasonCode: 'YELLOW_AUTH_FAILED',
917
+ message: `Authentication failed: ${message}`,
918
+ },
919
+ };
920
+ }
921
+ // Requirement 6.3: Validate transfer amount against sender's channel allocation
922
+ if (this.channelStateManager) {
923
+ for (const allocation of params.allocations) {
924
+ const openChannel = this.channelStateManager.findOpenChannel(allocation.asset);
925
+ if (openChannel) {
926
+ // Find the sender's allocation in the channel
927
+ const walletAddress = this.walletClient.account?.address ?? '0x0';
928
+ const senderAllocation = openChannel.allocations.find((a) => a.destination.toLowerCase() === walletAddress.toLowerCase() && a.token.toLowerCase() === allocation.asset.toLowerCase());
929
+ const senderBalance = senderAllocation ? BigInt(senderAllocation.amount) : 0n;
930
+ const transferAmount = BigInt(allocation.amount);
931
+ if (transferAmount > senderBalance) {
932
+ return {
933
+ success: false,
934
+ fallback: {
935
+ enabled: true,
936
+ reasonCode: 'INSUFFICIENT_CHANNEL_BALANCE',
937
+ message: `Transfer amount ${allocation.amount} exceeds sender's channel allocation ${senderBalance.toString()} for asset ${allocation.asset}`,
938
+ },
939
+ };
940
+ }
941
+ }
942
+ }
943
+ }
944
+ try {
945
+ // Requirement 6.1: Send signed transfer message to ClearNode
946
+ const transferMsg = JSON.stringify({
947
+ method: 'transfer',
948
+ params: {
949
+ destination: params.destination,
950
+ allocations: params.allocations,
951
+ },
952
+ });
953
+ let clearNodeResponse;
954
+ try {
955
+ clearNodeResponse = await this.connection.sendAndWait(transferMsg, 'transfer', this.config.messageTimeout);
956
+ }
957
+ catch (error) {
958
+ // Requirement 6.4: Timeout reason code; use centralized mapping (Requirements 13.1, 13.4)
959
+ const message = error instanceof Error ? error.message : String(error);
960
+ const reasonCode = mapErrorToReasonCode(error);
961
+ return {
962
+ success: false,
963
+ fallback: {
964
+ enabled: true,
965
+ reasonCode,
966
+ message: `ClearNode transfer failed: ${message}`,
967
+ },
968
+ };
969
+ }
970
+ // Check for error in ClearNode response
971
+ if (clearNodeResponse.error) {
972
+ const errorMsg = clearNodeResponse.error.toLowerCase();
973
+ if (errorMsg.includes('insufficient') || errorMsg.includes('balance')) {
974
+ return {
975
+ success: false,
976
+ fallback: {
977
+ enabled: true,
978
+ reasonCode: 'INSUFFICIENT_CHANNEL_BALANCE',
979
+ message: `Transfer rejected: ${clearNodeResponse.error}`,
980
+ },
981
+ };
982
+ }
983
+ return {
984
+ success: false,
985
+ fallback: {
986
+ enabled: true,
987
+ reasonCode: 'YELLOW_UNAVAILABLE',
988
+ message: `Transfer rejected by ClearNode: ${clearNodeResponse.error}`,
989
+ },
990
+ };
991
+ }
992
+ // Requirement 6.2: Return transfer result with updated allocations
993
+ const updatedAllocations = clearNodeResponse.data?.allocations ?? params.allocations.map((a) => ({
994
+ destination: params.destination,
995
+ token: a.asset,
996
+ amount: a.amount,
997
+ }));
998
+ // Update local channel state cache with new allocations
999
+ if (this.channelStateManager && clearNodeResponse.data?.channelId) {
1000
+ const existingState = this.channelStateManager.getChannel(clearNodeResponse.data.channelId);
1001
+ if (existingState) {
1002
+ this.channelStateManager.updateChannel(clearNodeResponse.data.channelId, {
1003
+ ...existingState,
1004
+ allocations: updatedAllocations,
1005
+ stateVersion: clearNodeResponse.data.stateVersion ?? existingState.stateVersion + 1,
1006
+ stateIntent: 'OPERATE',
1007
+ updatedAt: Math.floor(Date.now() / 1000),
1008
+ });
1009
+ }
1010
+ }
1011
+ // Emit event
1012
+ this.emit('transfer_completed', { destination: params.destination, allocations: updatedAllocations });
1013
+ return {
1014
+ success: true,
1015
+ updatedAllocations,
1016
+ };
1017
+ }
1018
+ catch (error) {
1019
+ const message = error instanceof Error ? error.message : String(error);
1020
+ const reasonCode = mapErrorToReasonCode(error);
1021
+ return {
1022
+ success: false,
1023
+ fallback: {
1024
+ enabled: true,
1025
+ reasonCode,
1026
+ message: `Transfer failed: ${message}`,
1027
+ },
1028
+ };
1029
+ }
1030
+ }
1031
+ /**
1032
+ * Execute an intent via Yellow Network clearing.
1033
+ *
1034
+ * Validates the intent parameters, finds or creates a state channel for the
1035
+ * intent's token/chain, submits the intent for solver matching via ClearNode,
1036
+ * normalizes the solver quote into a YellowQuote, and returns a YellowExecutionResult
1037
+ * with the clearing result on success.
1038
+ *
1039
+ * Requirements: 8.1, 8.2, 8.3, 8.4, 8.5
1040
+ *
1041
+ * @param params - Intent parameters (sourceChain, destinationChain, tokenIn, tokenOut, amountIn required)
1042
+ * @returns YellowExecutionResult with clearing result on success; fallback on failure
1043
+ */
1044
+ async executeIntent(params) {
1045
+ const now = Date.now();
1046
+ // ========================================================================
1047
+ // Step 1: Validate IntentParams (Requirement 8.5)
1048
+ // Required fields: sourceChain, destinationChain, tokenIn, tokenOut, amountIn
1049
+ // Return fallback without creating a channel on validation failure.
1050
+ // ========================================================================
1051
+ const requiredFields = [
1052
+ 'sourceChain',
1053
+ 'destinationChain',
1054
+ 'tokenIn',
1055
+ 'tokenOut',
1056
+ 'amountIn',
1057
+ ];
1058
+ const missingFields = requiredFields.filter((field) => {
1059
+ const value = params[field];
1060
+ return value === undefined || value === null || String(value).trim() === '';
1061
+ });
1062
+ if (missingFields.length > 0) {
1063
+ return {
1064
+ provider: 'fallback',
1065
+ timestamp: now,
1066
+ fallback: {
1067
+ enabled: true,
1068
+ reasonCode: 'MISSING_PARAMS',
1069
+ message: `Missing required intent parameters: ${missingFields.join(', ')}`,
1070
+ },
1071
+ };
1072
+ }
1073
+ // Validate supported chains - check that chainId in config matches or that
1074
+ // the chains are not obviously invalid (non-empty strings already validated above)
1075
+ // For now, we validate that sourceChain and destinationChain are non-numeric garbage
1076
+ // A more sophisticated check could validate against a known chain registry
1077
+ if (typeof params.sourceChain !== 'string' || typeof params.destinationChain !== 'string') {
1078
+ return {
1079
+ provider: 'fallback',
1080
+ timestamp: now,
1081
+ fallback: {
1082
+ enabled: true,
1083
+ reasonCode: 'UNSUPPORTED_CHAIN',
1084
+ message: `Unsupported chain: sourceChain and destinationChain must be strings`,
1085
+ },
1086
+ };
1087
+ }
1088
+ // ========================================================================
1089
+ // Step 2: Ensure connection and authentication
1090
+ // ========================================================================
1091
+ if (!this.connection || !this.connection.isConnected) {
1092
+ return {
1093
+ provider: 'fallback',
1094
+ timestamp: now,
1095
+ fallback: {
1096
+ enabled: true,
1097
+ reasonCode: 'YELLOW_UNAVAILABLE',
1098
+ message: 'Not connected to ClearNode',
1099
+ },
1100
+ };
1101
+ }
1102
+ if (!this.sessionManager) {
1103
+ return {
1104
+ provider: 'fallback',
1105
+ timestamp: now,
1106
+ fallback: {
1107
+ enabled: true,
1108
+ reasonCode: 'YELLOW_UNAVAILABLE',
1109
+ message: 'Session manager not initialized - call connect() first',
1110
+ },
1111
+ };
1112
+ }
1113
+ try {
1114
+ await this.sessionManager.ensureAuthenticated();
1115
+ }
1116
+ catch (error) {
1117
+ const message = error instanceof Error ? error.message : String(error);
1118
+ return {
1119
+ provider: 'fallback',
1120
+ timestamp: now,
1121
+ fallback: {
1122
+ enabled: true,
1123
+ reasonCode: 'YELLOW_AUTH_FAILED',
1124
+ message: `Authentication failed: ${message}`,
1125
+ },
1126
+ };
1127
+ }
1128
+ // ========================================================================
1129
+ // Step 3: Find or create a state channel (Requirement 8.1)
1130
+ // ========================================================================
1131
+ let channel;
1132
+ if (this.channelStateManager) {
1133
+ channel = this.channelStateManager.findOpenChannel(params.tokenIn);
1134
+ }
1135
+ if (!channel) {
1136
+ // Create a new channel for this token/chain
1137
+ const createResult = await this.createChannel({
1138
+ chainId: this.config.chainId,
1139
+ token: params.tokenIn,
1140
+ });
1141
+ if (createResult.fallback) {
1142
+ return {
1143
+ provider: 'fallback',
1144
+ timestamp: now,
1145
+ fallback: createResult.fallback,
1146
+ };
1147
+ }
1148
+ channel = createResult.state;
1149
+ }
1150
+ const channelId = channel?.channelId ?? '';
1151
+ // ========================================================================
1152
+ // Step 4: Submit intent for solver matching via ClearNode
1153
+ // ========================================================================
1154
+ try {
1155
+ const intentMsg = JSON.stringify({
1156
+ method: 'submit_intent',
1157
+ params: {
1158
+ sourceChain: params.sourceChain,
1159
+ destinationChain: params.destinationChain,
1160
+ tokenIn: params.tokenIn,
1161
+ tokenOut: params.tokenOut,
1162
+ amountIn: params.amountIn,
1163
+ minAmountOut: params.minAmountOut,
1164
+ deadline: params.deadline,
1165
+ channelId,
1166
+ },
1167
+ });
1168
+ let clearNodeResponse;
1169
+ try {
1170
+ clearNodeResponse = await this.connection.sendAndWait(intentMsg, 'submit_intent', this.config.messageTimeout);
1171
+ }
1172
+ catch (error) {
1173
+ // Check if this is a timeout (Requirement 8.4: NO_SOLVER_QUOTES on timeout)
1174
+ const message = error instanceof Error ? error.message : String(error);
1175
+ const baseReasonCode = mapErrorToReasonCode(error);
1176
+ // For intent submission, timeouts specifically mean no solver quotes were received
1177
+ const reasonCode = baseReasonCode === 'YELLOW_TIMEOUT' ? 'NO_SOLVER_QUOTES' : baseReasonCode;
1178
+ return {
1179
+ provider: 'fallback',
1180
+ channelId,
1181
+ timestamp: now,
1182
+ fallback: {
1183
+ enabled: true,
1184
+ reasonCode,
1185
+ message: reasonCode === 'NO_SOLVER_QUOTES'
1186
+ ? `No solver quotes received within timeout: ${message}`
1187
+ : `ClearNode submit_intent failed: ${message}`,
1188
+ },
1189
+ };
1190
+ }
1191
+ // Check for error in ClearNode response
1192
+ if (clearNodeResponse.error) {
1193
+ return {
1194
+ provider: 'fallback',
1195
+ channelId,
1196
+ timestamp: now,
1197
+ fallback: {
1198
+ enabled: true,
1199
+ reasonCode: 'NO_SOLVER_QUOTES',
1200
+ message: `Intent submission rejected: ${clearNodeResponse.error}`,
1201
+ },
1202
+ };
1203
+ }
1204
+ // ====================================================================
1205
+ // Step 5: Normalize solver quote into YellowQuote (Requirement 8.2)
1206
+ // ====================================================================
1207
+ const responseData = clearNodeResponse.data;
1208
+ const intentId = responseData?.intentId ?? `intent-${now}`;
1209
+ if (!responseData?.quote) {
1210
+ return {
1211
+ provider: 'fallback',
1212
+ intentId,
1213
+ channelId,
1214
+ timestamp: now,
1215
+ fallback: {
1216
+ enabled: true,
1217
+ reasonCode: 'NO_SOLVER_QUOTES',
1218
+ message: 'No solver quotes received from ClearNode',
1219
+ },
1220
+ };
1221
+ }
1222
+ const quoteTimestamp = Math.floor(now / 1000);
1223
+ // Apply toSerializableYellowQuote to ensure BigInt values from ClearNode
1224
+ // are converted to strings for JSON compatibility (Requirement 11.2)
1225
+ const quote = toSerializableYellowQuote({
1226
+ solverId: responseData.quote.solverId ?? 'unknown',
1227
+ channelId,
1228
+ amountIn: responseData.quote.amountIn ?? params.amountIn,
1229
+ amountOut: responseData.quote.amountOut ?? params.minAmountOut,
1230
+ estimatedTime: responseData.quote.estimatedTime ?? 60,
1231
+ timestamp: quoteTimestamp,
1232
+ });
1233
+ // Emit quote received event
1234
+ this.emit('quote_received', { intentId, quote });
1235
+ // ====================================================================
1236
+ // Step 6: Build ClearingResult if clearing data is present (Requirement 8.3)
1237
+ // ====================================================================
1238
+ let clearing;
1239
+ if (responseData.clearing) {
1240
+ const clearingData = responseData.clearing;
1241
+ clearing = {
1242
+ channelId: clearingData.channelId ?? channelId,
1243
+ matchedAmountIn: clearingData.matchedAmountIn ?? quote.amountIn,
1244
+ matchedAmountOut: clearingData.matchedAmountOut ?? quote.amountOut,
1245
+ netSettlement: clearingData.netSettlement ?? '0',
1246
+ timestamp: Math.floor(now / 1000),
1247
+ };
1248
+ // Include settlement proof if available
1249
+ if (clearingData.settlementProof) {
1250
+ clearing.settlementProof = {
1251
+ stateHash: clearingData.settlementProof.stateHash ?? '',
1252
+ signatures: clearingData.settlementProof.signatures ?? [],
1253
+ txHash: clearingData.settlementProof.txHash,
1254
+ finalAllocations: clearingData.settlementProof.finalAllocations ?? [],
1255
+ };
1256
+ }
1257
+ // Emit clearing completed event
1258
+ this.emit('clearing_completed', { intentId, clearing });
1259
+ }
1260
+ // ====================================================================
1261
+ // Step 7: Return YellowExecutionResult (Requirement 8.1)
1262
+ // ====================================================================
1263
+ return {
1264
+ provider: 'yellow',
1265
+ intentId,
1266
+ quote,
1267
+ clearing,
1268
+ channelId,
1269
+ timestamp: now,
1270
+ };
1271
+ }
1272
+ catch (error) {
1273
+ const message = error instanceof Error ? error.message : String(error);
1274
+ const reasonCode = mapErrorToReasonCode(error);
1275
+ return {
1276
+ provider: 'fallback',
1277
+ channelId,
1278
+ timestamp: now,
1279
+ fallback: {
1280
+ enabled: true,
1281
+ reasonCode,
1282
+ message: `Intent execution failed: ${message}`,
1283
+ },
1284
+ };
1285
+ }
1286
+ }
1287
+ /**
1288
+ * Query channel list and balances.
1289
+ *
1290
+ * When connected to ClearNode, sends a get_ledger_balances message and parses
1291
+ * the response into ChannelState objects, updating the local cache.
1292
+ * When disconnected, falls back to the ChannelStateManager's cached data.
1293
+ *
1294
+ * Requirements: 7.1, 7.4
1295
+ *
1296
+ * @returns YellowChannelsResult with the list of channels
1297
+ */
1298
+ async getChannels() {
1299
+ // Requirement 7.4: Fall back to cached on-chain data if disconnected
1300
+ if (!this.connection || !this.connection.isConnected) {
1301
+ if (this.channelStateManager) {
1302
+ const cachedChannels = this.channelStateManager.getAllChannels();
1303
+ return {
1304
+ channels: cachedChannels,
1305
+ };
1306
+ }
1307
+ return {
1308
+ channels: [],
1309
+ fallback: {
1310
+ enabled: true,
1311
+ reasonCode: 'YELLOW_UNAVAILABLE',
1312
+ message: 'Not connected to ClearNode and no cached channel data available',
1313
+ },
1314
+ };
1315
+ }
1316
+ try {
1317
+ // Requirement 7.1: Send get_ledger_balances to ClearNode
1318
+ const queryMsg = JSON.stringify({
1319
+ method: 'get_ledger_balances',
1320
+ params: {},
1321
+ });
1322
+ let clearNodeResponse;
1323
+ try {
1324
+ clearNodeResponse = await this.connection.sendAndWait(queryMsg, 'get_ledger_balances', this.config.messageTimeout);
1325
+ }
1326
+ catch (error) {
1327
+ // On timeout or error, fall back to cached data
1328
+ const message = error instanceof Error ? error.message : String(error);
1329
+ if (this.channelStateManager) {
1330
+ const cachedChannels = this.channelStateManager.getAllChannels();
1331
+ if (cachedChannels.length > 0) {
1332
+ return {
1333
+ channels: cachedChannels,
1334
+ };
1335
+ }
1336
+ }
1337
+ // Use centralized mapping (Requirements 13.1, 13.4)
1338
+ const reasonCode = mapErrorToReasonCode(error);
1339
+ return {
1340
+ channels: [],
1341
+ fallback: {
1342
+ enabled: true,
1343
+ reasonCode,
1344
+ message: `ClearNode get_ledger_balances failed: ${message}`,
1345
+ },
1346
+ };
1347
+ }
1348
+ // Parse response into ChannelState array
1349
+ // Apply toSerializableChannelState to ensure BigInt values are converted
1350
+ // to strings for JSON compatibility (Requirements 11.1, 11.5)
1351
+ const channels = (clearNodeResponse.data?.channels ?? []).map((ch) => toSerializableChannelState({
1352
+ channelId: ch.channelId,
1353
+ status: ch.status,
1354
+ chainId: ch.chainId,
1355
+ token: ch.token,
1356
+ allocations: ch.allocations,
1357
+ stateVersion: ch.stateVersion,
1358
+ stateIntent: ch.stateIntent,
1359
+ stateHash: ch.stateHash,
1360
+ adjudicator: ch.adjudicator,
1361
+ challengePeriod: ch.challengePeriod,
1362
+ challengeExpiration: ch.challengeExpiration,
1363
+ createdAt: ch.createdAt,
1364
+ updatedAt: ch.updatedAt,
1365
+ }));
1366
+ // Update local cache with fresh data
1367
+ if (this.channelStateManager) {
1368
+ for (const channel of channels) {
1369
+ this.channelStateManager.updateChannel(channel.channelId, channel);
1370
+ }
1371
+ }
1372
+ return {
1373
+ channels,
1374
+ };
1375
+ }
1376
+ catch (error) {
1377
+ const message = error instanceof Error ? error.message : String(error);
1378
+ // Fall back to cached data on unexpected errors
1379
+ if (this.channelStateManager) {
1380
+ const cachedChannels = this.channelStateManager.getAllChannels();
1381
+ if (cachedChannels.length > 0) {
1382
+ return {
1383
+ channels: cachedChannels,
1384
+ };
1385
+ }
1386
+ }
1387
+ const reasonCode = mapErrorToReasonCode(error);
1388
+ return {
1389
+ channels: [],
1390
+ fallback: {
1391
+ enabled: true,
1392
+ reasonCode,
1393
+ message: `Failed to query channels: ${message}`,
1394
+ },
1395
+ };
1396
+ }
1397
+ }
1398
+ /**
1399
+ * Query a specific channel's on-chain state.
1400
+ *
1401
+ * Queries the on-chain custody contract via ChannelStateManager.queryOnChainBalances
1402
+ * for authoritative balance data, and also checks the local cache for additional
1403
+ * channel metadata.
1404
+ *
1405
+ * Requirements: 7.2, 7.3
1406
+ *
1407
+ * @param channelId - The channel identifier to query
1408
+ * @returns YellowChannelResult with the channel state
1409
+ */
1410
+ async getChannelState(channelId) {
1411
+ if (!this.channelStateManager) {
1412
+ return {
1413
+ channelId,
1414
+ fallback: {
1415
+ enabled: true,
1416
+ reasonCode: 'YELLOW_UNAVAILABLE',
1417
+ message: 'Channel state manager not initialized - call connect() first',
1418
+ },
1419
+ };
1420
+ }
1421
+ // Check local cache first for channel metadata
1422
+ const cachedState = this.channelStateManager.getChannel(channelId);
1423
+ // Requirement 7.2: Query on-chain custody contract for authoritative balances
1424
+ try {
1425
+ const tokens = cachedState
1426
+ ? [cachedState.token]
1427
+ : [];
1428
+ if (tokens.length > 0) {
1429
+ const onChainBalances = await this.channelStateManager.queryOnChainBalances(channelId, tokens);
1430
+ // Merge on-chain balances with cached state
1431
+ // Apply toSerializableChannelState to ensure BigInt values from on-chain queries
1432
+ // are converted to strings for JSON compatibility (Requirements 11.1, 11.5)
1433
+ if (cachedState) {
1434
+ const updatedAllocations = cachedState.allocations.map((alloc, index) => ({
1435
+ ...alloc,
1436
+ amount: index < onChainBalances.length ? onChainBalances[index].toString() : alloc.amount,
1437
+ }));
1438
+ const updatedState = toSerializableChannelState({
1439
+ ...cachedState,
1440
+ allocations: updatedAllocations,
1441
+ updatedAt: Math.floor(Date.now() / 1000),
1442
+ });
1443
+ // Update cache with on-chain data
1444
+ this.channelStateManager.updateChannel(channelId, updatedState);
1445
+ return {
1446
+ channelId,
1447
+ state: updatedState,
1448
+ };
1449
+ }
1450
+ }
1451
+ }
1452
+ catch {
1453
+ // On-chain query failed - fall through to return cached data if available
1454
+ }
1455
+ // Return cached state if available (even without on-chain refresh)
1456
+ if (cachedState) {
1457
+ return {
1458
+ channelId,
1459
+ state: cachedState,
1460
+ };
1461
+ }
1462
+ // No cached state and no on-chain data available
1463
+ return {
1464
+ channelId,
1465
+ fallback: {
1466
+ enabled: true,
1467
+ reasonCode: 'YELLOW_UNAVAILABLE',
1468
+ message: `No channel state found for channel ${channelId}`,
1469
+ },
1470
+ };
1471
+ }
1472
+ // ============================================================================
1473
+ // Internal helpers
1474
+ // ============================================================================
1475
+ /**
1476
+ * Emit an event to all registered handlers.
1477
+ *
1478
+ * @param event - The event type to emit
1479
+ * @param data - The data to pass to handlers
1480
+ */
1481
+ emit(event, data) {
1482
+ const handlers = this.eventHandlers.get(event);
1483
+ if (handlers) {
1484
+ for (const handler of handlers) {
1485
+ try {
1486
+ handler(data);
1487
+ }
1488
+ catch {
1489
+ // Swallow handler errors to prevent one handler from breaking others
1490
+ }
1491
+ }
1492
+ }
1493
+ }
1494
+ /**
1495
+ * Simulate an on-chain transaction via NitroliteClient.
1496
+ *
1497
+ * Since NitroliteClient is a stub, this generates a deterministic transaction hash
1498
+ * based on the operation and channel ID. In production, this would call the actual
1499
+ * NitroliteClient methods (createChannel, resizeChannel, closeChannel, withdraw).
1500
+ *
1501
+ * @param operation - The on-chain operation name
1502
+ * @param channelId - The channel identifier
1503
+ * @returns A simulated transaction hash
1504
+ */
1505
+ simulateOnChainTransaction(operation, channelId) {
1506
+ // In production, this would dispatch to the real NitroliteClient:
1507
+ // - 'createChannel' → this.nitroliteClient.createChannel(...)
1508
+ // - 'resizeChannel' → this.nitroliteClient.resizeChannel(...)
1509
+ // - 'closeChannel' → this.nitroliteClient.closeChannel(...)
1510
+ // - 'withdraw' → this.nitroliteClient.withdraw(...)
1511
+ //
1512
+ // Since NitroliteClient is a stub, generate a deterministic tx hash.
1513
+ const timestamp = Date.now();
1514
+ const hashInput = `${operation}:${channelId}:${timestamp}`;
1515
+ // Simple hash: use the hex encoding of the operation+channelId+timestamp
1516
+ let hash = 0;
1517
+ for (let i = 0; i < hashInput.length; i++) {
1518
+ const char = hashInput.charCodeAt(i);
1519
+ hash = ((hash << 5) - hash + char) | 0;
1520
+ }
1521
+ return `0x${Math.abs(hash).toString(16).padStart(64, '0')}`;
1522
+ }
1523
+ /**
1524
+ * Get the internal NitroliteClient instance.
1525
+ * Exposed for testing and advanced use cases.
1526
+ */
1527
+ getNitroliteClient() {
1528
+ return this.nitroliteClient;
1529
+ }
1530
+ /**
1531
+ * Get the resolved configuration with defaults applied.
1532
+ * Exposed for testing.
1533
+ */
1534
+ getResolvedConfig() {
1535
+ return this.config;
1536
+ }
1537
+ }
1538
+ //# sourceMappingURL=yellow-provider.js.map