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