@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.
- package/dist/cjs/index.js +125 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lifi/chain-map.js +39 -0
- package/dist/cjs/lifi/chain-map.js.map +1 -0
- package/dist/cjs/lifi/fallback.js +135 -0
- package/dist/cjs/lifi/fallback.js.map +1 -0
- package/dist/cjs/lifi/index.js +34 -0
- package/dist/cjs/lifi/index.js.map +1 -0
- package/dist/cjs/lifi/lifi-provider.js +496 -0
- package/dist/cjs/lifi/lifi-provider.js.map +1 -0
- package/dist/cjs/lifi/token-map.js +75 -0
- package/dist/cjs/lifi/token-map.js.map +1 -0
- package/dist/cjs/lifi/types.js +3 -0
- package/dist/cjs/lifi/types.js.map +1 -0
- package/dist/cjs/lifi/utils.js +45 -0
- package/dist/cjs/lifi/utils.js.map +1 -0
- package/dist/cjs/yellow/channel-state-manager.js +167 -0
- package/dist/cjs/yellow/channel-state-manager.js.map +1 -0
- package/dist/cjs/yellow/clear-node-connection.js +390 -0
- package/dist/cjs/yellow/clear-node-connection.js.map +1 -0
- package/dist/cjs/yellow/event-mapper.js +254 -0
- package/dist/cjs/yellow/event-mapper.js.map +1 -0
- package/dist/cjs/yellow/serialization.js +130 -0
- package/dist/cjs/yellow/serialization.js.map +1 -0
- package/dist/cjs/yellow/session-key-manager.js +308 -0
- package/dist/cjs/yellow/session-key-manager.js.map +1 -0
- package/dist/cjs/yellow/types.js +12 -0
- package/dist/cjs/yellow/types.js.map +1 -0
- package/dist/cjs/yellow/yellow-provider.js +1545 -0
- package/dist/cjs/yellow/yellow-provider.js.map +1 -0
- package/dist/esm/index.js +102 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lifi/chain-map.js +35 -0
- package/dist/esm/lifi/chain-map.js.map +1 -0
- package/dist/esm/lifi/fallback.js +128 -0
- package/dist/esm/lifi/fallback.js.map +1 -0
- package/dist/esm/lifi/index.js +19 -0
- package/dist/esm/lifi/index.js.map +1 -0
- package/dist/esm/lifi/lifi-provider.js +492 -0
- package/dist/esm/lifi/lifi-provider.js.map +1 -0
- package/dist/esm/lifi/token-map.js +71 -0
- package/dist/esm/lifi/token-map.js.map +1 -0
- package/dist/esm/lifi/types.js +2 -0
- package/dist/esm/lifi/types.js.map +1 -0
- package/dist/esm/lifi/utils.js +41 -0
- package/dist/esm/lifi/utils.js.map +1 -0
- package/dist/esm/yellow/channel-state-manager.js +163 -0
- package/dist/esm/yellow/channel-state-manager.js.map +1 -0
- package/dist/esm/yellow/clear-node-connection.js +385 -0
- package/dist/esm/yellow/clear-node-connection.js.map +1 -0
- package/dist/esm/yellow/event-mapper.js +248 -0
- package/dist/esm/yellow/event-mapper.js.map +1 -0
- package/dist/esm/yellow/serialization.js +125 -0
- package/dist/esm/yellow/serialization.js.map +1 -0
- package/dist/esm/yellow/session-key-manager.js +302 -0
- package/dist/esm/yellow/session-key-manager.js.map +1 -0
- package/dist/esm/yellow/types.js +11 -0
- package/dist/esm/yellow/types.js.map +1 -0
- package/dist/esm/yellow/yellow-provider.js +1538 -0
- package/dist/esm/yellow/yellow-provider.js.map +1 -0
- package/dist/types/index.d.ts +104 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lifi/chain-map.d.ts +27 -0
- package/dist/types/lifi/chain-map.d.ts.map +1 -0
- package/dist/types/lifi/fallback.d.ts +58 -0
- package/dist/types/lifi/fallback.d.ts.map +1 -0
- package/dist/types/lifi/index.d.ts +18 -0
- package/dist/types/lifi/index.d.ts.map +1 -0
- package/dist/types/lifi/lifi-provider.d.ts +133 -0
- package/dist/types/lifi/lifi-provider.d.ts.map +1 -0
- package/dist/types/lifi/token-map.d.ts +34 -0
- package/dist/types/lifi/token-map.d.ts.map +1 -0
- package/dist/types/lifi/types.d.ts +52 -0
- package/dist/types/lifi/types.d.ts.map +1 -0
- package/dist/types/lifi/utils.d.ts +29 -0
- package/dist/types/lifi/utils.d.ts.map +1 -0
- package/dist/types/yellow/channel-state-manager.d.ts +106 -0
- package/dist/types/yellow/channel-state-manager.d.ts.map +1 -0
- package/dist/types/yellow/clear-node-connection.d.ts +202 -0
- package/dist/types/yellow/clear-node-connection.d.ts.map +1 -0
- package/dist/types/yellow/event-mapper.d.ts +74 -0
- package/dist/types/yellow/event-mapper.d.ts.map +1 -0
- package/dist/types/yellow/serialization.d.ts +52 -0
- package/dist/types/yellow/serialization.d.ts.map +1 -0
- package/dist/types/yellow/session-key-manager.d.ts +179 -0
- package/dist/types/yellow/session-key-manager.d.ts.map +1 -0
- package/dist/types/yellow/types.d.ts +177 -0
- package/dist/types/yellow/types.d.ts.map +1 -0
- package/dist/types/yellow/yellow-provider.d.ts +303 -0
- package/dist/types/yellow/yellow-provider.d.ts.map +1 -0
- 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
|