@invisible-labs/sdk 0.6.0-devnet.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 (54) hide show
  1. package/README.md +210 -0
  2. package/dist/chunk-7DSVN3MV.js +1145 -0
  3. package/dist/chunk-7DSVN3MV.js.map +1 -0
  4. package/dist/chunk-JNNVPZEU.js +599 -0
  5. package/dist/chunk-JNNVPZEU.js.map +1 -0
  6. package/dist/chunk-JRMKCE3S.js +50 -0
  7. package/dist/chunk-JRMKCE3S.js.map +1 -0
  8. package/dist/chunk-LBC7ALYO.js +3783 -0
  9. package/dist/chunk-LBC7ALYO.js.map +1 -0
  10. package/dist/chunk-NCY4FCYU.js +1911 -0
  11. package/dist/chunk-NCY4FCYU.js.map +1 -0
  12. package/dist/chunk-OHIM2YWU.js +126 -0
  13. package/dist/chunk-OHIM2YWU.js.map +1 -0
  14. package/dist/chunk-SZAYO2L5.js +123 -0
  15. package/dist/chunk-SZAYO2L5.js.map +1 -0
  16. package/dist/chunk-TQNNTV5F.js +17 -0
  17. package/dist/chunk-TQNNTV5F.js.map +1 -0
  18. package/dist/chunk-VR6T6OJS.js +24 -0
  19. package/dist/chunk-VR6T6OJS.js.map +1 -0
  20. package/dist/chunk-XUWBGET5.js +221 -0
  21. package/dist/chunk-XUWBGET5.js.map +1 -0
  22. package/dist/coordinator-7Y45MCCZ.js +4 -0
  23. package/dist/coordinator-7Y45MCCZ.js.map +1 -0
  24. package/dist/createSession-D3ym-Ira.d.ts +177 -0
  25. package/dist/dkgWorker.d.ts +2 -0
  26. package/dist/dkgWorker.js +61 -0
  27. package/dist/dkgWorker.js.map +1 -0
  28. package/dist/events.d.ts +54 -0
  29. package/dist/events.js +143 -0
  30. package/dist/events.js.map +1 -0
  31. package/dist/frostRuntime-JESDHN6O.js +4 -0
  32. package/dist/frostRuntime-JESDHN6O.js.map +1 -0
  33. package/dist/index.d.ts +499 -0
  34. package/dist/index.js +132 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/lp.d.ts +388 -0
  37. package/dist/lp.js +1711 -0
  38. package/dist/lp.js.map +1 -0
  39. package/dist/presets.d.ts +43 -0
  40. package/dist/presets.js +70 -0
  41. package/dist/presets.js.map +1 -0
  42. package/dist/session-DFuy54C-.d.ts +31 -0
  43. package/dist/stats.d.ts +47 -0
  44. package/dist/stats.js +21 -0
  45. package/dist/stats.js.map +1 -0
  46. package/dist/storage.d.ts +82 -0
  47. package/dist/storage.js +246 -0
  48. package/dist/storage.js.map +1 -0
  49. package/dist/types-ZhV7TIQY.d.ts +76 -0
  50. package/dist/types.generated-CHGSbmLp.d.ts +67 -0
  51. package/dist/user.d.ts +377 -0
  52. package/dist/user.js +1225 -0
  53. package/dist/user.js.map +1 -0
  54. package/package.json +82 -0
package/dist/user.js ADDED
@@ -0,0 +1,1225 @@
1
+ import { scheduledPayoutWindowSeconds } from './chunk-LBC7ALYO.js';
2
+ import { createSession, createExponentialBackoff, validatePayoutPolicyShape, formatAjvErrors } from './chunk-7DSVN3MV.js';
3
+ import { PRODUCT_MAX_ENTRY_LAMPORTS, PRODUCT_MIN_ENTRY_LAMPORTS } from './chunk-TQNNTV5F.js';
4
+ import { solanaAddressFromPublicKeyBytes, publicKey, assertAttested } from './chunk-SZAYO2L5.js';
5
+ import { CoordinatorMessageType, onEnvelope, closeProtocolRuntime, EnvelopeKind, requestEnvelope } from './chunk-XUWBGET5.js';
6
+ import { getSessionState } from './chunk-JRMKCE3S.js';
7
+ import './chunk-NCY4FCYU.js';
8
+ import { TransportError, CommandError, InvalidDepositAmountError, normalizeError, PolicyValidationError, NotImplementedError, RoutingError, AttestationError } from './chunk-OHIM2YWU.js';
9
+ export { InvalidDepositAmountError } from './chunk-OHIM2YWU.js';
10
+ import './chunk-VR6T6OJS.js';
11
+
12
+ // src/validation/payoutPolicy.ts
13
+ var PAYOUT_WINDOW_MILLISECONDS_PER_SECOND = 1e3;
14
+ var INSTANT_PAYOUT_WINDOW_MS = 0;
15
+ var SCHEDULED_PAYOUT_WINDOW_DURATIONS_MS = Object.freeze(
16
+ scheduledPayoutWindowSeconds().map(
17
+ (windowSeconds) => windowSeconds * PAYOUT_WINDOW_MILLISECONDS_PER_SECOND
18
+ )
19
+ );
20
+ var SUPPORTED_TOTAL_DEADLINE_MS = Object.freeze([
21
+ INSTANT_PAYOUT_WINDOW_MS,
22
+ ...SCHEDULED_PAYOUT_WINDOW_DURATIONS_MS
23
+ ]);
24
+ var SUPPORTED_TOTAL_DEADLINE_MS_SET = new Set(SUPPORTED_TOTAL_DEADLINE_MS);
25
+ Math.max(...SUPPORTED_TOTAL_DEADLINE_MS);
26
+ var SHARE_SUM_TOLERANCE = 0.01;
27
+ var CURRENT_USER_PAYOUT_DESTINATION_LIMIT = 1;
28
+ var CURRENT_USER_PAYOUT_DESTINATION_LIMIT_MESSAGE = "current V0 normal-user SDK and coordinator support exactly one payout destination; multi-address receive is not supported yet";
29
+ var CURRENT_USER_BATCH_EXCLUSION_ID_UNSUPPORTED_MESSAGE = "batchExclusionId is not supported in V0 normal-user payout requests; V1 defines it as campaign-scoped batch exclusion, never a stable actor identity";
30
+ var CURRENT_USER_MIN_DELAY_UNSUPPORTED_MESSAGE = "destinations[*].minDelayMs is not supported in V0 normal-user payout requests";
31
+ var CURRENT_USER_MAX_DELAY_UNSUPPORTED_MESSAGE = "destinations[*].maxDelayMs is not supported in V0 normal-user payout requests";
32
+ var CURRENT_USER_PAYOUT_FRAGMENT_LIMIT = 1;
33
+ var CURRENT_USER_PAYOUT_FRAGMENT_LIMIT_MESSAGE = "current SDK and coordinator support exactly one scheduled payout fragment; multi-fragment receive is not supported yet";
34
+ function validatePayoutPolicy(input, ctx = {}) {
35
+ if (!validatePayoutPolicyShape(input)) {
36
+ throw new PolicyValidationError(
37
+ "INVALID_PAYOUT_PLAN",
38
+ `payout policy shape invalid: ${formatAjvErrors(validatePayoutPolicyShape.errors)}`
39
+ );
40
+ }
41
+ const policy = input;
42
+ const { destinations } = policy;
43
+ assertCurrentUserUnsupportedPayoutFeatures(policy);
44
+ assertCurrentUserPayoutDestinationLimit(destinations.length);
45
+ for (const { address } of destinations) {
46
+ publicKey(address);
47
+ }
48
+ const seen = /* @__PURE__ */ new Set();
49
+ for (const { address } of destinations) {
50
+ if (seen.has(address)) {
51
+ throw new PolicyValidationError(
52
+ "INVALID_PAYOUT_PLAN",
53
+ `duplicate payout destination: ${address}`
54
+ );
55
+ }
56
+ seen.add(address);
57
+ }
58
+ if (ctx.originAddress !== void 0 && seen.has(ctx.originAddress)) {
59
+ throw new PolicyValidationError(
60
+ "INVALID_PAYOUT_PLAN",
61
+ "a payout destination must not be the deposit origin"
62
+ );
63
+ }
64
+ const shareSum = destinations.reduce((acc, d) => acc + d.sharePercent, 0);
65
+ if (Math.abs(shareSum - 100) > SHARE_SUM_TOLERANCE) {
66
+ throw new PolicyValidationError(
67
+ "INVALID_PAYOUT_PLAN",
68
+ `destination shares must sum to 100, got ${shareSum}`
69
+ );
70
+ }
71
+ for (const d of destinations) {
72
+ if (d.minLamports !== void 0 && ctx.totalLamports !== void 0) {
73
+ const shareFloor = Math.floor(ctx.totalLamports * d.sharePercent / 100);
74
+ if (d.minLamports > shareFloor) {
75
+ throw new PolicyValidationError(
76
+ "INVALID_AMOUNT",
77
+ `minLamports (${d.minLamports}) exceeds the implied share floor (${shareFloor}) for ${d.address}`
78
+ );
79
+ }
80
+ }
81
+ }
82
+ if (policy.totalDeadlineMs !== void 0) {
83
+ if (!SUPPORTED_TOTAL_DEADLINE_MS_SET.has(policy.totalDeadlineMs)) {
84
+ throw new PolicyValidationError(
85
+ "INVALID_PAYOUT_PLAN",
86
+ `totalDeadlineMs (${policy.totalDeadlineMs}) must be one of [${SUPPORTED_TOTAL_DEADLINE_MS.join(", ")}] milliseconds; 0 selects instant payout`
87
+ );
88
+ }
89
+ }
90
+ return policy;
91
+ }
92
+ function assertCurrentUserUnsupportedPayoutFeatures(policy) {
93
+ if (policy.batchExclusionId !== void 0) {
94
+ throw new PolicyValidationError(
95
+ "INVALID_PAYOUT_PLAN",
96
+ CURRENT_USER_BATCH_EXCLUSION_ID_UNSUPPORTED_MESSAGE
97
+ );
98
+ }
99
+ for (const destination of policy.destinations) {
100
+ if (destination.minDelayMs !== void 0) {
101
+ throw new PolicyValidationError(
102
+ "INVALID_PAYOUT_PLAN",
103
+ CURRENT_USER_MIN_DELAY_UNSUPPORTED_MESSAGE
104
+ );
105
+ }
106
+ if (destination.maxDelayMs !== void 0) {
107
+ throw new PolicyValidationError(
108
+ "INVALID_PAYOUT_PLAN",
109
+ CURRENT_USER_MAX_DELAY_UNSUPPORTED_MESSAGE
110
+ );
111
+ }
112
+ }
113
+ }
114
+ function assertCurrentUserPayoutDestinationLimit(destinationCount) {
115
+ if (destinationCount !== CURRENT_USER_PAYOUT_DESTINATION_LIMIT) {
116
+ throw new PolicyValidationError(
117
+ "INVALID_PAYOUT_PLAN",
118
+ `${CURRENT_USER_PAYOUT_DESTINATION_LIMIT_MESSAGE}; got ${destinationCount}`
119
+ );
120
+ }
121
+ }
122
+ function assertCurrentUserPayoutFragmentLimit(fragmentCount) {
123
+ if (fragmentCount !== CURRENT_USER_PAYOUT_FRAGMENT_LIMIT) {
124
+ throw new PolicyValidationError(
125
+ "INVALID_PAYOUT_PLAN",
126
+ `${CURRENT_USER_PAYOUT_FRAGMENT_LIMIT_MESSAGE}; got ${fragmentCount}`
127
+ );
128
+ }
129
+ }
130
+
131
+ // src/user/recoveryCode.ts
132
+ var RECOVERY_CODE_BYTE_LENGTH = 32;
133
+ function assertRecoveryCode(code) {
134
+ if (code.byteLength !== RECOVERY_CODE_BYTE_LENGTH) {
135
+ throw new PolicyValidationError(
136
+ "INVALID_WITHDRAWAL_PLAN",
137
+ `recoveryCode must be ${RECOVERY_CODE_BYTE_LENGTH} bytes`
138
+ );
139
+ }
140
+ }
141
+ async function generateRecoveryCode() {
142
+ const code = crypto.getRandomValues(new Uint8Array(RECOVERY_CODE_BYTE_LENGTH));
143
+ const commitment = await commitmentFromCode(code);
144
+ return { code, commitment };
145
+ }
146
+ async function commitmentFromCode(code) {
147
+ const hashBuffer = await crypto.subtle.digest("SHA-256", new Uint8Array(code));
148
+ return new Uint8Array(hashBuffer);
149
+ }
150
+ function recoveryCodeToHex(code) {
151
+ return Array.from(code).map((b) => b.toString(16).padStart(2, "0")).join("");
152
+ }
153
+ function recoveryCodeFromHex(hex) {
154
+ const expectedHexLength = RECOVERY_CODE_BYTE_LENGTH * 2;
155
+ if (!new RegExp(`^[0-9a-fA-F]{${expectedHexLength}}$`).test(hex)) {
156
+ throw new Error(
157
+ `recovery code must be exactly ${expectedHexLength} hex characters (${RECOVERY_CODE_BYTE_LENGTH} bytes)`
158
+ );
159
+ }
160
+ const bytes = new Uint8Array(RECOVERY_CODE_BYTE_LENGTH);
161
+ for (let i = 0; i < RECOVERY_CODE_BYTE_LENGTH; i += 1) {
162
+ bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
163
+ }
164
+ return bytes;
165
+ }
166
+ function recoveryCodeFromUserInput(input) {
167
+ return recoveryCodeFromHex(input.replace(/\s+/g, ""));
168
+ }
169
+
170
+ // src/user/coordinatorSession.ts
171
+ var protocolPromise = null;
172
+ var frostRuntimePromise = null;
173
+ function loadProtocol() {
174
+ protocolPromise ??= import('./coordinator-7Y45MCCZ.js');
175
+ return protocolPromise;
176
+ }
177
+ function loadFrostRuntime() {
178
+ frostRuntimePromise ??= import('./frostRuntime-JESDHN6O.js');
179
+ return frostRuntimePromise;
180
+ }
181
+ function loadFrost() {
182
+ return loadFrostRuntime().then((runtime) => runtime.loadFrost());
183
+ }
184
+ var BENIGN_TRANSPORT_DROP = /* @__PURE__ */ Symbol("benignTransportDrop");
185
+ var ALICE_ID = 1;
186
+ var COORDINATOR_ID = 2;
187
+ var SYNC_SECRET_BYTE_LENGTH = 32;
188
+ var ACTIVE_CONTRACT_REQUEST_MESSAGE = "coordinator session already has an active contract";
189
+ var DEFAULT_SWAP_STATUS_RECONCILE_INTERVAL_MS = 5e3;
190
+ var MAX_SWAP_STATUS_RETRY_DELAY_MS = 3e4;
191
+ var SWAP_STATUS_RETRY_MULTIPLIER = 2;
192
+ var SWAP_STATUS_RETRY_JITTER_RATIO = 0.2;
193
+ var DEFAULT_REFUND_OUTCOME_POLL_BUDGET_MS = 5 * 60 * 1e3;
194
+ var REFUND_OUTCOME_POLL_INTERVAL_MS = 3e3;
195
+ var REFUND_OUTCOME_PER_POLL_TIMEOUT_MS = 5e3;
196
+ var REQUEST_TIMEOUT_MESSAGE = "coordinator request timed out";
197
+ var CURRENT_SCHEDULED_PAYOUT_WINDOW_SECONDS = new Set(scheduledPayoutWindowSeconds());
198
+ var CURRENT_SCHEDULED_PAYOUT_WINDOW_MESSAGE = `scheduled payout_spec.window_seconds must be one of [${scheduledPayoutWindowSeconds().join(", ")}]`;
199
+ var TERMINAL_SWAP_STATES = /* @__PURE__ */ new Set(["completed", "refunded", "failed"]);
200
+ var activeFunctionalContractRequests = /* @__PURE__ */ new WeakSet();
201
+ function createCoordinatorSession(options) {
202
+ return new UserCoordinatorSession(options);
203
+ }
204
+ function isBenignTransportDrop(err) {
205
+ return typeof err === "object" && err !== null && err[BENIGN_TRANSPORT_DROP] === true;
206
+ }
207
+ function readDkgComplete(protocol, envelope) {
208
+ const payload = protocol.asDkgCompletePayload(envelope);
209
+ return {
210
+ jointPublicKey: payload.joint_pubkey,
211
+ depositExpiresAtMs: payload.deposit_expires_at_ms,
212
+ coordinatorTimestampMs: envelope.timestamp_ms
213
+ };
214
+ }
215
+ function tagBenignTransportDrop(err) {
216
+ err[BENIGN_TRANSPORT_DROP] = true;
217
+ return err;
218
+ }
219
+ function isBenignTransportDropCandidate(err) {
220
+ return err instanceof TransportError && err.code === "CONNECTION_LOST";
221
+ }
222
+ var UserCoordinatorSession = class {
223
+ constructor(options, closeSessionAfterRefund = true) {
224
+ this.protocol = null;
225
+ this.session = null;
226
+ this.unsubscribeEnvelope = null;
227
+ this.unsubscribeTransportState = null;
228
+ this.swapState = "new";
229
+ this.sessionStatus = "idle";
230
+ this.swapId = null;
231
+ this.syncSecret = null;
232
+ this.attested = false;
233
+ this.pushListeners = /* @__PURE__ */ new Map();
234
+ this.stateListeners = /* @__PURE__ */ new Set();
235
+ this.logListeners = /* @__PURE__ */ new Set();
236
+ this.payoutExecutedListeners = /* @__PURE__ */ new Set();
237
+ this.refundExecutedListeners = /* @__PURE__ */ new Set();
238
+ this.normalUserActorSyncListeners = /* @__PURE__ */ new Set();
239
+ this.coordinatorErrorListeners = /* @__PURE__ */ new Set();
240
+ this.emittedPayoutKeys = /* @__PURE__ */ new Set();
241
+ this.emittedRefundKeys = /* @__PURE__ */ new Set();
242
+ this.stopStatusReconciliation = null;
243
+ this.bufferDepositConfirmedUntilDelegateAck = false;
244
+ this.bufferedDepositConfirmed = null;
245
+ this.coordinator = options.coordinator;
246
+ this.createSessionImpl = options.createSession ?? createSession;
247
+ this.closeSessionAfterRefund = closeSessionAfterRefund;
248
+ }
249
+ onStateChange(listener) {
250
+ this.stateListeners.add(listener);
251
+ return () => this.stateListeners.delete(listener);
252
+ }
253
+ onLog(listener) {
254
+ this.logListeners.add(listener);
255
+ return () => this.logListeners.delete(listener);
256
+ }
257
+ onPayoutExecuted(listener) {
258
+ this.payoutExecutedListeners.add(listener);
259
+ return () => this.payoutExecutedListeners.delete(listener);
260
+ }
261
+ onRefundExecuted(listener) {
262
+ this.refundExecutedListeners.add(listener);
263
+ return () => this.refundExecutedListeners.delete(listener);
264
+ }
265
+ onNormalUserActorSync(listener) {
266
+ this.normalUserActorSyncListeners.add(listener);
267
+ return () => this.normalUserActorSyncListeners.delete(listener);
268
+ }
269
+ onCoordinatorError(listener) {
270
+ this.coordinatorErrorListeners.add(listener);
271
+ return () => this.coordinatorErrorListeners.delete(listener);
272
+ }
273
+ get currentSwapState() {
274
+ return this.swapState;
275
+ }
276
+ get currentSessionStatus() {
277
+ return this.sessionStatus;
278
+ }
279
+ /** @internal The resolved contract id used as the V0 refund request id. */
280
+ get currentSwapId() {
281
+ return this.swapId;
282
+ }
283
+ async runSwap(params) {
284
+ assertCurrentUserPayoutSpecRuntimeLimits(params.payoutSpec);
285
+ this.assertNoActiveContract();
286
+ this.setSessionStatus("connecting");
287
+ let completed = false;
288
+ try {
289
+ await ensureFrostWasm();
290
+ await this.openSession();
291
+ this.setSessionStatus("running");
292
+ const result = await this.runProtocol(params);
293
+ completed = true;
294
+ this.setSessionStatus("done");
295
+ return result;
296
+ } catch (err) {
297
+ const error = this.normalizeSessionError(err);
298
+ if (!isBenignTransportDrop(error)) {
299
+ this.setSessionStatus("failed");
300
+ }
301
+ throw error;
302
+ } finally {
303
+ if (!completed) {
304
+ this.close();
305
+ }
306
+ }
307
+ }
308
+ async contractRequest(params) {
309
+ assertCurrentUserPayoutSpecRuntimeLimits(params.payoutSpec);
310
+ this.assertNoActiveContract();
311
+ this.setSessionStatus("connecting");
312
+ try {
313
+ await ensureFrostWasm();
314
+ await this.openSession();
315
+ this.setSessionStatus("running");
316
+ const prepared = await this.prepareContract(params);
317
+ return toContractRequestResult(prepared);
318
+ } catch (err) {
319
+ const error = this.normalizeSessionError(err);
320
+ this.setSessionStatus("failed");
321
+ throw error;
322
+ }
323
+ }
324
+ /** @internal Used by the functional Session API without closing its caller-owned session. */
325
+ releaseWithoutClosing() {
326
+ this.stopStatusReconciliation?.();
327
+ this.stopStatusReconciliation = null;
328
+ this.detachSession(
329
+ new TransportError("CONNECTION_LOST", "coordinator session operation released"),
330
+ false,
331
+ false
332
+ );
333
+ }
334
+ close() {
335
+ this.stopStatusReconciliation?.();
336
+ this.stopStatusReconciliation = null;
337
+ this.detachSession(new TransportError("CONNECTION_LOST", "coordinator session closed"), true);
338
+ }
339
+ restoreSwapContext(params) {
340
+ this.swapId = params.swapId;
341
+ if (params.syncSecret !== void 0) {
342
+ if (params.syncSecret.length !== SYNC_SECRET_BYTE_LENGTH) {
343
+ throw new CommandError(
344
+ "REJECTED_GUARD",
345
+ `syncSecret must be ${SYNC_SECRET_BYTE_LENGTH} bytes`
346
+ );
347
+ }
348
+ this.syncSecret = new Uint8Array(params.syncSecret);
349
+ }
350
+ }
351
+ async requestSwapStatus(timeoutMs) {
352
+ if (!this.swapId) {
353
+ throw new CommandError("REJECTED_STATE", "Sync requires swapId");
354
+ }
355
+ await this.openSession();
356
+ const env = await this.request(
357
+ CoordinatorMessageType.Sync,
358
+ {
359
+ actor: "user",
360
+ ...this.syncSecret ? { sync_secret: this.syncSecret } : {}
361
+ },
362
+ timeoutMs
363
+ );
364
+ const status = this.requireProtocol().asSyncUserResponsePayload(env);
365
+ this.applySwapStatus(status, env.timestamp_ms);
366
+ return status;
367
+ }
368
+ startSwapStatusReconciliation(intervalMs = DEFAULT_SWAP_STATUS_RECONCILE_INTERVAL_MS) {
369
+ this.stopStatusReconciliation?.();
370
+ let stopped = false;
371
+ let reconciling = false;
372
+ let timer = null;
373
+ const browserWindow = typeof window === "undefined" ? null : window;
374
+ const browserDocument = typeof document === "undefined" ? null : document;
375
+ const retryBackoff = createExponentialBackoff({
376
+ initialDelayMs: intervalMs,
377
+ maxDelayMs: Math.max(MAX_SWAP_STATUS_RETRY_DELAY_MS, intervalMs),
378
+ multiplier: SWAP_STATUS_RETRY_MULTIPLIER,
379
+ jitterRatio: SWAP_STATUS_RETRY_JITTER_RATIO
380
+ });
381
+ const isVisible = () => browserDocument === null || browserDocument.visibilityState === "visible";
382
+ const schedule = (delayMs) => {
383
+ if (stopped || !isVisible()) return;
384
+ if (timer) clearTimeout(timer);
385
+ timer = setTimeout(() => {
386
+ timer = null;
387
+ void reconcile();
388
+ }, delayMs);
389
+ };
390
+ const reconcile = async () => {
391
+ if (stopped || reconciling || !isVisible()) return;
392
+ reconciling = true;
393
+ try {
394
+ const status = await this.requestSwapStatus();
395
+ retryBackoff.reset();
396
+ if (status.state === "completed" || status.state === "refunded" || status.state === "failed") {
397
+ stopped = true;
398
+ if (timer) clearTimeout(timer);
399
+ } else {
400
+ schedule(intervalMs);
401
+ }
402
+ } catch (err) {
403
+ this.log(`SwapStatus reconcile failed: ${normalizeError(err)}`, "info");
404
+ if (!isRetryableSwapStatusReconciliationError(err)) {
405
+ stopped = true;
406
+ if (timer) clearTimeout(timer);
407
+ this.close();
408
+ return;
409
+ }
410
+ schedule(retryBackoff.nextDelayMs());
411
+ } finally {
412
+ reconciling = false;
413
+ }
414
+ };
415
+ const reconcileWhenVisible = () => {
416
+ if (!isVisible()) return;
417
+ if (timer) clearTimeout(timer);
418
+ timer = null;
419
+ void reconcile();
420
+ };
421
+ const pauseWhileInactive = () => {
422
+ if (timer) clearTimeout(timer);
423
+ timer = null;
424
+ };
425
+ const handleVisibilityChange = () => {
426
+ if (!isVisible()) {
427
+ pauseWhileInactive();
428
+ return;
429
+ }
430
+ reconcileWhenVisible();
431
+ };
432
+ const handleFreeze = () => {
433
+ pauseWhileInactive();
434
+ this.detachSession(new TransportError("CONNECTION_LOST", "coordinator page frozen"), true);
435
+ };
436
+ browserWindow?.addEventListener("focus", reconcileWhenVisible);
437
+ browserWindow?.addEventListener("online", reconcileWhenVisible);
438
+ browserWindow?.addEventListener("pageshow", reconcileWhenVisible);
439
+ browserDocument?.addEventListener("visibilitychange", handleVisibilityChange);
440
+ browserDocument?.addEventListener("freeze", handleFreeze);
441
+ browserDocument?.addEventListener("resume", reconcileWhenVisible);
442
+ reconcileWhenVisible();
443
+ this.stopStatusReconciliation = () => {
444
+ stopped = true;
445
+ if (timer) clearTimeout(timer);
446
+ browserWindow?.removeEventListener("focus", reconcileWhenVisible);
447
+ browserWindow?.removeEventListener("online", reconcileWhenVisible);
448
+ browserWindow?.removeEventListener("pageshow", reconcileWhenVisible);
449
+ browserDocument?.removeEventListener("visibilitychange", handleVisibilityChange);
450
+ browserDocument?.removeEventListener("freeze", handleFreeze);
451
+ browserDocument?.removeEventListener("resume", reconcileWhenVisible);
452
+ };
453
+ return this.stopStatusReconciliation;
454
+ }
455
+ async requestRefund(params) {
456
+ const result = await this.requestRefundIntentInternal(params, { waitForFinality: true });
457
+ if (result.kind === "executed") return result.refund;
458
+ throw new CommandError("REJECTED_STALE_JOB", "refund accepted without final execution");
459
+ }
460
+ requestRefundIntent(params) {
461
+ return this.requestRefundIntentInternal(params, { waitForFinality: false });
462
+ }
463
+ async requestRefundIntentInternal(params, options) {
464
+ assertRecoveryCode(params.recoveryCode);
465
+ const recoveryCode = new Uint8Array(params.recoveryCode);
466
+ const ackTimeout = params.timeoutMs ?? 3e4;
467
+ let ackTimer;
468
+ let openedForRefund = false;
469
+ try {
470
+ if (params.syncSecret !== void 0) {
471
+ const swapId = params.swapId ?? this.swapId;
472
+ if (!swapId) {
473
+ throw new CommandError("REJECTED_STATE", "syncSecret requires swapId");
474
+ }
475
+ this.restoreSwapContext({ swapId, syncSecret: params.syncSecret });
476
+ } else if (params.swapId) {
477
+ const swapId = params.swapId;
478
+ this.restoreSwapContext({ swapId });
479
+ }
480
+ openedForRefund = !this.hasUsableSession();
481
+ await this.openSession();
482
+ const env = await Promise.race([
483
+ this.request(CoordinatorMessageType.WithdrawRequest, {
484
+ recovery_code: recoveryCode
485
+ }),
486
+ new Promise((_, reject) => {
487
+ ackTimer = setTimeout(
488
+ () => reject(
489
+ new CommandError(
490
+ "REJECTED_STALE_JOB",
491
+ `WithdrawRequest acknowledgement timed out after ${ackTimeout}ms`
492
+ )
493
+ ),
494
+ ackTimeout
495
+ );
496
+ })
497
+ ]);
498
+ if (ackTimer) clearTimeout(ackTimer);
499
+ if (env.type === CoordinatorMessageType.WithdrawExecuted) {
500
+ if (env.swap_id && !this.swapId) {
501
+ this.swapId = env.swap_id;
502
+ }
503
+ return { kind: "executed", refund: this.requireProtocol().asWithdrawExecutedPayload(env) };
504
+ }
505
+ if (env.type !== CoordinatorMessageType.WithdrawAccepted) {
506
+ throw new CommandError(
507
+ "REJECTED_GUARD",
508
+ `Expected WithdrawAccepted or WithdrawExecuted response, got ${env.type}`
509
+ );
510
+ }
511
+ if (env.swap_id && !this.swapId) {
512
+ this.swapId = env.swap_id;
513
+ }
514
+ const accepted = this.requireProtocol().asWithdrawAcceptedPayload(env);
515
+ if (!options.waitForFinality) {
516
+ return { kind: "accepted", accepted };
517
+ }
518
+ if (!this.swapId) {
519
+ throw new CommandError(
520
+ "REJECTED_STALE_JOB",
521
+ "refund accepted and pending finalization; check status again shortly"
522
+ );
523
+ }
524
+ return { kind: "executed", refund: await this.awaitRefundOutcomeFromSwapStatus() };
525
+ } finally {
526
+ if (ackTimer) clearTimeout(ackTimer);
527
+ recoveryCode.fill(0);
528
+ if (openedForRefund && this.closeSessionAfterRefund) {
529
+ this.close();
530
+ }
531
+ }
532
+ }
533
+ async awaitRefundOutcomeFromSwapStatus() {
534
+ const deadline = Date.now() + DEFAULT_REFUND_OUTCOME_POLL_BUDGET_MS;
535
+ for (; ; ) {
536
+ const perPollTimeout = Math.min(
537
+ REFUND_OUTCOME_PER_POLL_TIMEOUT_MS,
538
+ Math.max(0, deadline - Date.now())
539
+ );
540
+ try {
541
+ const status = await this.requestSwapStatus(perPollTimeout);
542
+ if (status.withdraw_execution) {
543
+ return status.withdraw_execution;
544
+ }
545
+ } catch (err) {
546
+ if (!isRequestTimeoutError(err)) {
547
+ throw err;
548
+ }
549
+ }
550
+ if (Date.now() >= deadline) {
551
+ throw new CommandError(
552
+ "REJECTED_STALE_JOB",
553
+ "refund accepted and pending finalization; check status again shortly"
554
+ );
555
+ }
556
+ await delay(REFUND_OUTCOME_POLL_INTERVAL_MS);
557
+ }
558
+ }
559
+ async runProtocol(params) {
560
+ const protocol = this.requireProtocol();
561
+ const prepared = await this.prepareContract(params);
562
+ const depositEnv = await prepared.depositConfirmedPromise;
563
+ const depositPayload = protocol.asDepositConfirmedPayload(depositEnv);
564
+ if (depositPayload.amount_lamports !== params.amountLamports) {
565
+ throw new InvalidDepositAmountError(
566
+ BigInt(depositPayload.amount_lamports),
567
+ BigInt(params.amountLamports)
568
+ );
569
+ }
570
+ this.log(
571
+ `deposit confirmed tx=${depositPayload.tx_signature} amount=${depositPayload.amount_lamports}`,
572
+ "in"
573
+ );
574
+ await params.onDepositConfirmed?.({ originAddress: depositPayload.origin_address });
575
+ await this.requestSwapStatus();
576
+ return {
577
+ swapId: prepared.swapId,
578
+ jointPublicKey: prepared.jointPublicKey,
579
+ depositTxSignature: depositPayload.tx_signature,
580
+ depositObservedAtMs: depositPayload.observed_at_ms,
581
+ delegatedAtMs: prepared.delegatedAtMs,
582
+ recoveryCodeHex: prepared.recoveryCodeHex,
583
+ policySnapshot: prepared.policySnapshot,
584
+ delegatePolicySnapshot: prepared.delegatePolicySnapshot
585
+ };
586
+ }
587
+ async prepareContract(params) {
588
+ const protocol = this.requireProtocol();
589
+ const requestedPayoutWindowMs = params.payoutWindowMs ?? payoutWindowMsFromSpec(params.payoutSpec);
590
+ const payoutDeadlineMs = requestedPayoutWindowMs !== void 0 ? Date.now() + requestedPayoutWindowMs : void 0;
591
+ let generatedSyncSecret = null;
592
+ const syncSecret = params.syncSecret ? new Uint8Array(params.syncSecret) : (() => {
593
+ generatedSyncSecret = randomSecret32();
594
+ return generatedSyncSecret;
595
+ })();
596
+ if (syncSecret.length !== SYNC_SECRET_BYTE_LENGTH) {
597
+ throw new CommandError(
598
+ "REJECTED_GUARD",
599
+ `syncSecret must be ${SYNC_SECRET_BYTE_LENGTH} bytes`
600
+ );
601
+ }
602
+ this.syncSecret = new Uint8Array(syncSecret);
603
+ const syncSecretHex = bytesToHex(syncSecret);
604
+ const syncCommitment = await sha256Commitment(syncSecret);
605
+ if (generatedSyncSecret) {
606
+ await params.onSyncSecretReady?.({ secretHex: syncSecretHex });
607
+ }
608
+ const swapResp = await this.request(CoordinatorMessageType.ContractRequest, {
609
+ actor: "user",
610
+ amount_lamports: params.amountLamports,
611
+ payout_spec: params.payoutSpec,
612
+ sync_commitment: syncCommitment,
613
+ ...(params.payoutDeadlineMs !== void 0 || payoutDeadlineMs !== void 0) && {
614
+ payout_deadline_ms: params.payoutDeadlineMs ?? payoutDeadlineMs
615
+ }
616
+ });
617
+ const swapPayload = protocol.asSwapRequestResponsePayload(swapResp);
618
+ const swapId = swapPayload.swap_id;
619
+ this.swapId = swapId;
620
+ params.onPolicySnapshot?.(swapPayload.policy_snapshot);
621
+ this.log(`swap accepted - id=${swapId}`, "in");
622
+ this.bufferDepositConfirmedUntilDelegateAck = true;
623
+ this.bufferedDepositConfirmed = null;
624
+ const depositConfirmedPromise = this.awaitPush(CoordinatorMessageType.DepositConfirmed, null);
625
+ void depositConfirmedPromise.catch(() => void 0);
626
+ const { jointPublicKey, keyHandle, depositExpiresAtMs, coordinatorTimestampMs } = await this.runDkg();
627
+ let recoveryCommitment = params.recoveryCommitment;
628
+ let recoveryCodeHex;
629
+ if (!recoveryCommitment) {
630
+ const generated = await generateRecoveryCode();
631
+ recoveryCommitment = generated.commitment;
632
+ try {
633
+ recoveryCodeHex = recoveryCodeToHex(generated.code);
634
+ await params.onRecoveryCodeReady?.({ codeHex: recoveryCodeHex });
635
+ } finally {
636
+ generated.code.fill(0);
637
+ }
638
+ }
639
+ const delegateAckPromise = this.awaitPush(CoordinatorMessageType.DelegateAck);
640
+ const share = params.delegateShare ?? keyHandle.secretShareBytes();
641
+ let delegateResp;
642
+ try {
643
+ delegateResp = await this.request(CoordinatorMessageType.DelegateShare, {
644
+ share,
645
+ recovery_commitment: recoveryCommitment
646
+ });
647
+ } catch (err) {
648
+ void delegateAckPromise.catch(() => void 0);
649
+ throw err;
650
+ }
651
+ const delegatePayload = protocol.asDelegateShareResponsePayload(delegateResp);
652
+ const delegateAck = protocol.asDelegateAckPayload(await delegateAckPromise);
653
+ if (delegateAck.delegated_at_ms !== delegatePayload.delegated_at_ms) {
654
+ throw new CommandError(
655
+ "REJECTED_GUARD",
656
+ "DelegateAck delegated_at_ms does not match DelegateShare response"
657
+ );
658
+ }
659
+ if (!spendingPoliciesEqual(delegateAck.spending_policy, swapPayload.policy_snapshot)) {
660
+ throw new CommandError(
661
+ "REJECTED_GUARD",
662
+ "DelegateAck spending_policy does not match ContractRequest policy_snapshot"
663
+ );
664
+ }
665
+ this.log(`delegated at ${delegateAck.delegated_at_ms}`, "in");
666
+ this.releaseDepositConfirmedGate();
667
+ await params.onDepositAddressReady?.({
668
+ swapId,
669
+ jointPublicKey,
670
+ amountLamports: params.amountLamports,
671
+ depositExpiresAtMs,
672
+ coordinatorTimestampMs
673
+ });
674
+ return {
675
+ requestId: swapId,
676
+ swapId,
677
+ amountLamports: params.amountLamports,
678
+ jointPublicKey,
679
+ depositExpiresAtMs,
680
+ coordinatorTimestampMs,
681
+ delegatedAtMs: delegateAck.delegated_at_ms,
682
+ syncSecret,
683
+ syncSecretHex,
684
+ recoveryCodeHex,
685
+ policySnapshot: swapPayload.policy_snapshot,
686
+ delegatePolicySnapshot: delegateAck.spending_policy,
687
+ depositConfirmedPromise
688
+ };
689
+ }
690
+ async runDkg() {
691
+ await ensureFrostWasm();
692
+ const protocol = this.requireProtocol();
693
+ const { dkgRound1 } = await loadFrost();
694
+ await this.request(CoordinatorMessageType.DkgInit, {});
695
+ this.log("DKG initialised", "info");
696
+ const aliceRound1State = dkgRound1(ALICE_ID);
697
+ const aliceRound1Bytes = aliceRound1State.round1PackageBytes();
698
+ const round1Resp = await this.request(CoordinatorMessageType.DkgRound1, {
699
+ package: aliceRound1Bytes
700
+ });
701
+ const round1Payload = protocol.asDkgRound1ResponsePayload(round1Resp);
702
+ this.log(`DKG round1 complete - coord pkg ${round1Payload.package.byteLength}B`, "in");
703
+ const aliceRound2State = aliceRound1State.intoRound2(COORDINATOR_ID, round1Payload.package);
704
+ const aliceRound2Bytes = aliceRound2State.round2PackageBytesFor(COORDINATOR_ID);
705
+ let aliceKeyHandle;
706
+ try {
707
+ aliceKeyHandle = aliceRound2State.intoRound3(
708
+ COORDINATOR_ID,
709
+ round1Payload.package,
710
+ round1Payload.round2_for_alice
711
+ );
712
+ } catch (err) {
713
+ throw new CommandError("REJECTED_GUARD", `Alice DKG finalize failed: ${normalizeError(err)}`);
714
+ }
715
+ const dkgCompletePromise = this.awaitPush(CoordinatorMessageType.DkgComplete);
716
+ await this.request(CoordinatorMessageType.DkgRound2, {
717
+ package: aliceRound2Bytes
718
+ });
719
+ const completeEnvelope = await dkgCompletePromise;
720
+ const complete = readDkgComplete(protocol, completeEnvelope);
721
+ const alicePubkey = aliceKeyHandle.groupPublicKeyBytes();
722
+ if (!bytesEqual(alicePubkey, complete.jointPublicKey)) {
723
+ throw new CommandError(
724
+ "REJECTED_GUARD",
725
+ "DKG joint public key mismatch between Alice and coordinator"
726
+ );
727
+ }
728
+ const pubkeyHexPrefix = Array.from(alicePubkey.slice(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join("");
729
+ this.log(`DKG complete - joint pubkey ${pubkeyHexPrefix}`, "info");
730
+ return {
731
+ jointPublicKey: alicePubkey,
732
+ keyHandle: aliceKeyHandle,
733
+ depositExpiresAtMs: complete.depositExpiresAtMs,
734
+ coordinatorTimestampMs: complete.coordinatorTimestampMs
735
+ };
736
+ }
737
+ async openSession() {
738
+ if (this.hasUsableSession()) return;
739
+ if (this.session !== null) {
740
+ this.detachSession(
741
+ new TransportError("CONNECTION_LOST", "cached coordinator session is not usable"),
742
+ true
743
+ );
744
+ }
745
+ this.protocol = await loadProtocol();
746
+ const session = await this.createSessionImpl({ coordinator: this.coordinator });
747
+ this.session = session;
748
+ this.attested = true;
749
+ this.unsubscribeEnvelope = onEnvelope(session, (envelope) => this.handleEnvelope(envelope));
750
+ this.unsubscribeTransportState = getSessionState(session).transport.onStateChange((state) => {
751
+ if (this.session !== session) return;
752
+ if (state === "closing" || state === "closed") {
753
+ const fatalError = getSessionState(session).lastFatalError;
754
+ this.detachSession(
755
+ fatalError ?? new TransportError("CONNECTION_LOST", "coordinator transport closed"),
756
+ false
757
+ );
758
+ }
759
+ });
760
+ this.log(`connected to ${this.coordinator.endpoints[0]?.wsUrl ?? "coordinator"}`, "info");
761
+ this.log("attestation verified", "info");
762
+ }
763
+ assertNoActiveContract() {
764
+ if (this.swapId !== null || this.sessionStatus === "connecting" || this.sessionStatus === "running") {
765
+ throw new CommandError("REJECTED_STATE", ACTIVE_CONTRACT_REQUEST_MESSAGE);
766
+ }
767
+ }
768
+ hasUsableSession() {
769
+ if (this.session === null) return false;
770
+ const state = getSessionState(this.session);
771
+ return state.attested && state.channel !== null && state.transport.state() === "open";
772
+ }
773
+ detachSession(error, closeTransport, closeProtocolRuntime2 = true) {
774
+ const rejectionError = this.attested && !closeTransport && isBenignTransportDropCandidate(error) ? tagBenignTransportDrop(error) : error;
775
+ this.unsubscribeEnvelope?.();
776
+ this.unsubscribeEnvelope = null;
777
+ this.unsubscribeTransportState?.();
778
+ this.unsubscribeTransportState = null;
779
+ const session = this.session;
780
+ this.session = null;
781
+ this.attested = false;
782
+ if (session !== null) {
783
+ if (closeProtocolRuntime2) closeProtocolRuntime(session);
784
+ if (closeTransport) {
785
+ getSessionState(session).close();
786
+ }
787
+ }
788
+ this.rejectAllPushListeners(rejectionError);
789
+ }
790
+ handleEnvelope(env) {
791
+ const protocol = this.requireProtocol();
792
+ this.log(`<- ${env.type} kind=${env.kind}`, "in");
793
+ const payoutExecuted = env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.PayoutExecuted ? protocol.asPayoutExecutedPayload(env) : null;
794
+ const payoutMatchesCurrentSwap = payoutExecuted !== null && (env.swap_id === null || env.swap_id === this.swapId);
795
+ const refundExecuted = env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.WithdrawExecuted ? protocol.asWithdrawExecutedPayload(env) : null;
796
+ if (this.shouldBufferDepositConfirmed(env)) {
797
+ this.bufferDepositConfirmed(env);
798
+ return;
799
+ }
800
+ const foreignPayout = payoutExecuted !== null && !payoutMatchesCurrentSwap;
801
+ const next = foreignPayout ? null : protocol.nextState(
802
+ this.swapState,
803
+ env.kind,
804
+ env.type,
805
+ payoutExecuted ? { payoutFinal: payoutExecuted.final } : void 0
806
+ );
807
+ if (env.kind === EnvelopeKind.PUSH && next === null) {
808
+ this.log(
809
+ foreignPayout ? `ignored ${env.type} for swap ${env.swap_id ?? "null"} while current swap is ${this.swapId ?? "none"}` : `ignored invalid push ${env.type} in state ${this.swapState}`,
810
+ "info"
811
+ );
812
+ return;
813
+ }
814
+ if (next !== null) {
815
+ this.swapState = next;
816
+ this.emitStateChange();
817
+ }
818
+ if (env.kind === EnvelopeKind.ERROR) {
819
+ const coordinatorError = coordinatorErrorFromEnvelope(protocol, env);
820
+ if (env.req_id === 0) {
821
+ for (const listener of this.coordinatorErrorListeners) {
822
+ listener(coordinatorError);
823
+ }
824
+ }
825
+ return;
826
+ }
827
+ if (env.kind === EnvelopeKind.PUSH) {
828
+ if (payoutExecuted && payoutMatchesCurrentSwap) {
829
+ this.emitPayoutExecuted(payoutExecuted, env.swap_id ?? this.swapId);
830
+ }
831
+ if (refundExecuted) {
832
+ this.emitRefundExecuted(refundExecuted, env.swap_id ?? this.swapId);
833
+ }
834
+ this.resolvePushListener(env);
835
+ }
836
+ }
837
+ applySwapStatus(status, coordinatorTimestampMs) {
838
+ const keepTerminalState = TERMINAL_SWAP_STATES.has(this.swapState) && status.state !== this.swapState;
839
+ if (!keepTerminalState && this.swapState !== status.state) {
840
+ this.swapState = status.state;
841
+ this.emitStateChange();
842
+ }
843
+ for (const fragment of status.fragments_executed) {
844
+ this.emitPayoutExecuted(fragment, this.swapId);
845
+ }
846
+ if (status.withdraw_execution) {
847
+ this.emitRefundExecuted(status.withdraw_execution, this.swapId);
848
+ }
849
+ if (status.actor_sync !== void 0) {
850
+ for (const listener of this.normalUserActorSyncListeners) {
851
+ listener(status.actor_sync, coordinatorTimestampMs);
852
+ }
853
+ }
854
+ }
855
+ emitPayoutExecuted(payload, swapId) {
856
+ if (!swapId) return;
857
+ const key = `${swapId}:${payload.index}:${payload.tx_signature}`;
858
+ if (this.emittedPayoutKeys.has(key)) return;
859
+ this.emittedPayoutKeys.add(key);
860
+ this.log(
861
+ `payout executed tx=${payload.tx_signature} amount=${payload.amount_lamports} final=${payload.final}`,
862
+ "in"
863
+ );
864
+ for (const listener of this.payoutExecutedListeners) {
865
+ listener(payload, swapId);
866
+ }
867
+ }
868
+ emitRefundExecuted(payload, swapId) {
869
+ if (!swapId) return;
870
+ const key = `${swapId}:${payload.tx_signature}`;
871
+ if (this.emittedRefundKeys.has(key)) return;
872
+ this.emittedRefundKeys.add(key);
873
+ this.log(`refund executed tx=${payload.tx_signature} amount=${payload.amount_lamports}`, "in");
874
+ for (const listener of this.refundExecutedListeners) {
875
+ listener(payload, swapId);
876
+ }
877
+ }
878
+ request(type, payload, timeoutMs) {
879
+ if (this.session === null) {
880
+ throw new TransportError("CONNECTION_LOST", "coordinator session is not connected");
881
+ }
882
+ return requestEnvelope(this.session, type, payload, {
883
+ swapId: this.swapId,
884
+ timeoutMs,
885
+ onLog: (message, direction) => this.log(message, direction)
886
+ });
887
+ }
888
+ awaitPush(type, timeoutMs = 3e4) {
889
+ return new Promise((resolve, reject) => {
890
+ const timer = timeoutMs === null ? null : setTimeout(() => {
891
+ this.pushListeners.delete(type);
892
+ reject(new CommandError("REJECTED_STALE_JOB", `timed out waiting for ${type} push`));
893
+ }, timeoutMs);
894
+ this.pushListeners.set(type, { resolve, reject, timer });
895
+ });
896
+ }
897
+ rejectAllPushListeners(err) {
898
+ for (const listener of this.pushListeners.values()) {
899
+ if (listener.timer) clearTimeout(listener.timer);
900
+ listener.reject(err);
901
+ }
902
+ this.pushListeners.clear();
903
+ this.bufferDepositConfirmedUntilDelegateAck = false;
904
+ this.bufferedDepositConfirmed = null;
905
+ }
906
+ shouldBufferDepositConfirmed(env) {
907
+ return env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.DepositConfirmed && this.bufferDepositConfirmedUntilDelegateAck;
908
+ }
909
+ bufferDepositConfirmed(env) {
910
+ if (!this.bufferedDepositConfirmed) {
911
+ this.bufferedDepositConfirmed = env;
912
+ this.clearPushListenerTimer(CoordinatorMessageType.DepositConfirmed);
913
+ this.log("buffered DepositConfirmed until DelegateAck is verified", "info");
914
+ return;
915
+ }
916
+ this.log("ignored duplicate DepositConfirmed while waiting for DelegateAck", "info");
917
+ }
918
+ releaseDepositConfirmedGate() {
919
+ const protocol = this.requireProtocol();
920
+ this.bufferDepositConfirmedUntilDelegateAck = false;
921
+ const buffered = this.bufferedDepositConfirmed;
922
+ if (!buffered) return;
923
+ this.bufferedDepositConfirmed = null;
924
+ const next = protocol.nextState(this.swapState, buffered.kind, buffered.type);
925
+ if (next === null) {
926
+ this.log(`ignored buffered DepositConfirmed in state ${this.swapState}`, "info");
927
+ return;
928
+ }
929
+ this.swapState = next;
930
+ this.emitStateChange();
931
+ this.resolvePushListener(buffered);
932
+ }
933
+ resolvePushListener(env) {
934
+ const listener = this.pushListeners.get(env.type);
935
+ if (!listener) return;
936
+ this.pushListeners.delete(env.type);
937
+ if (listener.timer) clearTimeout(listener.timer);
938
+ listener.resolve(env);
939
+ }
940
+ clearPushListenerTimer(type) {
941
+ const listener = this.pushListeners.get(type);
942
+ if (!listener?.timer) return;
943
+ clearTimeout(listener.timer);
944
+ listener.timer = null;
945
+ }
946
+ setSessionStatus(status) {
947
+ this.sessionStatus = status;
948
+ this.emitStateChange();
949
+ }
950
+ emitStateChange() {
951
+ for (const listener of this.stateListeners) {
952
+ listener(this.swapState, this.sessionStatus);
953
+ }
954
+ }
955
+ log(msg, direction) {
956
+ for (const listener of this.logListeners) {
957
+ listener(msg, direction);
958
+ }
959
+ }
960
+ normalizeSessionError(err) {
961
+ if (err instanceof Error) {
962
+ if (this.attested && isBenignTransportDropCandidate(err)) return tagBenignTransportDrop(err);
963
+ return err;
964
+ }
965
+ return new Error(String(err));
966
+ }
967
+ requireProtocol() {
968
+ if (this.protocol === null) {
969
+ throw new CommandError("REJECTED_STATE", "coordinator protocol is not loaded");
970
+ }
971
+ return this.protocol;
972
+ }
973
+ };
974
+ function toContractRequestResult(prepared) {
975
+ return {
976
+ accepted: true,
977
+ requestId: prepared.requestId,
978
+ state: "awaiting_deposit",
979
+ swapId: prepared.swapId,
980
+ depositAddress: solanaAddressFromPublicKeyBytes(prepared.jointPublicKey),
981
+ jointPublicKey: new Uint8Array(prepared.jointPublicKey),
982
+ amountLamports: prepared.amountLamports,
983
+ depositExpiresAtMs: prepared.depositExpiresAtMs,
984
+ coordinatorTimestampMs: prepared.coordinatorTimestampMs,
985
+ delegatedAtMs: prepared.delegatedAtMs,
986
+ syncSecret: new Uint8Array(prepared.syncSecret),
987
+ syncSecretHex: prepared.syncSecretHex,
988
+ recoveryCodeHex: prepared.recoveryCodeHex,
989
+ policySnapshot: prepared.policySnapshot,
990
+ delegatePolicySnapshot: prepared.delegatePolicySnapshot
991
+ };
992
+ }
993
+ async function contractRequestOnSession(session, params) {
994
+ if (activeFunctionalContractRequests.has(session)) {
995
+ throw new CommandError("REJECTED_STATE", ACTIVE_CONTRACT_REQUEST_MESSAGE);
996
+ }
997
+ activeFunctionalContractRequests.add(session);
998
+ let client = null;
999
+ try {
1000
+ client = new UserCoordinatorSession({
1001
+ coordinator: getSessionState(session).pool,
1002
+ createSession: async () => session
1003
+ });
1004
+ return await client.contractRequest(params);
1005
+ } finally {
1006
+ activeFunctionalContractRequests.delete(session);
1007
+ client?.releaseWithoutClosing();
1008
+ }
1009
+ }
1010
+ async function requestRefundOnSession(session, params) {
1011
+ const client = new UserCoordinatorSession(
1012
+ {
1013
+ coordinator: getSessionState(session).pool,
1014
+ createSession: async () => session
1015
+ },
1016
+ false
1017
+ );
1018
+ try {
1019
+ await client.requestRefundIntent(params);
1020
+ const requestId = client.currentSwapId;
1021
+ if (!requestId) {
1022
+ throw new CommandError(
1023
+ "REJECTED_STALE_JOB",
1024
+ "refund response did not include the resolved contract id"
1025
+ );
1026
+ }
1027
+ return { accepted: true, requestId };
1028
+ } finally {
1029
+ client.releaseWithoutClosing();
1030
+ }
1031
+ }
1032
+ async function requestUserSync(session, swapId, syncSecret, timeoutMs) {
1033
+ if (swapId.trim().length === 0) {
1034
+ throw new CommandError("REJECTED_STATE", "sync requires a non-empty swapId");
1035
+ }
1036
+ if (syncSecret.length !== SYNC_SECRET_BYTE_LENGTH) {
1037
+ throw new CommandError("REJECTED_GUARD", `syncSecret must be ${SYNC_SECRET_BYTE_LENGTH} bytes`);
1038
+ }
1039
+ const env = await requestEnvelope(
1040
+ session,
1041
+ CoordinatorMessageType.Sync,
1042
+ {
1043
+ actor: "user",
1044
+ sync_secret: new Uint8Array(syncSecret)
1045
+ },
1046
+ { swapId, timeoutMs }
1047
+ );
1048
+ return (await loadProtocol()).asSyncUserResponsePayload(env);
1049
+ }
1050
+ async function ensureFrostWasm() {
1051
+ await (await loadFrostRuntime()).ensureFrostWasm();
1052
+ }
1053
+ function randomSecret32() {
1054
+ return crypto.getRandomValues(new Uint8Array(SYNC_SECRET_BYTE_LENGTH));
1055
+ }
1056
+ async function sha256Commitment(secret) {
1057
+ const hashBuffer = await crypto.subtle.digest("SHA-256", new Uint8Array(secret));
1058
+ return new Uint8Array(hashBuffer);
1059
+ }
1060
+ function bytesToHex(bytes) {
1061
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1062
+ }
1063
+ function delay(ms) {
1064
+ return new Promise((resolve) => setTimeout(resolve, ms));
1065
+ }
1066
+ function payoutWindowMsFromSpec(payoutSpec) {
1067
+ if (payoutSpec.mode === "instant") return void 0;
1068
+ return payoutSpec.window_seconds * 1e3;
1069
+ }
1070
+ function assertCurrentUserPayoutSpecRuntimeLimits(payoutSpec) {
1071
+ if (payoutSpec.mode === "instant") {
1072
+ if (typeof payoutSpec.destination_address !== "string") {
1073
+ throw new PolicyValidationError(
1074
+ "INVALID_PAYOUT_PLAN",
1075
+ "instant payout_spec.destination_address must be a string"
1076
+ );
1077
+ }
1078
+ publicKey(payoutSpec.destination_address);
1079
+ return;
1080
+ }
1081
+ if (!Array.isArray(payoutSpec.destination_addresses)) {
1082
+ throw new PolicyValidationError(
1083
+ "INVALID_PAYOUT_PLAN",
1084
+ "scheduled payout_spec.destination_addresses must be an array"
1085
+ );
1086
+ }
1087
+ assertCurrentUserPayoutDestinationLimit(payoutSpec.destination_addresses.length);
1088
+ for (const address of payoutSpec.destination_addresses) {
1089
+ publicKey(address);
1090
+ }
1091
+ assertCurrentUserPayoutFragmentLimit(payoutSpec.num_fragments);
1092
+ if (!CURRENT_SCHEDULED_PAYOUT_WINDOW_SECONDS.has(payoutSpec.window_seconds)) {
1093
+ throw new PolicyValidationError(
1094
+ "INVALID_PAYOUT_PLAN",
1095
+ `${CURRENT_SCHEDULED_PAYOUT_WINDOW_MESSAGE}; got ${payoutSpec.window_seconds}`
1096
+ );
1097
+ }
1098
+ }
1099
+ function bytesEqual(a, b) {
1100
+ if (a.byteLength !== b.byteLength) return false;
1101
+ let diff = 0;
1102
+ for (let index = 0; index < a.byteLength; index += 1) {
1103
+ diff |= a[index] ^ b[index];
1104
+ }
1105
+ return diff === 0;
1106
+ }
1107
+ function spendingPoliciesEqual(a, b) {
1108
+ return a.asset === b.asset && a.entry_amount_lamports === b.entry_amount_lamports && a.total_committed_lamports === b.total_committed_lamports && a.payout_deadline_ms === b.payout_deadline_ms && a.payout_window_ms === b.payout_window_ms && a.fragment_count === b.fragment_count && a.payout_mode === b.payout_mode && a.matching_mode === b.matching_mode && a.fragmentation_allowed === b.fragmentation_allowed && a.min_fee_bps === b.min_fee_bps && a.fee_bps === b.fee_bps && a.fee_lamports === b.fee_lamports && a.net_payout_lamports === b.net_payout_lamports && payoutSchedulesEqual(a.payout_schedule, b.payout_schedule);
1109
+ }
1110
+ function payoutSchedulesEqual(a, b) {
1111
+ if (a.length !== b.length) return false;
1112
+ return a.every((entry, index) => entry.destination_address === b[index]?.destination_address);
1113
+ }
1114
+ function isRequestTimeoutError(err) {
1115
+ return err instanceof CommandError && err.message.includes(REQUEST_TIMEOUT_MESSAGE);
1116
+ }
1117
+ function isRetryableSwapStatusReconciliationError(err) {
1118
+ if (err instanceof TransportError || err instanceof RoutingError) return true;
1119
+ if (err instanceof AttestationError) return false;
1120
+ return err instanceof CommandError && err.code === "REJECTED_STALE_JOB";
1121
+ }
1122
+ function coordinatorErrorFromEnvelope(protocol, env) {
1123
+ try {
1124
+ const payload = protocol.asErrorPayload(env);
1125
+ return coordinatorProtocolError(payload.code, payload.message, payload.failed_req_id);
1126
+ } catch (err) {
1127
+ return coordinatorProtocolError(
1128
+ "ERR_INTERNAL",
1129
+ err instanceof Error ? err.message : "coordinator sent an invalid error payload"
1130
+ );
1131
+ }
1132
+ }
1133
+ function coordinatorProtocolError(code, message, failedReqId) {
1134
+ const error = new Error(message);
1135
+ error.name = "CoordinatorProtocolError";
1136
+ error.code = code;
1137
+ if (failedReqId !== void 0) error.failedReqId = failedReqId;
1138
+ return error;
1139
+ }
1140
+
1141
+ // src/user/index.ts
1142
+ var MIN_ENTRY_AMOUNT_LAMPORTS = PRODUCT_MIN_ENTRY_LAMPORTS;
1143
+ var MAX_ENTRY_AMOUNT_LAMPORTS = PRODUCT_MAX_ENTRY_LAMPORTS;
1144
+ var CURRENT_USER_MIN_LAMPORTS_UNSUPPORTED_MESSAGE = "destinations[*].minLamports is not supported in the current normal-user wire contract";
1145
+ async function contractRequest(session, args) {
1146
+ const payoutPolicy = validatePayoutPolicy(args.payoutPolicy, {
1147
+ totalLamports: args.amountLamports
1148
+ });
1149
+ assertEntryAmount(args.amountLamports);
1150
+ assertCurrentUserContractPolicy(payoutPolicy);
1151
+ assertAttested(session);
1152
+ const destination = payoutPolicy.destinations[0];
1153
+ const payoutSpec = payoutSpecFromPolicy(payoutPolicy, destination.address);
1154
+ return contractRequestOnSession(session, {
1155
+ amountLamports: args.amountLamports,
1156
+ payoutSpec,
1157
+ ...args.syncSecret !== void 0 && { syncSecret: args.syncSecret },
1158
+ ...args.recoveryCommitment !== void 0 && {
1159
+ recoveryCommitment: args.recoveryCommitment
1160
+ },
1161
+ ...args.onSyncSecretReady !== void 0 && {
1162
+ onSyncSecretReady: args.onSyncSecretReady
1163
+ },
1164
+ ...args.onRecoveryCodeReady !== void 0 && {
1165
+ onRecoveryCodeReady: args.onRecoveryCodeReady
1166
+ }
1167
+ });
1168
+ }
1169
+ async function sync(session, contractId, syncSecret, timeoutMs) {
1170
+ assertAttested(session);
1171
+ return requestUserSync(session, contractId, syncSecret, timeoutMs);
1172
+ }
1173
+ function assertEntryAmount(amountLamports) {
1174
+ if (!Number.isSafeInteger(amountLamports) || amountLamports < MIN_ENTRY_AMOUNT_LAMPORTS || amountLamports > MAX_ENTRY_AMOUNT_LAMPORTS) {
1175
+ throw new PolicyValidationError(
1176
+ "INVALID_AMOUNT",
1177
+ `amountLamports must be an integer between ${MIN_ENTRY_AMOUNT_LAMPORTS} and ${MAX_ENTRY_AMOUNT_LAMPORTS} lamports`
1178
+ );
1179
+ }
1180
+ }
1181
+ function assertCurrentUserContractPolicy(policy) {
1182
+ for (const destination of policy.destinations) {
1183
+ if (destination.minLamports !== void 0) {
1184
+ throw new PolicyValidationError(
1185
+ "INVALID_PAYOUT_PLAN",
1186
+ CURRENT_USER_MIN_LAMPORTS_UNSUPPORTED_MESSAGE
1187
+ );
1188
+ }
1189
+ }
1190
+ }
1191
+ function payoutSpecFromPolicy(policy, destinationAddress) {
1192
+ const totalDeadlineMs = policy.totalDeadlineMs;
1193
+ if (totalDeadlineMs === void 0 || totalDeadlineMs === INSTANT_PAYOUT_WINDOW_MS) {
1194
+ return {
1195
+ mode: "instant",
1196
+ destination_address: destinationAddress
1197
+ };
1198
+ }
1199
+ return {
1200
+ mode: "scheduled",
1201
+ num_fragments: CURRENT_USER_PAYOUT_FRAGMENT_LIMIT,
1202
+ window_seconds: totalDeadlineMs / PAYOUT_WINDOW_MILLISECONDS_PER_SECOND,
1203
+ destination_addresses: [destinationAddress]
1204
+ };
1205
+ }
1206
+ async function requestRefund(session, args) {
1207
+ assertAttested(session);
1208
+ assertRecoveryCode(args.recoveryCode);
1209
+ return requestRefundOnSession(session, {
1210
+ recoveryCode: new Uint8Array(args.recoveryCode)
1211
+ });
1212
+ }
1213
+ async function getDepositAddress(_session, _requestId, _syncSecret) {
1214
+ throw new NotImplementedError("user.getDepositAddress");
1215
+ }
1216
+ function subscribe(_session, _requestId, _syncSecret, _handler) {
1217
+ throw new NotImplementedError("user.subscribe");
1218
+ }
1219
+ async function replayFromStorage(_session, _storage) {
1220
+ throw new NotImplementedError("user.replayFromStorage");
1221
+ }
1222
+
1223
+ export { INSTANT_PAYOUT_WINDOW_MS, MAX_ENTRY_AMOUNT_LAMPORTS, MIN_ENTRY_AMOUNT_LAMPORTS, PAYOUT_WINDOW_MILLISECONDS_PER_SECOND, RECOVERY_CODE_BYTE_LENGTH, SUPPORTED_TOTAL_DEADLINE_MS, commitmentFromCode, contractRequest, createCoordinatorSession, generateRecoveryCode, getDepositAddress, isBenignTransportDrop, recoveryCodeFromHex, recoveryCodeFromUserInput, recoveryCodeToHex, replayFromStorage, requestRefund, subscribe, sync };
1224
+ //# sourceMappingURL=user.js.map
1225
+ //# sourceMappingURL=user.js.map