@invisible-labs/sdk 0.6.0-devnet.3 → 0.6.0-devnet.5

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 (45) hide show
  1. package/README.md +129 -15
  2. package/dist/.invisible-sdk-build-target.json +7 -0
  3. package/dist/{chunk-QQSI3ZD7.js → chunk-3MDHAS4D.js} +109 -17
  4. package/dist/chunk-3MDHAS4D.js.map +1 -0
  5. package/dist/{chunk-NKRVQXRP.js → chunk-3TGQ4CZ3.js} +297 -11
  6. package/dist/chunk-3TGQ4CZ3.js.map +1 -0
  7. package/dist/chunk-DCTYBUY3.js +258 -0
  8. package/dist/chunk-DCTYBUY3.js.map +1 -0
  9. package/dist/{chunk-NHEHCUS5.js → chunk-EBWTAXNT.js} +3 -3
  10. package/dist/{chunk-NHEHCUS5.js.map → chunk-EBWTAXNT.js.map} +1 -1
  11. package/dist/{chunk-V5HXJRBW.js → chunk-HJFMDMPY.js} +3 -3
  12. package/dist/chunk-HJFMDMPY.js.map +1 -0
  13. package/dist/{chunk-GHBQF3A7.js → chunk-QRN46R3F.js} +5 -2
  14. package/dist/chunk-QRN46R3F.js.map +1 -0
  15. package/dist/chunk-Y7AK6RBV.js +4189 -0
  16. package/dist/chunk-Y7AK6RBV.js.map +1 -0
  17. package/dist/{coordinator-VVJ33WF2.js → coordinator-PEWHNZ7I.js} +3 -3
  18. package/dist/{coordinator-VVJ33WF2.js.map → coordinator-PEWHNZ7I.js.map} +1 -1
  19. package/dist/events.js +3 -3
  20. package/dist/index.d.ts +69 -353
  21. package/dist/index.js +161 -43
  22. package/dist/index.js.map +1 -1
  23. package/dist/lp.d.ts +76 -18
  24. package/dist/lp.js +511 -111
  25. package/dist/lp.js.map +1 -1
  26. package/dist/presets.d.ts +7 -40
  27. package/dist/presets.js +10 -68
  28. package/dist/presets.js.map +1 -1
  29. package/dist/stats.js +1 -1
  30. package/dist/storage.js +2 -243
  31. package/dist/storage.js.map +1 -1
  32. package/dist/{errors-N2touVm4.d.ts → types.generated-YhFu9ZBR.d.ts} +65 -4
  33. package/dist/user.d.ts +134 -8
  34. package/dist/user.js +991 -250
  35. package/dist/user.js.map +1 -1
  36. package/package.json +5 -31
  37. package/dist/chunk-BWCGNTN5.js +0 -1153
  38. package/dist/chunk-BWCGNTN5.js.map +0 -1
  39. package/dist/chunk-DIA6U2CV.js +0 -1927
  40. package/dist/chunk-DIA6U2CV.js.map +0 -1
  41. package/dist/chunk-GHBQF3A7.js.map +0 -1
  42. package/dist/chunk-NKRVQXRP.js.map +0 -1
  43. package/dist/chunk-QQSI3ZD7.js.map +0 -1
  44. package/dist/chunk-V5HXJRBW.js.map +0 -1
  45. package/dist/types.generated-BSEBz4jR.d.ts +0 -68
package/dist/user.js CHANGED
@@ -1,12 +1,13 @@
1
- import { scheduledPayoutWindowSeconds } from './chunk-NKRVQXRP.js';
2
- import { validatePayoutPolicyShape, formatAjvErrors, createSession, createExponentialBackoff } from './chunk-BWCGNTN5.js';
1
+ import { scheduledPayoutWindowSeconds } from './chunk-3TGQ4CZ3.js';
2
+ import { validatePayoutPolicyShape, formatAjvErrors, persistTransferStatus, createSession, disabledClientPersistence, readPersistedTransferForResume, persistTransferReady, createExponentialBackoff, persistTransferTerminal, persistTransferRecoveryCode, persistTransferAllocation, persistTransferDkgMetadata } from './chunk-Y7AK6RBV.js';
3
+ export { getPersistedTransfer, listPersistedTransfers, purgePersistedTransfers, removePersistedTransfer } from './chunk-Y7AK6RBV.js';
3
4
  import { PRODUCT_MAX_ENTRY_LAMPORTS, PRODUCT_MIN_ENTRY_LAMPORTS } from './chunk-4X5OCCQW.js';
4
- import { assertAttested, publicKey, solanaAddressFromPublicKeyBytes } from './chunk-NHEHCUS5.js';
5
- import { requestEnvelope, CoordinatorMessageType, onEnvelope, closeProtocolRuntime, EnvelopeKind } from './chunk-QQSI3ZD7.js';
6
- import { getSessionState } from './chunk-V5HXJRBW.js';
7
- import './chunk-DIA6U2CV.js';
8
- import { PolicyValidationError, CommandError, TransportError, InvalidDepositAmountError, normalizeError, RoutingError, AttestationError } from './chunk-GHBQF3A7.js';
9
- export { InvalidDepositAmountError } from './chunk-GHBQF3A7.js';
5
+ import { assertAttested, publicKey, solanaAddressFromPublicKeyBytes } from './chunk-EBWTAXNT.js';
6
+ import { requestEnvelope, CoordinatorMessageType, onEnvelope, closeProtocolRuntime, EnvelopeKind } from './chunk-3MDHAS4D.js';
7
+ import { getSessionState } from './chunk-HJFMDMPY.js';
8
+ import './chunk-DCTYBUY3.js';
9
+ import { PolicyValidationError, CommandError, StorageError, TransportError, normalizeError, InvalidDepositAmountError, AttestationError, RoutingError } from './chunk-QRN46R3F.js';
10
+ export { InvalidDepositAmountError } from './chunk-QRN46R3F.js';
10
11
  import './chunk-VR6T6OJS.js';
11
12
 
12
13
  // src/validation/payoutPolicy.ts
@@ -171,7 +172,7 @@ function recoveryCodeFromUserInput(input) {
171
172
  var protocolPromise = null;
172
173
  var frostRuntimePromise = null;
173
174
  function loadProtocol() {
174
- protocolPromise ??= import('./coordinator-VVJ33WF2.js');
175
+ protocolPromise ??= import('./coordinator-PEWHNZ7I.js');
175
176
  return protocolPromise;
176
177
  }
177
178
  function loadFrostRuntime() {
@@ -185,20 +186,22 @@ var BENIGN_TRANSPORT_DROP = /* @__PURE__ */ Symbol("benignTransportDrop");
185
186
  var ALICE_ID = 1;
186
187
  var COORDINATOR_ID = 2;
187
188
  var SYNC_SECRET_BYTE_LENGTH = 32;
189
+ var RESUME_SECRET_BYTE_LENGTH = SYNC_SECRET_BYTE_LENGTH;
188
190
  var SYNC_CONTRACT_ID_MISMATCH_MESSAGE = "sync response contract id does not match requested contract";
189
- var ACTIVE_CONTRACT_REQUEST_MESSAGE = "coordinator session already has an active contract";
190
191
  var DEFAULT_SWAP_STATUS_RECONCILE_INTERVAL_MS = 5e3;
191
192
  var MAX_SWAP_STATUS_RETRY_DELAY_MS = 3e4;
192
193
  var SWAP_STATUS_RETRY_MULTIPLIER = 2;
194
+ var MAX_EVENT_DEDUP_ENTRIES_PER_KIND = 1024;
193
195
  var SWAP_STATUS_RETRY_JITTER_RATIO = 0.2;
194
196
  var DEFAULT_REFUND_OUTCOME_POLL_BUDGET_MS = 5 * 60 * 1e3;
195
197
  var REFUND_OUTCOME_POLL_INTERVAL_MS = 3e3;
196
198
  var REFUND_OUTCOME_PER_POLL_TIMEOUT_MS = 5e3;
199
+ var UNBOUND_RECONCILIATION_KEY = "__unbound__";
197
200
  var REQUEST_TIMEOUT_MESSAGE = "coordinator request timed out";
201
+ var REFUND_PENDING_FINALIZATION_MESSAGE = "refund accepted and pending finalization; check status again shortly";
198
202
  var CURRENT_SCHEDULED_PAYOUT_WINDOW_SECONDS = new Set(scheduledPayoutWindowSeconds());
199
203
  var CURRENT_SCHEDULED_PAYOUT_WINDOW_MESSAGE = `scheduled payout_spec.window_seconds must be one of [${scheduledPayoutWindowSeconds().join(", ")}]`;
200
204
  var TERMINAL_SWAP_STATES = /* @__PURE__ */ new Set(["completed", "refunded", "failed"]);
201
- var activeFunctionalContractRequests = /* @__PURE__ */ new WeakSet();
202
205
  function isBenignTransportDrop(err) {
203
206
  return typeof err === "object" && err !== null && err[BENIGN_TRANSPORT_DROP] === true;
204
207
  }
@@ -210,6 +213,16 @@ function readDkgComplete(protocol, envelope) {
210
213
  coordinatorTimestampMs: envelope.timestamp_ms
211
214
  };
212
215
  }
216
+ function assertDepositConfirmedAmount(protocol, envelope, expectedAmountLamports) {
217
+ const payload = protocol.asDepositConfirmedPayload(envelope);
218
+ if (payload.amount_lamports !== expectedAmountLamports) {
219
+ throw new InvalidDepositAmountError(
220
+ BigInt(payload.amount_lamports),
221
+ BigInt(expectedAmountLamports)
222
+ );
223
+ }
224
+ return payload;
225
+ }
213
226
  function tagBenignTransportDrop(err) {
214
227
  err[BENIGN_TRANSPORT_DROP] = true;
215
228
  return err;
@@ -227,22 +240,27 @@ var UserCoordinatorSession = class {
227
240
  this.sessionStatus = "idle";
228
241
  this.swapId = null;
229
242
  this.syncSecret = null;
243
+ this.resumeSecret = null;
230
244
  this.attested = false;
231
245
  this.pushListeners = /* @__PURE__ */ new Map();
246
+ this.swapContexts = /* @__PURE__ */ new Map();
247
+ this.openingSession = null;
248
+ this.activeOperations = 0;
232
249
  this.stateListeners = /* @__PURE__ */ new Set();
233
250
  this.logListeners = /* @__PURE__ */ new Set();
234
251
  this.payoutExecutedListeners = /* @__PURE__ */ new Set();
235
252
  this.refundExecutedListeners = /* @__PURE__ */ new Set();
236
253
  this.normalUserActorSyncListeners = /* @__PURE__ */ new Set();
237
254
  this.coordinatorErrorListeners = /* @__PURE__ */ new Set();
255
+ this.storageErrorListeners = /* @__PURE__ */ new Set();
238
256
  this.emittedPayoutKeys = /* @__PURE__ */ new Set();
239
257
  this.emittedRefundKeys = /* @__PURE__ */ new Set();
240
- this.stopStatusReconciliation = null;
241
- this.bufferDepositConfirmedUntilDelegateAck = false;
242
- this.bufferedDepositConfirmed = null;
258
+ this.statusReconciliationStops = /* @__PURE__ */ new Map();
259
+ this.pushProcessing = /* @__PURE__ */ new Map();
260
+ this.terminalOutcomes = /* @__PURE__ */ new Map();
243
261
  this.coordinator = options.coordinator;
244
262
  this.createSessionImpl = options.createSession ?? createSession;
245
- this.closeSessionAfterRefund = closeSessionAfterRefund;
263
+ this.closeSessionAfterOperationFailure = closeSessionAfterRefund;
246
264
  }
247
265
  onStateChange(listener) {
248
266
  this.stateListeners.add(listener);
@@ -268,6 +286,10 @@ var UserCoordinatorSession = class {
268
286
  this.coordinatorErrorListeners.add(listener);
269
287
  return () => this.coordinatorErrorListeners.delete(listener);
270
288
  }
289
+ onStorageError(listener) {
290
+ this.storageErrorListeners.add(listener);
291
+ return () => this.storageErrorListeners.delete(listener);
292
+ }
271
293
  get currentSwapState() {
272
294
  return this.swapState;
273
295
  }
@@ -278,51 +300,301 @@ var UserCoordinatorSession = class {
278
300
  get currentSwapId() {
279
301
  return this.swapId;
280
302
  }
303
+ clientPersistenceState() {
304
+ return this.session ? getSessionState(this.session).clientPersistence : disabledClientPersistence();
305
+ }
306
+ beginOperation() {
307
+ this.activeOperations += 1;
308
+ if (this.activeOperations === 1) this.setSessionStatus("connecting");
309
+ }
310
+ endOperation() {
311
+ this.activeOperations = Math.max(0, this.activeOperations - 1);
312
+ if (this.activeOperations === 0 && this.sessionStatus === "connecting") {
313
+ this.setSessionStatus("idle");
314
+ }
315
+ }
316
+ ensureSwapContext(swapId, syncSecret, resumeSecret) {
317
+ let context = this.swapContexts.get(swapId);
318
+ if (!context) {
319
+ context = {
320
+ swapId,
321
+ requestedAmountLamports: null,
322
+ swapState: this.swapId === swapId ? this.swapState : "new",
323
+ syncSecret: this.swapId === swapId && this.syncSecret ? new Uint8Array(this.syncSecret) : null,
324
+ resumeSecret: this.swapId === swapId && this.resumeSecret ? new Uint8Array(this.resumeSecret) : null,
325
+ bufferDepositConfirmedUntilDelegateAck: false,
326
+ bufferedDepositConfirmed: null
327
+ };
328
+ this.swapContexts.set(swapId, context);
329
+ }
330
+ if (syncSecret !== void 0) context.syncSecret = new Uint8Array(syncSecret);
331
+ if (resumeSecret !== void 0) context.resumeSecret = new Uint8Array(resumeSecret);
332
+ this.setCurrentContext(context);
333
+ return context;
334
+ }
335
+ setCurrentContext(context) {
336
+ this.swapId = context.swapId;
337
+ this.swapState = context.swapState;
338
+ this.syncSecret = context.syncSecret ? new Uint8Array(context.syncSecret) : null;
339
+ this.resumeSecret = context.resumeSecret ? new Uint8Array(context.resumeSecret) : null;
340
+ }
341
+ updateContextState(context, state) {
342
+ context.swapState = state;
343
+ if (this.swapId === context.swapId) {
344
+ this.swapState = state;
345
+ }
346
+ if (TERMINAL_SWAP_STATES.has(state)) {
347
+ this.evictSwapContext(context.swapId);
348
+ }
349
+ this.emitStateChange(context.swapId, state);
350
+ }
351
+ evictSwapContext(swapId) {
352
+ this.statusReconciliationStops.get(swapId)?.();
353
+ this.statusReconciliationStops.delete(swapId);
354
+ const context = this.swapContexts.get(swapId);
355
+ if (context) {
356
+ if (isTerminalSwapState(context.swapState)) {
357
+ this.rememberTerminalOutcome(swapId, context.swapState);
358
+ }
359
+ context.syncSecret?.fill(0);
360
+ context.resumeSecret?.fill(0);
361
+ context.bufferedDepositConfirmed = null;
362
+ context.bufferDepositConfirmedUntilDelegateAck = false;
363
+ this.swapContexts.delete(swapId);
364
+ }
365
+ if (this.swapId === swapId) {
366
+ this.syncSecret?.fill(0);
367
+ this.resumeSecret?.fill(0);
368
+ this.syncSecret = null;
369
+ this.resumeSecret = null;
370
+ }
371
+ }
372
+ clearSwapContexts() {
373
+ for (const swapId of this.swapContexts.keys()) this.evictSwapContext(swapId);
374
+ this.swapContexts.clear();
375
+ this.pushProcessing.clear();
376
+ this.terminalOutcomes.clear();
377
+ this.emittedPayoutKeys.clear();
378
+ this.emittedRefundKeys.clear();
379
+ }
380
+ isActiveSwapContext(context) {
381
+ return this.swapContexts.get(context.swapId) === context;
382
+ }
383
+ rememberTerminalOutcome(swapId, state, refund, storageError) {
384
+ const previous = this.terminalOutcomes.get(swapId);
385
+ this.terminalOutcomes.set(swapId, {
386
+ state,
387
+ ...refund !== void 0 && refund !== null ? { refund } : previous?.refund !== void 0 ? { refund: previous.refund } : {},
388
+ ...storageError !== void 0 ? { storageError } : previous?.storageError !== void 0 ? { storageError: previous.storageError } : {}
389
+ });
390
+ if (this.terminalOutcomes.size > MAX_EVENT_DEDUP_ENTRIES_PER_KIND) {
391
+ const oldest = this.terminalOutcomes.keys().next().value;
392
+ if (oldest !== void 0) this.terminalOutcomes.delete(oldest);
393
+ }
394
+ }
281
395
  async runSwap(params) {
282
396
  assertCurrentUserPayoutSpecRuntimeLimits(params.payoutSpec);
283
- this.assertNoActiveContract();
284
- this.setSessionStatus("connecting");
397
+ this.beginOperation();
285
398
  let completed = false;
399
+ let terminalStatus = null;
286
400
  try {
287
401
  await ensureFrostWasm();
288
402
  await this.openSession();
289
403
  this.setSessionStatus("running");
290
404
  const result = await this.runProtocol(params);
291
405
  completed = true;
292
- this.setSessionStatus("done");
406
+ terminalStatus = "done";
293
407
  return result;
294
408
  } catch (err) {
295
409
  const error = this.normalizeSessionError(err);
296
410
  if (!isBenignTransportDrop(error)) {
297
- this.setSessionStatus("failed");
411
+ terminalStatus = "failed";
298
412
  }
299
413
  throw error;
300
414
  } finally {
301
- if (!completed) {
415
+ this.endOperation();
416
+ if (this.activeOperations === 0 && terminalStatus !== null) {
417
+ this.setSessionStatus(terminalStatus);
418
+ }
419
+ if (!completed && this.activeOperations === 0 && this.closeSessionAfterOperationFailure) {
302
420
  this.close();
303
421
  }
304
422
  }
305
423
  }
306
424
  async contractRequest(params) {
307
425
  assertCurrentUserPayoutSpecRuntimeLimits(params.payoutSpec);
308
- this.assertNoActiveContract();
309
- this.setSessionStatus("connecting");
426
+ this.beginOperation();
427
+ let completed = false;
428
+ let terminalStatus = null;
310
429
  try {
311
430
  await ensureFrostWasm();
312
431
  await this.openSession();
313
432
  this.setSessionStatus("running");
314
433
  const prepared = await this.prepareContract(params);
434
+ completed = true;
435
+ terminalStatus = "done";
315
436
  return toContractRequestResult(prepared);
316
437
  } catch (err) {
317
438
  const error = this.normalizeSessionError(err);
318
- this.setSessionStatus("failed");
439
+ terminalStatus = "failed";
319
440
  throw error;
441
+ } finally {
442
+ this.endOperation();
443
+ if (this.activeOperations === 0 && terminalStatus !== null) {
444
+ this.setSessionStatus(terminalStatus);
445
+ }
446
+ if (!completed && this.activeOperations === 0 && this.closeSessionAfterOperationFailure) {
447
+ this.close();
448
+ }
449
+ }
450
+ }
451
+ async resumePreDelegation(params) {
452
+ if (!params.policySnapshot) {
453
+ throw new CommandError(
454
+ "REJECTED_GUARD",
455
+ "policySnapshot is required to resume pre-delegation"
456
+ );
457
+ }
458
+ if (params.recoveryCommitment.length !== SYNC_SECRET_BYTE_LENGTH) {
459
+ throw new CommandError(
460
+ "REJECTED_GUARD",
461
+ `recoveryCommitment must be ${SYNC_SECRET_BYTE_LENGTH} bytes`
462
+ );
463
+ }
464
+ this.beginOperation();
465
+ let completed = false;
466
+ let terminalStatus = null;
467
+ try {
468
+ await ensureFrostWasm();
469
+ this.restoreSwapContext({
470
+ swapId: params.swapId,
471
+ syncSecret: params.syncSecret,
472
+ resumeSecret: params.resumeSecret
473
+ });
474
+ await this.openSession();
475
+ this.setSessionStatus("running");
476
+ const context = this.ensureSwapContext(params.swapId);
477
+ context.swapState = params.delegateShare ? "awaiting_deposit" : "awaiting_dkg";
478
+ context.bufferDepositConfirmedUntilDelegateAck = true;
479
+ this.setCurrentContext(context);
480
+ let jointPublicKey;
481
+ let depositAddress;
482
+ let depositExpiresAtMs;
483
+ let coordinatorTimestampMs;
484
+ let delegateShare = params.delegateShare ? new Uint8Array(params.delegateShare) : void 0;
485
+ const persistence = this.clientPersistenceState();
486
+ const persistedResumeRecord = await readPersistedTransferForResume(
487
+ persistence,
488
+ context.swapId
489
+ );
490
+ if (params.delegateShare !== void 0 && persistedResumeRecord !== null) {
491
+ if (persistedResumeRecord.depositAddress === void 0 || persistedResumeRecord.depositExpiresAtMs === void 0 || persistedResumeRecord.coordinatorTimestampMs === void 0) {
492
+ throw new StorageError(
493
+ "STORAGE_KEY_MISSING",
494
+ "persisted DKG metadata is missing for saved delegate share",
495
+ { phase: "ready", swapId: context.swapId, remoteAccepted: false }
496
+ );
497
+ }
498
+ depositAddress = persistedResumeRecord.depositAddress;
499
+ depositExpiresAtMs = persistedResumeRecord.depositExpiresAtMs;
500
+ coordinatorTimestampMs = persistedResumeRecord.coordinatorTimestampMs;
501
+ }
502
+ if (!delegateShare) {
503
+ const dkg = await this.runDkg(
504
+ context,
505
+ params.onDelegateShareReady,
506
+ persistedResumeRecord !== null
507
+ );
508
+ jointPublicKey = dkg.jointPublicKey;
509
+ depositAddress = solanaAddressFromPublicKeyBytes(jointPublicKey);
510
+ depositExpiresAtMs = dkg.depositExpiresAtMs;
511
+ coordinatorTimestampMs = dkg.coordinatorTimestampMs;
512
+ delegateShare = dkg.keyHandle.secretShareBytes();
513
+ this.updateContextState(context, "awaiting_deposit");
514
+ }
515
+ const delegateAckPromise = this.awaitPush(CoordinatorMessageType.DelegateAck, context.swapId);
516
+ let delegateResp;
517
+ try {
518
+ delegateResp = await this.request(
519
+ CoordinatorMessageType.DelegateShare,
520
+ {
521
+ share: delegateShare,
522
+ recovery_commitment: new Uint8Array(params.recoveryCommitment),
523
+ resume_secret: context.resumeSecret
524
+ },
525
+ { swapId: context.swapId }
526
+ );
527
+ } catch (err) {
528
+ void delegateAckPromise.catch(() => void 0);
529
+ throw err;
530
+ }
531
+ const delegatedAtMs = this.requireProtocol().asDelegateShareResponsePayload(delegateResp).delegated_at_ms;
532
+ const delegateAck = this.requireProtocol().asDelegateAckPayload(await delegateAckPromise);
533
+ if (delegateAck.delegated_at_ms !== delegatedAtMs) {
534
+ throw new CommandError(
535
+ "REJECTED_GUARD",
536
+ "DelegateAck delegated_at_ms does not match DelegateShare response"
537
+ );
538
+ }
539
+ if (!spendingPoliciesEqual(delegateAck.spending_policy, params.policySnapshot)) {
540
+ throw new CommandError(
541
+ "REJECTED_GUARD",
542
+ "DelegateAck spending_policy does not match persisted policy_snapshot"
543
+ );
544
+ }
545
+ context.requestedAmountLamports = delegateAck.spending_policy.entry_amount_lamports;
546
+ if (context.bufferedDepositConfirmed) {
547
+ assertDepositConfirmedAmount(
548
+ this.requireProtocol(),
549
+ context.bufferedDepositConfirmed,
550
+ delegateAck.spending_policy.entry_amount_lamports
551
+ );
552
+ }
553
+ if (persistedResumeRecord !== null) {
554
+ if (depositAddress === void 0 || depositExpiresAtMs === void 0 || coordinatorTimestampMs === void 0) {
555
+ throw new StorageError(
556
+ "STORAGE_KEY_MISSING",
557
+ "persisted DKG metadata is missing for saved delegate share",
558
+ { phase: "ready", swapId: context.swapId, remoteAccepted: true }
559
+ );
560
+ }
561
+ await persistTransferReady(persistence, {
562
+ swapId: context.swapId,
563
+ depositAddress,
564
+ depositExpiresAtMs,
565
+ delegatedAtMs,
566
+ coordinatorTimestampMs
567
+ });
568
+ }
569
+ const hadBufferedDeposit = context.bufferedDepositConfirmed !== null;
570
+ await this.releaseDepositConfirmedGate(context);
571
+ if (!hadBufferedDeposit && this.isActiveSwapContext(context)) {
572
+ this.updateContextState(context, "awaiting_deposit");
573
+ }
574
+ completed = true;
575
+ terminalStatus = "done";
576
+ return {
577
+ swapId: context.swapId,
578
+ jointPublicKey,
579
+ depositExpiresAtMs,
580
+ coordinatorTimestampMs,
581
+ delegatedAtMs
582
+ };
583
+ } catch (err) {
584
+ terminalStatus = "failed";
585
+ throw this.normalizeSessionError(err);
586
+ } finally {
587
+ if (!completed) this.cancelPushWaitersForSwap(params.swapId);
588
+ this.endOperation();
589
+ if (this.activeOperations === 0 && terminalStatus !== null) {
590
+ this.setSessionStatus(terminalStatus);
591
+ }
320
592
  }
321
593
  }
322
594
  /** @internal Used by the functional Session API without closing its caller-owned session. */
323
595
  releaseWithoutClosing() {
324
- this.stopStatusReconciliation?.();
325
- this.stopStatusReconciliation = null;
596
+ this.stopAllStatusReconciliations();
597
+ this.clearSwapContexts();
326
598
  this.detachSession(
327
599
  new TransportError("CONNECTION_LOST", "coordinator session operation released"),
328
600
  false,
@@ -330,12 +602,16 @@ var UserCoordinatorSession = class {
330
602
  );
331
603
  }
332
604
  close() {
333
- this.stopStatusReconciliation?.();
334
- this.stopStatusReconciliation = null;
605
+ this.stopAllStatusReconciliations();
606
+ this.clearSwapContexts();
335
607
  this.detachSession(new TransportError("CONNECTION_LOST", "coordinator session closed"), true);
336
608
  }
609
+ stopAllStatusReconciliations() {
610
+ for (const stop of [...this.statusReconciliationStops.values()]) stop();
611
+ this.statusReconciliationStops.clear();
612
+ }
337
613
  restoreSwapContext(params) {
338
- this.swapId = params.swapId;
614
+ if (this.terminalOutcomes.has(params.swapId)) return;
339
615
  if (params.syncSecret !== void 0) {
340
616
  if (params.syncSecret.length !== SYNC_SECRET_BYTE_LENGTH) {
341
617
  throw new CommandError(
@@ -343,28 +619,52 @@ var UserCoordinatorSession = class {
343
619
  `syncSecret must be ${SYNC_SECRET_BYTE_LENGTH} bytes`
344
620
  );
345
621
  }
346
- this.syncSecret = new Uint8Array(params.syncSecret);
347
622
  }
623
+ if (params.resumeSecret !== void 0) {
624
+ if (params.resumeSecret.length !== RESUME_SECRET_BYTE_LENGTH) {
625
+ throw new CommandError(
626
+ "REJECTED_GUARD",
627
+ `resumeSecret must be ${RESUME_SECRET_BYTE_LENGTH} bytes`
628
+ );
629
+ }
630
+ }
631
+ this.ensureSwapContext(params.swapId, params.syncSecret, params.resumeSecret);
348
632
  }
349
633
  async requestSwapStatus(timeoutMs) {
350
634
  if (!this.swapId) {
351
635
  throw new CommandError("REJECTED_STATE", "Sync requires swapId");
352
636
  }
637
+ const context = this.ensureSwapContext(this.swapId, this.syncSecret ?? void 0);
638
+ return this.requestSwapStatusForContext(context, timeoutMs);
639
+ }
640
+ async requestSwapStatusForContext(context, timeoutMs) {
353
641
  await this.openSession();
354
642
  const env = await this.request(
355
643
  CoordinatorMessageType.Sync,
356
644
  {
357
645
  actor: "user",
358
- ...this.syncSecret ? { sync_secret: this.syncSecret } : {}
646
+ ...context.syncSecret ? { sync_secret: context.syncSecret } : {}
359
647
  },
360
- timeoutMs
648
+ {
649
+ swapId: context.swapId,
650
+ timeoutMs,
651
+ rejectMatchingOutOfBandErrors: true
652
+ }
361
653
  );
362
654
  const status = this.requireProtocol().asSyncUserResponsePayload(env);
363
- this.applySwapStatus(status, env.timestamp_ms);
655
+ const persistence = this.clientPersistenceState();
656
+ await this.enqueueSwapProcessing(context.swapId, async () => {
657
+ if (!this.isActiveSwapContext(context)) return;
658
+ await persistTransferStatus(persistence, context.swapId, status, env.timestamp_ms);
659
+ if (!this.isActiveSwapContext(context)) return;
660
+ this.applySwapStatus(context, status, env.timestamp_ms);
661
+ });
364
662
  return status;
365
663
  }
366
664
  startSwapStatusReconciliation(intervalMs = DEFAULT_SWAP_STATUS_RECONCILE_INTERVAL_MS) {
367
- this.stopStatusReconciliation?.();
665
+ const reconciliationContext = this.swapId ? this.ensureSwapContext(this.swapId, this.syncSecret ?? void 0) : null;
666
+ const reconciliationKey = reconciliationContext?.swapId ?? UNBOUND_RECONCILIATION_KEY;
667
+ this.statusReconciliationStops.get(reconciliationKey)?.();
368
668
  let stopped = false;
369
669
  let reconciling = false;
370
670
  let timer = null;
@@ -389,20 +689,18 @@ var UserCoordinatorSession = class {
389
689
  if (stopped || reconciling || !isVisible()) return;
390
690
  reconciling = true;
391
691
  try {
392
- const status = await this.requestSwapStatus();
692
+ const status = reconciliationContext ? await this.requestSwapStatusForContext(reconciliationContext) : await this.requestSwapStatus();
393
693
  retryBackoff.reset();
394
694
  if (status.state === "completed" || status.state === "refunded" || status.state === "failed") {
395
- stopped = true;
396
- if (timer) clearTimeout(timer);
695
+ stopReconciliation();
397
696
  } else {
398
697
  schedule(intervalMs);
399
698
  }
400
699
  } catch (err) {
401
700
  this.log(`SwapStatus reconcile failed: ${normalizeError(err)}`, "info");
402
701
  if (!isRetryableSwapStatusReconciliationError(err)) {
403
- stopped = true;
404
- if (timer) clearTimeout(timer);
405
- this.close();
702
+ stopReconciliation();
703
+ if (err instanceof AttestationError) this.close();
406
704
  return;
407
705
  }
408
706
  schedule(retryBackoff.nextDelayMs());
@@ -438,7 +736,8 @@ var UserCoordinatorSession = class {
438
736
  browserDocument?.addEventListener("freeze", handleFreeze);
439
737
  browserDocument?.addEventListener("resume", reconcileWhenVisible);
440
738
  reconcileWhenVisible();
441
- this.stopStatusReconciliation = () => {
739
+ const stopReconciliation = () => {
740
+ if (stopped) return;
442
741
  stopped = true;
443
742
  if (timer) clearTimeout(timer);
444
743
  browserWindow?.removeEventListener("focus", reconcileWhenVisible);
@@ -447,8 +746,12 @@ var UserCoordinatorSession = class {
447
746
  browserDocument?.removeEventListener("visibilitychange", handleVisibilityChange);
448
747
  browserDocument?.removeEventListener("freeze", handleFreeze);
449
748
  browserDocument?.removeEventListener("resume", reconcileWhenVisible);
749
+ if (this.statusReconciliationStops.get(reconciliationKey) === stopReconciliation) {
750
+ this.statusReconciliationStops.delete(reconciliationKey);
751
+ }
450
752
  };
451
- return this.stopStatusReconciliation;
753
+ this.statusReconciliationStops.set(reconciliationKey, stopReconciliation);
754
+ return stopReconciliation;
452
755
  }
453
756
  async requestRefund(params) {
454
757
  const result = await this.requestRefundIntentInternal(params, { waitForFinality: true });
@@ -460,9 +763,10 @@ var UserCoordinatorSession = class {
460
763
  }
461
764
  async requestRefundIntentInternal(params, options) {
462
765
  assertRecoveryCode(params.recoveryCode);
766
+ this.beginOperation();
463
767
  const recoveryCode = new Uint8Array(params.recoveryCode);
464
768
  const ackTimeout = params.timeoutMs ?? 3e4;
465
- let ackTimer;
769
+ let requestSwapId = params.swapId ?? null;
466
770
  let openedForRefund = false;
467
771
  try {
468
772
  if (params.syncSecret !== void 0) {
@@ -471,34 +775,51 @@ var UserCoordinatorSession = class {
471
775
  throw new CommandError("REJECTED_STATE", "syncSecret requires swapId");
472
776
  }
473
777
  this.restoreSwapContext({ swapId, syncSecret: params.syncSecret });
778
+ this.ensureRefundContext(swapId);
779
+ requestSwapId = swapId;
474
780
  } else if (params.swapId) {
475
781
  const swapId = params.swapId;
476
782
  this.restoreSwapContext({ swapId });
783
+ this.ensureRefundContext(swapId);
784
+ requestSwapId = swapId;
477
785
  }
478
786
  openedForRefund = !this.hasUsableSession();
479
787
  await this.openSession();
480
- const env = await Promise.race([
481
- this.request(CoordinatorMessageType.WithdrawRequest, {
482
- recovery_code: recoveryCode
483
- }),
484
- new Promise((_, reject) => {
485
- ackTimer = setTimeout(
486
- () => reject(
487
- new CommandError(
488
- "REJECTED_STALE_JOB",
489
- `WithdrawRequest acknowledgement timed out after ${ackTimeout}ms`
490
- )
491
- ),
492
- ackTimeout
788
+ let env;
789
+ try {
790
+ env = await this.request(
791
+ CoordinatorMessageType.WithdrawRequest,
792
+ {
793
+ recovery_code: recoveryCode
794
+ },
795
+ { swapId: requestSwapId, timeoutMs: ackTimeout }
796
+ );
797
+ } catch (err) {
798
+ if (isRequestTimeoutError(err)) {
799
+ throw new CommandError(
800
+ "REJECTED_STALE_JOB",
801
+ `WithdrawRequest acknowledgement timed out after ${ackTimeout}ms`
493
802
  );
494
- })
495
- ]);
496
- if (ackTimer) clearTimeout(ackTimer);
803
+ }
804
+ throw err;
805
+ }
497
806
  if (env.type === CoordinatorMessageType.WithdrawExecuted) {
498
- if (env.swap_id && !this.swapId) {
499
- this.swapId = env.swap_id;
807
+ const resolvedSwapId2 = env.swap_id ?? requestSwapId;
808
+ if (resolvedSwapId2) requestSwapId = resolvedSwapId2;
809
+ if (resolvedSwapId2) await this.waitForPushProcessing(resolvedSwapId2);
810
+ const terminalOutcome2 = resolvedSwapId2 ? this.terminalOutcomes.get(resolvedSwapId2) : void 0;
811
+ if (terminalOutcome2?.storageError) throw terminalOutcome2.storageError;
812
+ if (!terminalOutcome2) this.ensureRefundContext(resolvedSwapId2);
813
+ const refund = this.requireProtocol().asWithdrawExecutedPayload(env);
814
+ if (resolvedSwapId2 && terminalOutcome2?.refund === void 0) {
815
+ await persistTransferTerminal(
816
+ this.clientPersistenceState(),
817
+ resolvedSwapId2,
818
+ "refunded",
819
+ env.timestamp_ms
820
+ );
500
821
  }
501
- return { kind: "executed", refund: this.requireProtocol().asWithdrawExecutedPayload(env) };
822
+ return { kind: "executed", refund };
502
823
  }
503
824
  if (env.type !== CoordinatorMessageType.WithdrawAccepted) {
504
825
  throw new CommandError(
@@ -506,29 +827,61 @@ var UserCoordinatorSession = class {
506
827
  `Expected WithdrawAccepted or WithdrawExecuted response, got ${env.type}`
507
828
  );
508
829
  }
509
- if (env.swap_id && !this.swapId) {
510
- this.swapId = env.swap_id;
511
- }
512
830
  const accepted = this.requireProtocol().asWithdrawAcceptedPayload(env);
831
+ const resolvedSwapId = env.swap_id ?? requestSwapId;
832
+ if (resolvedSwapId) requestSwapId = resolvedSwapId;
833
+ if (resolvedSwapId) await this.waitForPushProcessing(resolvedSwapId);
834
+ let terminalOutcome = resolvedSwapId ? this.terminalOutcomes.get(resolvedSwapId) : void 0;
835
+ if (terminalOutcome?.storageError) throw terminalOutcome.storageError;
836
+ let resolvedContext = terminalOutcome ? null : this.ensureRefundContext(resolvedSwapId);
837
+ if (resolvedSwapId && !terminalOutcome) {
838
+ await persistTransferRecoveryCode(
839
+ this.clientPersistenceState(),
840
+ resolvedSwapId,
841
+ recoveryCode
842
+ );
843
+ await this.waitForPushProcessing(resolvedSwapId);
844
+ terminalOutcome = this.terminalOutcomes.get(resolvedSwapId);
845
+ if (terminalOutcome?.storageError) throw terminalOutcome.storageError;
846
+ if (terminalOutcome) resolvedContext = null;
847
+ }
513
848
  if (!options.waitForFinality) {
514
849
  return { kind: "accepted", accepted };
515
850
  }
516
- if (!this.swapId) {
517
- throw new CommandError(
518
- "REJECTED_STALE_JOB",
519
- "refund accepted and pending finalization; check status again shortly"
520
- );
851
+ if (terminalOutcome?.refund !== void 0) {
852
+ return { kind: "executed", refund: terminalOutcome.refund };
521
853
  }
522
- return { kind: "executed", refund: await this.awaitRefundOutcomeFromSwapStatus() };
854
+ if (!resolvedContext) {
855
+ throw new CommandError("REJECTED_STALE_JOB", REFUND_PENDING_FINALIZATION_MESSAGE);
856
+ }
857
+ if (!resolvedContext.syncSecret) {
858
+ throw new CommandError("REJECTED_STALE_JOB", REFUND_PENDING_FINALIZATION_MESSAGE);
859
+ }
860
+ this.setCurrentContext(resolvedContext);
861
+ return {
862
+ kind: "executed",
863
+ refund: await this.awaitRefundOutcomeFromSwapStatus(resolvedContext)
864
+ };
523
865
  } finally {
524
- if (ackTimer) clearTimeout(ackTimer);
525
866
  recoveryCode.fill(0);
526
- if (openedForRefund && this.closeSessionAfterRefund) {
867
+ this.endOperation();
868
+ if (openedForRefund && this.closeSessionAfterOperationFailure && this.activeOperations === 0) {
869
+ await this.waitForPushProcessing(requestSwapId);
527
870
  this.close();
528
871
  }
529
872
  }
530
873
  }
531
- async awaitRefundOutcomeFromSwapStatus() {
874
+ ensureRefundContext(swapId) {
875
+ if (swapId === null) return null;
876
+ if (this.terminalOutcomes.has(swapId)) return null;
877
+ const context = this.ensureSwapContext(swapId);
878
+ if (context.swapState === "new") {
879
+ context.swapState = "awaiting_deposit";
880
+ this.setCurrentContext(context);
881
+ }
882
+ return context;
883
+ }
884
+ async awaitRefundOutcomeFromSwapStatus(context) {
532
885
  const deadline = Date.now() + DEFAULT_REFUND_OUTCOME_POLL_BUDGET_MS;
533
886
  for (; ; ) {
534
887
  const perPollTimeout = Math.min(
@@ -536,7 +889,7 @@ var UserCoordinatorSession = class {
536
889
  Math.max(0, deadline - Date.now())
537
890
  );
538
891
  try {
539
- const status = await this.requestSwapStatus(perPollTimeout);
892
+ const status = await this.requestSwapStatusForContext(context, perPollTimeout);
540
893
  if (status.withdraw_execution) {
541
894
  return status.withdraw_execution;
542
895
  }
@@ -546,10 +899,7 @@ var UserCoordinatorSession = class {
546
899
  }
547
900
  }
548
901
  if (Date.now() >= deadline) {
549
- throw new CommandError(
550
- "REJECTED_STALE_JOB",
551
- "refund accepted and pending finalization; check status again shortly"
552
- );
902
+ throw new CommandError("REJECTED_STALE_JOB", REFUND_PENDING_FINALIZATION_MESSAGE);
553
903
  }
554
904
  await delay(REFUND_OUTCOME_POLL_INTERVAL_MS);
555
905
  }
@@ -558,25 +908,29 @@ var UserCoordinatorSession = class {
558
908
  const protocol = this.requireProtocol();
559
909
  const prepared = await this.prepareContract(params);
560
910
  const depositEnv = await prepared.depositConfirmedPromise;
561
- const depositPayload = protocol.asDepositConfirmedPayload(depositEnv);
562
- if (depositPayload.amount_lamports !== params.amountLamports) {
563
- throw new InvalidDepositAmountError(
564
- BigInt(depositPayload.amount_lamports),
565
- BigInt(params.amountLamports)
566
- );
567
- }
911
+ const depositPayload = assertDepositConfirmedAmount(
912
+ protocol,
913
+ depositEnv,
914
+ params.amountLamports
915
+ );
568
916
  this.log(
569
917
  `deposit confirmed tx=${depositPayload.tx_signature} amount=${depositPayload.amount_lamports}`,
570
918
  "in"
571
919
  );
572
920
  await params.onDepositConfirmed?.({ originAddress: depositPayload.origin_address });
573
- await this.requestSwapStatus();
921
+ const context = this.swapContexts.get(prepared.swapId);
922
+ if (!context) {
923
+ throw new CommandError("REJECTED_STATE", `missing client context for ${prepared.swapId}`);
924
+ }
925
+ await this.requestSwapStatusForContext(context);
574
926
  return {
575
927
  swapId: prepared.swapId,
576
928
  jointPublicKey: prepared.jointPublicKey,
577
929
  depositTxSignature: depositPayload.tx_signature,
578
930
  depositObservedAtMs: depositPayload.observed_at_ms,
579
931
  delegatedAtMs: prepared.delegatedAtMs,
932
+ resumeSecret: new Uint8Array(prepared.resumeSecret),
933
+ resumeSecretHex: prepared.resumeSecretHex,
580
934
  recoveryCodeHex: prepared.recoveryCodeHex,
581
935
  policySnapshot: prepared.policySnapshot,
582
936
  delegatePolicySnapshot: prepared.delegatePolicySnapshot
@@ -587,6 +941,7 @@ var UserCoordinatorSession = class {
587
941
  const requestedPayoutWindowMs = params.payoutWindowMs ?? payoutWindowMsFromSpec(params.payoutSpec);
588
942
  const payoutDeadlineMs = requestedPayoutWindowMs !== void 0 ? Date.now() + requestedPayoutWindowMs : void 0;
589
943
  let generatedSyncSecret = null;
944
+ let generatedResumeSecret = null;
590
945
  const syncSecret = params.syncSecret ? new Uint8Array(params.syncSecret) : (() => {
591
946
  generatedSyncSecret = randomSecret32();
592
947
  return generatedSyncSecret;
@@ -597,105 +952,193 @@ var UserCoordinatorSession = class {
597
952
  `syncSecret must be ${SYNC_SECRET_BYTE_LENGTH} bytes`
598
953
  );
599
954
  }
600
- this.syncSecret = new Uint8Array(syncSecret);
601
955
  const syncSecretHex = bytesToHex(syncSecret);
602
956
  const syncCommitment = await sha256Commitment(syncSecret);
603
957
  if (generatedSyncSecret) {
604
958
  await params.onSyncSecretReady?.({ secretHex: syncSecretHex });
605
959
  }
606
- const swapResp = await this.request(CoordinatorMessageType.ContractRequest, {
607
- actor: "user",
608
- amount_lamports: params.amountLamports,
609
- payout_spec: params.payoutSpec,
610
- sync_commitment: syncCommitment,
611
- ...(params.payoutDeadlineMs !== void 0 || payoutDeadlineMs !== void 0) && {
612
- payout_deadline_ms: params.payoutDeadlineMs ?? payoutDeadlineMs
613
- }
614
- });
615
- const swapPayload = protocol.asSwapRequestResponsePayload(swapResp);
616
- const swapId = swapPayload.swap_id;
617
- this.swapId = swapId;
618
- params.onPolicySnapshot?.(swapPayload.policy_snapshot);
619
- this.log(`swap accepted - id=${swapId}`, "in");
620
- this.bufferDepositConfirmedUntilDelegateAck = true;
621
- this.bufferedDepositConfirmed = null;
622
- const depositConfirmedPromise = this.awaitPush(CoordinatorMessageType.DepositConfirmed, null);
623
- void depositConfirmedPromise.catch(() => void 0);
624
- const { jointPublicKey, keyHandle, depositExpiresAtMs, coordinatorTimestampMs } = await this.runDkg();
625
- let recoveryCommitment = params.recoveryCommitment;
626
- let recoveryCodeHex;
627
- if (!recoveryCommitment) {
628
- const generated = await generateRecoveryCode();
629
- recoveryCommitment = generated.commitment;
630
- try {
631
- recoveryCodeHex = recoveryCodeToHex(generated.code);
632
- await params.onRecoveryCodeReady?.({ codeHex: recoveryCodeHex });
633
- } finally {
634
- generated.code.fill(0);
635
- }
636
- }
637
- const delegateAckPromise = this.awaitPush(CoordinatorMessageType.DelegateAck);
638
- const share = params.delegateShare ?? keyHandle.secretShareBytes();
639
- let delegateResp;
640
- try {
641
- delegateResp = await this.request(CoordinatorMessageType.DelegateShare, {
642
- share,
643
- recovery_commitment: recoveryCommitment
644
- });
645
- } catch (err) {
646
- void delegateAckPromise.catch(() => void 0);
647
- throw err;
648
- }
649
- const delegatePayload = protocol.asDelegateShareResponsePayload(delegateResp);
650
- const delegateAck = protocol.asDelegateAckPayload(await delegateAckPromise);
651
- if (delegateAck.delegated_at_ms !== delegatePayload.delegated_at_ms) {
960
+ const resumeSecret = params.resumeSecret ? new Uint8Array(params.resumeSecret) : (() => {
961
+ generatedResumeSecret = randomSecret32();
962
+ return generatedResumeSecret;
963
+ })();
964
+ if (resumeSecret.length !== RESUME_SECRET_BYTE_LENGTH) {
652
965
  throw new CommandError(
653
966
  "REJECTED_GUARD",
654
- "DelegateAck delegated_at_ms does not match DelegateShare response"
967
+ `resumeSecret must be ${RESUME_SECRET_BYTE_LENGTH} bytes`
655
968
  );
656
969
  }
657
- if (!spendingPoliciesEqual(delegateAck.spending_policy, swapPayload.policy_snapshot)) {
658
- throw new CommandError(
659
- "REJECTED_GUARD",
660
- "DelegateAck spending_policy does not match ContractRequest policy_snapshot"
661
- );
970
+ const resumeSecretHex = bytesToHex(resumeSecret);
971
+ const resumeCommitment = await sha256Commitment(resumeSecret);
972
+ if (generatedResumeSecret) {
973
+ await params.onResumeSecretReady?.({ secretHex: resumeSecretHex });
662
974
  }
663
- this.log(`delegated at ${delegateAck.delegated_at_ms}`, "in");
664
- this.releaseDepositConfirmedGate();
665
- await params.onDepositAddressReady?.({
975
+ const swapResp = await this.request(
976
+ CoordinatorMessageType.ContractRequest,
977
+ {
978
+ actor: "user",
979
+ amount_lamports: params.amountLamports,
980
+ payout_spec: params.payoutSpec,
981
+ sync_commitment: syncCommitment,
982
+ resume_commitment: resumeCommitment,
983
+ ...(params.payoutDeadlineMs !== void 0 || payoutDeadlineMs !== void 0) && {
984
+ payout_deadline_ms: params.payoutDeadlineMs ?? payoutDeadlineMs
985
+ }
986
+ },
987
+ { swapId: null, requireNonNullResponseSwapId: true }
988
+ );
989
+ const swapPayload = protocol.asSwapRequestResponsePayload(swapResp);
990
+ const swapId = swapPayload.swap_id;
991
+ const context = {
666
992
  swapId,
667
- jointPublicKey,
668
- amountLamports: params.amountLamports,
669
- depositExpiresAtMs,
670
- coordinatorTimestampMs
671
- });
672
- return {
673
- requestId: swapId,
993
+ requestedAmountLamports: params.amountLamports,
994
+ swapState: "awaiting_dkg",
995
+ syncSecret: new Uint8Array(syncSecret),
996
+ resumeSecret: new Uint8Array(resumeSecret),
997
+ bufferDepositConfirmedUntilDelegateAck: true,
998
+ bufferedDepositConfirmed: null
999
+ };
1000
+ this.swapContexts.set(swapId, context);
1001
+ this.setCurrentContext(context);
1002
+ await persistTransferAllocation(this.clientPersistenceState(), {
674
1003
  swapId,
675
1004
  amountLamports: params.amountLamports,
676
- jointPublicKey,
677
- depositExpiresAtMs,
678
- coordinatorTimestampMs,
679
- delegatedAtMs: delegateAck.delegated_at_ms,
1005
+ payoutSpec: params.payoutSpec,
1006
+ policySnapshot: swapPayload.policy_snapshot,
680
1007
  syncSecret,
1008
+ coordinatorTimestampMs: swapResp.timestamp_ms
1009
+ });
1010
+ await params.onSwapAllocated?.({
1011
+ swapId,
681
1012
  syncSecretHex,
682
- recoveryCodeHex,
683
- policySnapshot: swapPayload.policy_snapshot,
684
- delegatePolicySnapshot: delegateAck.spending_policy,
685
- depositConfirmedPromise
686
- };
1013
+ resumeSecretHex
1014
+ });
1015
+ params.onPolicySnapshot?.(swapPayload.policy_snapshot);
1016
+ this.log(`swap accepted - id=${swapId}`, "in");
1017
+ let preparedSuccessfully = false;
1018
+ try {
1019
+ const depositConfirmedPromise = this.awaitPush(
1020
+ CoordinatorMessageType.DepositConfirmed,
1021
+ context.swapId,
1022
+ null
1023
+ );
1024
+ void depositConfirmedPromise.catch(() => void 0);
1025
+ const { jointPublicKey, keyHandle, depositExpiresAtMs, coordinatorTimestampMs } = await this.runDkg(context, params.delegateShare ? void 0 : params.onDelegateShareReady);
1026
+ this.updateContextState(context, "awaiting_deposit");
1027
+ let recoveryCommitment = params.recoveryCommitment;
1028
+ let recoveryCodeHex;
1029
+ if (!recoveryCommitment) {
1030
+ const generated = await generateRecoveryCode();
1031
+ recoveryCommitment = generated.commitment;
1032
+ try {
1033
+ recoveryCodeHex = recoveryCodeToHex(generated.code);
1034
+ await persistTransferRecoveryCode(
1035
+ this.clientPersistenceState(),
1036
+ context.swapId,
1037
+ generated.code
1038
+ );
1039
+ await params.onRecoveryCodeReady?.({ codeHex: recoveryCodeHex });
1040
+ } finally {
1041
+ generated.code.fill(0);
1042
+ }
1043
+ }
1044
+ const delegateAckPromise = this.awaitPush(CoordinatorMessageType.DelegateAck, context.swapId);
1045
+ const share = params.delegateShare ?? keyHandle.secretShareBytes();
1046
+ let delegateResp;
1047
+ try {
1048
+ delegateResp = await this.request(
1049
+ CoordinatorMessageType.DelegateShare,
1050
+ {
1051
+ share,
1052
+ recovery_commitment: recoveryCommitment,
1053
+ resume_secret: context.resumeSecret
1054
+ },
1055
+ { swapId: context.swapId }
1056
+ );
1057
+ } catch (err) {
1058
+ void delegateAckPromise.catch(() => void 0);
1059
+ throw err;
1060
+ }
1061
+ const delegatePayload = protocol.asDelegateShareResponsePayload(delegateResp);
1062
+ const delegateAck = protocol.asDelegateAckPayload(await delegateAckPromise);
1063
+ if (delegateAck.delegated_at_ms !== delegatePayload.delegated_at_ms) {
1064
+ throw new CommandError(
1065
+ "REJECTED_GUARD",
1066
+ "DelegateAck delegated_at_ms does not match DelegateShare response"
1067
+ );
1068
+ }
1069
+ if (!spendingPoliciesEqual(delegateAck.spending_policy, swapPayload.policy_snapshot)) {
1070
+ throw new CommandError(
1071
+ "REJECTED_GUARD",
1072
+ "DelegateAck spending_policy does not match ContractRequest policy_snapshot"
1073
+ );
1074
+ }
1075
+ if (context.bufferedDepositConfirmed) {
1076
+ assertDepositConfirmedAmount(
1077
+ this.requireProtocol(),
1078
+ context.bufferedDepositConfirmed,
1079
+ params.amountLamports
1080
+ );
1081
+ }
1082
+ await persistTransferReady(this.clientPersistenceState(), {
1083
+ swapId,
1084
+ depositAddress: solanaAddressFromPublicKeyBytes(jointPublicKey),
1085
+ depositExpiresAtMs,
1086
+ delegatedAtMs: delegateAck.delegated_at_ms,
1087
+ coordinatorTimestampMs
1088
+ });
1089
+ this.log(`delegated at ${delegateAck.delegated_at_ms}`, "in");
1090
+ await this.releaseDepositConfirmedGate(context);
1091
+ await params.onDepositAddressReady?.({
1092
+ swapId,
1093
+ jointPublicKey,
1094
+ amountLamports: params.amountLamports,
1095
+ depositExpiresAtMs,
1096
+ coordinatorTimestampMs
1097
+ });
1098
+ preparedSuccessfully = true;
1099
+ return {
1100
+ requestId: swapId,
1101
+ swapId,
1102
+ amountLamports: params.amountLamports,
1103
+ jointPublicKey,
1104
+ depositExpiresAtMs,
1105
+ coordinatorTimestampMs,
1106
+ delegatedAtMs: delegateAck.delegated_at_ms,
1107
+ syncSecret,
1108
+ syncSecretHex,
1109
+ resumeSecret,
1110
+ resumeSecretHex,
1111
+ recoveryCodeHex,
1112
+ policySnapshot: swapPayload.policy_snapshot,
1113
+ delegatePolicySnapshot: delegateAck.spending_policy,
1114
+ depositConfirmedPromise
1115
+ };
1116
+ } finally {
1117
+ if (!preparedSuccessfully) {
1118
+ this.cancelPushWaitersForSwap(context.swapId);
1119
+ }
1120
+ }
687
1121
  }
688
- async runDkg() {
1122
+ async runDkg(context, onDelegateShareReady, persistDkgMetadata = true) {
689
1123
  await ensureFrostWasm();
690
1124
  const protocol = this.requireProtocol();
691
1125
  const { dkgRound1 } = await loadFrost();
692
- await this.request(CoordinatorMessageType.DkgInit, {});
1126
+ await this.request(
1127
+ CoordinatorMessageType.DkgInit,
1128
+ { resume_secret: context.resumeSecret },
1129
+ { swapId: context.swapId }
1130
+ );
693
1131
  this.log("DKG initialised", "info");
694
1132
  const aliceRound1State = dkgRound1(ALICE_ID);
695
1133
  const aliceRound1Bytes = aliceRound1State.round1PackageBytes();
696
- const round1Resp = await this.request(CoordinatorMessageType.DkgRound1, {
697
- package: aliceRound1Bytes
698
- });
1134
+ const round1Resp = await this.request(
1135
+ CoordinatorMessageType.DkgRound1,
1136
+ {
1137
+ package: aliceRound1Bytes,
1138
+ resume_secret: context.resumeSecret
1139
+ },
1140
+ { swapId: context.swapId }
1141
+ );
699
1142
  const round1Payload = protocol.asDkgRound1ResponsePayload(round1Resp);
700
1143
  this.log(`DKG round1 complete - coord pkg ${round1Payload.package.byteLength}B`, "in");
701
1144
  const aliceRound2State = aliceRound1State.intoRound2(COORDINATOR_ID, round1Payload.package);
@@ -710,10 +1153,20 @@ var UserCoordinatorSession = class {
710
1153
  } catch (err) {
711
1154
  throw new CommandError("REJECTED_GUARD", `Alice DKG finalize failed: ${normalizeError(err)}`);
712
1155
  }
713
- const dkgCompletePromise = this.awaitPush(CoordinatorMessageType.DkgComplete);
714
- await this.request(CoordinatorMessageType.DkgRound2, {
715
- package: aliceRound2Bytes
1156
+ await onDelegateShareReady?.({
1157
+ swapId: context.swapId,
1158
+ delegateShare: new Uint8Array(aliceKeyHandle.secretShareBytes())
716
1159
  });
1160
+ const dkgCompletePromise = this.awaitPush(CoordinatorMessageType.DkgComplete, context.swapId);
1161
+ void dkgCompletePromise.catch(() => void 0);
1162
+ await this.request(
1163
+ CoordinatorMessageType.DkgRound2,
1164
+ {
1165
+ package: aliceRound2Bytes,
1166
+ resume_secret: context.resumeSecret
1167
+ },
1168
+ { swapId: context.swapId }
1169
+ );
717
1170
  const completeEnvelope = await dkgCompletePromise;
718
1171
  const complete = readDkgComplete(protocol, completeEnvelope);
719
1172
  const alicePubkey = aliceKeyHandle.groupPublicKeyBytes();
@@ -723,6 +1176,14 @@ var UserCoordinatorSession = class {
723
1176
  "DKG joint public key mismatch between Alice and coordinator"
724
1177
  );
725
1178
  }
1179
+ if (persistDkgMetadata) {
1180
+ await persistTransferDkgMetadata(this.clientPersistenceState(), {
1181
+ swapId: context.swapId,
1182
+ depositAddress: solanaAddressFromPublicKeyBytes(alicePubkey),
1183
+ depositExpiresAtMs: complete.depositExpiresAtMs,
1184
+ coordinatorTimestampMs: complete.coordinatorTimestampMs
1185
+ });
1186
+ }
726
1187
  const pubkeyHexPrefix = Array.from(alicePubkey.slice(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join("");
727
1188
  this.log(`DKG complete - joint pubkey ${pubkeyHexPrefix}`, "info");
728
1189
  return {
@@ -733,6 +1194,20 @@ var UserCoordinatorSession = class {
733
1194
  };
734
1195
  }
735
1196
  async openSession() {
1197
+ if (this.hasUsableSession()) return;
1198
+ if (this.openingSession !== null) {
1199
+ await this.openingSession;
1200
+ return;
1201
+ }
1202
+ const opening = this.openSessionOnce();
1203
+ this.openingSession = opening;
1204
+ try {
1205
+ await opening;
1206
+ } finally {
1207
+ if (this.openingSession === opening) this.openingSession = null;
1208
+ }
1209
+ }
1210
+ async openSessionOnce() {
736
1211
  if (this.hasUsableSession()) return;
737
1212
  if (this.session !== null) {
738
1213
  this.detachSession(
@@ -758,11 +1233,6 @@ var UserCoordinatorSession = class {
758
1233
  this.log(`connected to ${this.coordinator.endpoints[0]?.wsUrl ?? "coordinator"}`, "info");
759
1234
  this.log("attestation verified", "info");
760
1235
  }
761
- assertNoActiveContract() {
762
- if (this.swapId !== null || this.sessionStatus === "connecting" || this.sessionStatus === "running") {
763
- throw new CommandError("REJECTED_STATE", ACTIVE_CONTRACT_REQUEST_MESSAGE);
764
- }
765
- }
766
1236
  hasUsableSession() {
767
1237
  if (this.session === null) return false;
768
1238
  const state = getSessionState(this.session);
@@ -788,61 +1258,213 @@ var UserCoordinatorSession = class {
788
1258
  handleEnvelope(env) {
789
1259
  const protocol = this.requireProtocol();
790
1260
  this.log(`<- ${env.type} kind=${env.kind}`, "in");
791
- const payoutExecuted = env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.PayoutExecuted ? protocol.asPayoutExecutedPayload(env) : null;
792
- const payoutMatchesCurrentSwap = payoutExecuted !== null && (env.swap_id === null || env.swap_id === this.swapId);
793
- const refundExecuted = env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.WithdrawExecuted ? protocol.asWithdrawExecutedPayload(env) : null;
1261
+ if (env.kind === EnvelopeKind.PUSH) {
1262
+ if (env.swap_id === null) {
1263
+ this.log(`ignored ${env.type} without an explicit swap_id`, "info");
1264
+ return;
1265
+ }
1266
+ if (!this.swapContexts.has(env.swap_id)) {
1267
+ this.log(`ignored ${env.type} for unknown swap ${env.swap_id}`, "info");
1268
+ return;
1269
+ }
1270
+ this.enqueuePush(env);
1271
+ return;
1272
+ }
1273
+ const context = env.swap_id ? this.swapContexts.get(env.swap_id) ?? null : null;
1274
+ if (env.swap_id === null && env.kind !== EnvelopeKind.ERROR) {
1275
+ this.log(`ignored ${env.type} without an explicit swap_id`, "info");
1276
+ return;
1277
+ }
1278
+ if (env.swap_id !== null && context === null) {
1279
+ this.log(`ignored ${env.type} for unknown swap ${env.swap_id}`, "info");
1280
+ return;
1281
+ }
1282
+ const stateForEnvelope = context?.swapState ?? this.swapState;
1283
+ const next = protocol.nextState(stateForEnvelope, env.kind, env.type);
1284
+ if (next !== null) {
1285
+ if (context) {
1286
+ this.updateContextState(context, next);
1287
+ } else {
1288
+ this.swapState = next;
1289
+ this.emitStateChange(this.swapId, next);
1290
+ }
1291
+ }
1292
+ if (env.kind === EnvelopeKind.ERROR) {
1293
+ const coordinatorError = coordinatorErrorFromEnvelope(protocol, env);
1294
+ if (env.req_id === 0) {
1295
+ for (const listener of this.coordinatorErrorListeners) {
1296
+ listener(coordinatorError);
1297
+ }
1298
+ }
1299
+ return;
1300
+ }
1301
+ }
1302
+ enqueuePush(env) {
1303
+ const swapId = env.swap_id;
1304
+ if (swapId === null) return;
1305
+ const context = this.swapContexts.get(swapId);
1306
+ if (context === void 0) return;
1307
+ const persistence = this.clientPersistenceState();
1308
+ void this.enqueueSwapProcessing(
1309
+ swapId,
1310
+ () => this.processPushEnvelope(env, context, persistence)
1311
+ ).catch(() => void 0);
1312
+ }
1313
+ enqueueSwapProcessing(swapId, process) {
1314
+ const previous = this.pushProcessing.get(swapId) ?? Promise.resolve();
1315
+ const processing = previous.catch(() => void 0).then(process);
1316
+ this.pushProcessing.set(swapId, processing);
1317
+ void processing.finally(() => {
1318
+ if (this.pushProcessing.get(swapId) === processing) {
1319
+ this.pushProcessing.delete(swapId);
1320
+ }
1321
+ }).catch(() => void 0);
1322
+ return processing;
1323
+ }
1324
+ async waitForPushProcessing(swapId) {
1325
+ if (swapId === null) return;
1326
+ await (this.pushProcessing.get(swapId)?.catch(() => void 0) ?? Promise.resolve());
1327
+ }
1328
+ async processPushEnvelope(env, context, persistence) {
1329
+ const protocol = this.requireProtocol();
1330
+ const payoutExecuted = env.type === CoordinatorMessageType.PayoutExecuted ? protocol.asPayoutExecutedPayload(env) : null;
1331
+ const refundExecuted = env.type === CoordinatorMessageType.WithdrawExecuted ? protocol.asWithdrawExecutedPayload(env) : null;
794
1332
  if (this.shouldBufferDepositConfirmed(env)) {
795
1333
  this.bufferDepositConfirmed(env);
796
1334
  return;
797
1335
  }
798
- const foreignPayout = payoutExecuted !== null && !payoutMatchesCurrentSwap;
799
- const next = foreignPayout ? null : protocol.nextState(
800
- this.swapState,
1336
+ if (env.type === CoordinatorMessageType.DkgComplete) {
1337
+ this.settlePushListener(env);
1338
+ return;
1339
+ }
1340
+ if (env.type === CoordinatorMessageType.DepositConfirmed) {
1341
+ if (context.requestedAmountLamports === null) {
1342
+ this.log(
1343
+ `ignored DepositConfirmed for swap ${context.swapId} without requested amount`,
1344
+ "info"
1345
+ );
1346
+ return;
1347
+ }
1348
+ try {
1349
+ assertDepositConfirmedAmount(protocol, env, context.requestedAmountLamports);
1350
+ } catch (error) {
1351
+ this.settlePushListener(env, error instanceof Error ? error : new Error(String(error)));
1352
+ return;
1353
+ }
1354
+ }
1355
+ const next = protocol.nextState(
1356
+ context.swapState,
801
1357
  env.kind,
802
1358
  env.type,
803
1359
  payoutExecuted ? { payoutFinal: payoutExecuted.final } : void 0
804
1360
  );
805
- if (env.kind === EnvelopeKind.PUSH && next === null) {
1361
+ if (next === null) {
806
1362
  this.log(
807
- 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}`,
1363
+ `ignored ${env.type} for swap ${env.swap_id ?? "null"} while client context is ${context.swapId} in state ${context.swapState}`,
808
1364
  "info"
809
1365
  );
810
1366
  return;
811
1367
  }
812
- if (next !== null) {
813
- this.swapState = next;
814
- this.emitStateChange();
815
- }
816
- if (env.kind === EnvelopeKind.ERROR) {
817
- const coordinatorError = coordinatorErrorFromEnvelope(protocol, env);
818
- if (env.req_id === 0) {
819
- for (const listener of this.coordinatorErrorListeners) {
820
- listener(coordinatorError);
821
- }
1368
+ if (env.type === CoordinatorMessageType.DelegateAck) {
1369
+ if (!this.isActiveSwapContext(context)) {
1370
+ context.swapState = next;
1371
+ return;
822
1372
  }
1373
+ this.updateContextState(context, next);
1374
+ this.settlePushListener(env);
823
1375
  return;
824
1376
  }
825
- if (env.kind === EnvelopeKind.PUSH) {
826
- if (payoutExecuted && payoutMatchesCurrentSwap) {
827
- this.emitPayoutExecuted(payoutExecuted, env.swap_id ?? this.swapId);
1377
+ if (isTerminalSwapState(next)) {
1378
+ await this.handleTerminalPush(
1379
+ env,
1380
+ context,
1381
+ next,
1382
+ payoutExecuted,
1383
+ refundExecuted,
1384
+ persistence
1385
+ );
1386
+ return;
1387
+ }
1388
+ await this.handleNonTerminalPush(env, context, next, payoutExecuted, persistence);
1389
+ }
1390
+ async handleNonTerminalPush(env, context, next, payoutExecuted, persistence) {
1391
+ if (!this.isActiveSwapContext(context)) {
1392
+ context.swapState = next;
1393
+ return;
1394
+ }
1395
+ let storageError = null;
1396
+ try {
1397
+ await persistTransferStatus(persistence, context.swapId, { state: next }, env.timestamp_ms);
1398
+ } catch (error) {
1399
+ storageError = storageErrorFromPush(error, context.swapId, null);
1400
+ }
1401
+ if (!this.isActiveSwapContext(context)) {
1402
+ context.swapState = next;
1403
+ return;
1404
+ }
1405
+ try {
1406
+ this.updateContextState(context, next);
1407
+ if (payoutExecuted) this.emitPayoutExecuted(payoutExecuted, context.swapId);
1408
+ } finally {
1409
+ try {
1410
+ if (storageError) this.emitStorageError(storageError);
1411
+ } finally {
1412
+ this.settlePushListener(env);
828
1413
  }
829
- if (refundExecuted) {
830
- this.emitRefundExecuted(refundExecuted, env.swap_id ?? this.swapId);
1414
+ }
1415
+ }
1416
+ async handleTerminalPush(env, context, next, payoutExecuted, refundExecuted, persistence) {
1417
+ this.rememberTerminalOutcome(context.swapId, next, refundExecuted);
1418
+ let storageError = null;
1419
+ if (persistence.enabled) {
1420
+ try {
1421
+ await persistTransferTerminal(
1422
+ persistence,
1423
+ context.swapId,
1424
+ next,
1425
+ env.timestamp_ms,
1426
+ refundExecuted ? "refund" : "sync"
1427
+ );
1428
+ } catch (error) {
1429
+ storageError = storageErrorFromPush(error, context.swapId, refundExecuted);
1430
+ }
1431
+ }
1432
+ if (storageError) {
1433
+ this.rememberTerminalOutcome(context.swapId, next, refundExecuted, storageError);
1434
+ }
1435
+ if (!this.isActiveSwapContext(context)) {
1436
+ context.swapState = next;
1437
+ return;
1438
+ }
1439
+ try {
1440
+ this.updateContextState(context, next);
1441
+ if (payoutExecuted) this.emitPayoutExecuted(payoutExecuted, context.swapId);
1442
+ if (refundExecuted) this.emitRefundExecuted(refundExecuted, context.swapId);
1443
+ } finally {
1444
+ try {
1445
+ if (storageError) this.emitStorageError(storageError);
1446
+ } finally {
1447
+ this.settlePushListener(env);
831
1448
  }
832
- this.resolvePushListener(env);
833
1449
  }
834
1450
  }
835
- applySwapStatus(status, coordinatorTimestampMs) {
836
- const keepTerminalState = TERMINAL_SWAP_STATES.has(this.swapState) && status.state !== this.swapState;
837
- if (!keepTerminalState && this.swapState !== status.state) {
838
- this.swapState = status.state;
839
- this.emitStateChange();
1451
+ emitStorageError(error) {
1452
+ for (const listener of this.storageErrorListeners) listener(error);
1453
+ this.log(`client persistence failed: ${normalizeError(error)}`, "info");
1454
+ }
1455
+ applySwapStatus(context, status, coordinatorTimestampMs) {
1456
+ const keepTerminalState = TERMINAL_SWAP_STATES.has(context.swapState) && status.state !== context.swapState;
1457
+ if (!keepTerminalState && isTerminalSwapState(status.state)) {
1458
+ this.rememberTerminalOutcome(context.swapId, status.state, status.withdraw_execution);
1459
+ }
1460
+ if (!keepTerminalState && context.swapState !== status.state) {
1461
+ this.updateContextState(context, status.state);
840
1462
  }
841
1463
  for (const fragment of status.fragments_executed) {
842
- this.emitPayoutExecuted(fragment, this.swapId);
1464
+ this.emitPayoutExecuted(fragment, context.swapId);
843
1465
  }
844
1466
  if (status.withdraw_execution) {
845
- this.emitRefundExecuted(status.withdraw_execution, this.swapId);
1467
+ this.emitRefundExecuted(status.withdraw_execution, context.swapId);
846
1468
  }
847
1469
  if (status.actor_sync !== void 0) {
848
1470
  for (const listener of this.normalUserActorSyncListeners) {
@@ -853,8 +1475,7 @@ var UserCoordinatorSession = class {
853
1475
  emitPayoutExecuted(payload, swapId) {
854
1476
  if (!swapId) return;
855
1477
  const key = `${swapId}:${payload.index}:${payload.tx_signature}`;
856
- if (this.emittedPayoutKeys.has(key)) return;
857
- this.emittedPayoutKeys.add(key);
1478
+ if (!this.rememberEventKey(this.emittedPayoutKeys, key)) return;
858
1479
  this.log(
859
1480
  `payout executed tx=${payload.tx_signature} amount=${payload.amount_lamports} final=${payload.final}`,
860
1481
  "in"
@@ -866,88 +1487,150 @@ var UserCoordinatorSession = class {
866
1487
  emitRefundExecuted(payload, swapId) {
867
1488
  if (!swapId) return;
868
1489
  const key = `${swapId}:${payload.tx_signature}`;
869
- if (this.emittedRefundKeys.has(key)) return;
870
- this.emittedRefundKeys.add(key);
1490
+ if (!this.rememberEventKey(this.emittedRefundKeys, key)) return;
871
1491
  this.log(`refund executed tx=${payload.tx_signature} amount=${payload.amount_lamports}`, "in");
872
1492
  for (const listener of this.refundExecutedListeners) {
873
1493
  listener(payload, swapId);
874
1494
  }
875
1495
  }
876
- request(type, payload, timeoutMs) {
1496
+ rememberEventKey(keys, key) {
1497
+ if (keys.has(key)) return false;
1498
+ keys.add(key);
1499
+ if (keys.size > MAX_EVENT_DEDUP_ENTRIES_PER_KIND) {
1500
+ const oldest = keys.values().next().value;
1501
+ if (oldest !== void 0) keys.delete(oldest);
1502
+ }
1503
+ return true;
1504
+ }
1505
+ request(type, payload, options = {}) {
877
1506
  if (this.session === null) {
878
1507
  throw new TransportError("CONNECTION_LOST", "coordinator session is not connected");
879
1508
  }
880
1509
  return requestEnvelope(this.session, type, payload, {
881
- swapId: this.swapId,
882
- timeoutMs,
1510
+ swapId: options.swapId === void 0 ? this.swapId : options.swapId,
1511
+ timeoutMs: options.timeoutMs,
1512
+ rejectMatchingOutOfBandErrors: options.rejectMatchingOutOfBandErrors,
1513
+ requireNonNullResponseSwapId: options.requireNonNullResponseSwapId,
883
1514
  onLog: (message, direction) => this.log(message, direction)
884
1515
  });
885
1516
  }
886
- awaitPush(type, timeoutMs = 3e4) {
1517
+ awaitPush(type, swapId, timeoutMs = 3e4) {
887
1518
  return new Promise((resolve, reject) => {
888
1519
  const timer = timeoutMs === null ? null : setTimeout(() => {
889
- this.pushListeners.delete(type);
1520
+ this.removePushListener(type, listener);
890
1521
  reject(new CommandError("REJECTED_STALE_JOB", `timed out waiting for ${type} push`));
891
1522
  }, timeoutMs);
892
- this.pushListeners.set(type, { resolve, reject, timer });
1523
+ const listener = { swapId, resolve, reject, timer };
1524
+ const listeners = this.pushListeners.get(type) ?? [];
1525
+ listeners.push(listener);
1526
+ this.pushListeners.set(type, listeners);
893
1527
  });
894
1528
  }
895
1529
  rejectAllPushListeners(err) {
896
- for (const listener of this.pushListeners.values()) {
897
- if (listener.timer) clearTimeout(listener.timer);
898
- listener.reject(err);
1530
+ for (const listeners of this.pushListeners.values()) {
1531
+ for (const listener of listeners) {
1532
+ if (listener.timer) clearTimeout(listener.timer);
1533
+ listener.reject(err);
1534
+ }
899
1535
  }
900
1536
  this.pushListeners.clear();
901
- this.bufferDepositConfirmedUntilDelegateAck = false;
902
- this.bufferedDepositConfirmed = null;
1537
+ for (const context of this.swapContexts.values()) {
1538
+ context.bufferDepositConfirmedUntilDelegateAck = false;
1539
+ context.bufferedDepositConfirmed = null;
1540
+ }
903
1541
  }
904
1542
  shouldBufferDepositConfirmed(env) {
905
- return env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.DepositConfirmed && this.bufferDepositConfirmedUntilDelegateAck;
1543
+ const context = env.swap_id ? this.swapContexts.get(env.swap_id) : null;
1544
+ return env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.DepositConfirmed && context?.bufferDepositConfirmedUntilDelegateAck === true;
906
1545
  }
907
1546
  bufferDepositConfirmed(env) {
908
- if (!this.bufferedDepositConfirmed) {
909
- this.bufferedDepositConfirmed = env;
910
- this.clearPushListenerTimer(CoordinatorMessageType.DepositConfirmed);
1547
+ const context = env.swap_id ? this.swapContexts.get(env.swap_id) : null;
1548
+ if (!context) return;
1549
+ if (!context.bufferedDepositConfirmed) {
1550
+ context.bufferedDepositConfirmed = env;
1551
+ this.clearPushListenerTimer(CoordinatorMessageType.DepositConfirmed, context.swapId);
911
1552
  this.log("buffered DepositConfirmed until DelegateAck is verified", "info");
912
1553
  return;
913
1554
  }
914
1555
  this.log("ignored duplicate DepositConfirmed while waiting for DelegateAck", "info");
915
1556
  }
916
- releaseDepositConfirmedGate() {
1557
+ async releaseDepositConfirmedGate(context) {
917
1558
  const protocol = this.requireProtocol();
918
- this.bufferDepositConfirmedUntilDelegateAck = false;
919
- const buffered = this.bufferedDepositConfirmed;
1559
+ context.bufferDepositConfirmedUntilDelegateAck = false;
1560
+ const buffered = context.bufferedDepositConfirmed;
920
1561
  if (!buffered) return;
921
- this.bufferedDepositConfirmed = null;
922
- const next = protocol.nextState(this.swapState, buffered.kind, buffered.type);
1562
+ context.bufferedDepositConfirmed = null;
1563
+ const next = protocol.nextState(context.swapState, buffered.kind, buffered.type);
923
1564
  if (next === null) {
924
- this.log(`ignored buffered DepositConfirmed in state ${this.swapState}`, "info");
1565
+ this.log(`ignored buffered DepositConfirmed in state ${context.swapState}`, "info");
925
1566
  return;
926
1567
  }
927
- this.swapState = next;
928
- this.emitStateChange();
929
- this.resolvePushListener(buffered);
930
- }
931
- resolvePushListener(env) {
932
- const listener = this.pushListeners.get(env.type);
933
- if (!listener) return;
934
- this.pushListeners.delete(env.type);
1568
+ let storageError = null;
1569
+ try {
1570
+ await persistTransferStatus(
1571
+ this.clientPersistenceState(),
1572
+ context.swapId,
1573
+ { state: next },
1574
+ buffered.timestamp_ms
1575
+ );
1576
+ } catch (error) {
1577
+ storageError = storageErrorFromPush(error, context.swapId, null);
1578
+ }
1579
+ if (storageError) {
1580
+ this.emitStorageError(storageError);
1581
+ throw storageError;
1582
+ }
1583
+ if (!this.isActiveSwapContext(context)) return;
1584
+ this.updateContextState(context, next);
1585
+ this.settlePushListener(buffered);
1586
+ }
1587
+ settlePushListener(env, error) {
1588
+ const listeners = this.pushListeners.get(env.type);
1589
+ if (!listeners) return;
1590
+ const index = listeners.findIndex(
1591
+ (candidate) => candidate.swapId === void 0 || candidate.swapId === env.swap_id
1592
+ );
1593
+ if (index < 0) return;
1594
+ const listener = listeners.splice(index, 1)[0];
1595
+ if (listeners.length === 0) this.pushListeners.delete(env.type);
935
1596
  if (listener.timer) clearTimeout(listener.timer);
936
- listener.resolve(env);
1597
+ if (error) listener.reject(error);
1598
+ else listener.resolve(env);
937
1599
  }
938
- clearPushListenerTimer(type) {
939
- const listener = this.pushListeners.get(type);
1600
+ clearPushListenerTimer(type, swapId) {
1601
+ const listener = this.pushListeners.get(type)?.find((candidate) => candidate.swapId === void 0 || candidate.swapId === swapId);
940
1602
  if (!listener?.timer) return;
941
1603
  clearTimeout(listener.timer);
942
1604
  listener.timer = null;
943
1605
  }
1606
+ removePushListener(type, listener) {
1607
+ const listeners = this.pushListeners.get(type);
1608
+ if (!listeners) return;
1609
+ const index = listeners.indexOf(listener);
1610
+ if (index >= 0) listeners.splice(index, 1);
1611
+ if (listeners.length === 0) this.pushListeners.delete(type);
1612
+ }
1613
+ cancelPushWaitersForSwap(swapId) {
1614
+ for (const [type, listeners] of this.pushListeners.entries()) {
1615
+ const remaining = listeners.filter((listener) => {
1616
+ if (listener.swapId !== swapId) return true;
1617
+ if (listener.timer) clearTimeout(listener.timer);
1618
+ return false;
1619
+ });
1620
+ if (remaining.length === 0) {
1621
+ this.pushListeners.delete(type);
1622
+ } else if (remaining.length !== listeners.length) {
1623
+ this.pushListeners.set(type, remaining);
1624
+ }
1625
+ }
1626
+ }
944
1627
  setSessionStatus(status) {
945
1628
  this.sessionStatus = status;
946
1629
  this.emitStateChange();
947
1630
  }
948
- emitStateChange() {
1631
+ emitStateChange(swapId = this.swapId, swapState = this.swapState) {
949
1632
  for (const listener of this.stateListeners) {
950
- listener(this.swapState, this.sessionStatus);
1633
+ listener(swapState, this.sessionStatus, swapId);
951
1634
  }
952
1635
  }
953
1636
  log(msg, direction) {
@@ -983,28 +1666,42 @@ function toContractRequestResult(prepared) {
983
1666
  delegatedAtMs: prepared.delegatedAtMs,
984
1667
  syncSecret: new Uint8Array(prepared.syncSecret),
985
1668
  syncSecretHex: prepared.syncSecretHex,
1669
+ resumeSecret: new Uint8Array(prepared.resumeSecret),
1670
+ resumeSecretHex: prepared.resumeSecretHex,
986
1671
  recoveryCodeHex: prepared.recoveryCodeHex,
987
1672
  policySnapshot: prepared.policySnapshot,
988
1673
  delegatePolicySnapshot: prepared.delegatePolicySnapshot
989
1674
  };
990
1675
  }
991
1676
  async function contractRequestOnSession(session, params) {
992
- if (activeFunctionalContractRequests.has(session)) {
993
- throw new CommandError("REJECTED_STATE", ACTIVE_CONTRACT_REQUEST_MESSAGE);
994
- }
995
- activeFunctionalContractRequests.add(session);
996
1677
  let client = null;
997
1678
  try {
998
- client = new UserCoordinatorSession({
999
- coordinator: getSessionState(session).pool,
1000
- createSession: async () => session
1001
- });
1679
+ client = new UserCoordinatorSession(
1680
+ {
1681
+ coordinator: getSessionState(session).pool,
1682
+ createSession: async () => session
1683
+ },
1684
+ false
1685
+ );
1002
1686
  return await client.contractRequest(params);
1003
1687
  } finally {
1004
- activeFunctionalContractRequests.delete(session);
1005
1688
  client?.releaseWithoutClosing();
1006
1689
  }
1007
1690
  }
1691
+ async function resumePreDelegationOnSession(session, params) {
1692
+ const client = new UserCoordinatorSession(
1693
+ {
1694
+ coordinator: getSessionState(session).pool,
1695
+ createSession: async () => session
1696
+ },
1697
+ false
1698
+ );
1699
+ try {
1700
+ return await client.resumePreDelegation(params);
1701
+ } finally {
1702
+ client.releaseWithoutClosing();
1703
+ }
1704
+ }
1008
1705
  async function requestRefundOnSession(session, params) {
1009
1706
  const client = new UserCoordinatorSession(
1010
1707
  {
@@ -1057,6 +1754,12 @@ async function requestUserSync(session, swapId, syncSecret, timeoutMs) {
1057
1754
  );
1058
1755
  const payload = (await loadProtocol()).asSyncUserResponsePayload(env);
1059
1756
  assertSyncResponseContractId(env, payload, swapId);
1757
+ await persistTransferStatus(
1758
+ getSessionState(session).clientPersistence,
1759
+ swapId,
1760
+ payload,
1761
+ env.timestamp_ms
1762
+ );
1060
1763
  return {
1061
1764
  ...payload,
1062
1765
  coordinatorTimestampMs: env.timestamp_ms
@@ -1140,6 +1843,18 @@ function isRetryableSwapStatusReconciliationError(err) {
1140
1843
  if (err instanceof AttestationError) return false;
1141
1844
  return err instanceof CommandError && err.code === "REJECTED_STALE_JOB";
1142
1845
  }
1846
+ function isTerminalSwapState(state) {
1847
+ return state === "completed" || state === "refunded" || state === "failed";
1848
+ }
1849
+ function storageErrorFromPush(error, swapId, refundExecuted) {
1850
+ if (error instanceof StorageError) return error;
1851
+ return new StorageError("STORAGE_NOT_AVAILABLE", "client persistence update failed", {
1852
+ cause: error,
1853
+ swapId,
1854
+ phase: refundExecuted ? "refund" : "sync",
1855
+ remoteAccepted: true
1856
+ });
1857
+ }
1143
1858
  function coordinatorErrorFromEnvelope(protocol, env) {
1144
1859
  try {
1145
1860
  const payload = protocol.asErrorPayload(env);
@@ -1182,11 +1897,35 @@ async function contractRequest(session, args) {
1182
1897
  ...args.onSyncSecretReady !== void 0 && {
1183
1898
  onSyncSecretReady: args.onSyncSecretReady
1184
1899
  },
1900
+ ...args.resumeSecret !== void 0 && { resumeSecret: args.resumeSecret },
1901
+ ...args.onResumeSecretReady !== void 0 && {
1902
+ onResumeSecretReady: args.onResumeSecretReady
1903
+ },
1904
+ ...args.onSwapAllocated !== void 0 && {
1905
+ onSwapAllocated: args.onSwapAllocated
1906
+ },
1185
1907
  ...args.onRecoveryCodeReady !== void 0 && {
1186
1908
  onRecoveryCodeReady: args.onRecoveryCodeReady
1909
+ },
1910
+ ...args.onDelegateShareReady !== void 0 && {
1911
+ onDelegateShareReady: args.onDelegateShareReady
1187
1912
  }
1188
1913
  });
1189
1914
  }
1915
+ async function resumePreDelegation(session, args) {
1916
+ assertAttested(session);
1917
+ return resumePreDelegationOnSession(session, {
1918
+ swapId: args.swapId,
1919
+ resumeSecret: new Uint8Array(args.resumeSecret),
1920
+ ...args.syncSecret !== void 0 && { syncSecret: new Uint8Array(args.syncSecret) },
1921
+ recoveryCommitment: new Uint8Array(args.recoveryCommitment),
1922
+ policySnapshot: args.policySnapshot,
1923
+ ...args.onDelegateShareReady !== void 0 && {
1924
+ onDelegateShareReady: args.onDelegateShareReady
1925
+ },
1926
+ ...args.delegateShare !== void 0 && { delegateShare: new Uint8Array(args.delegateShare) }
1927
+ });
1928
+ }
1190
1929
  async function sync(session, contractId, syncSecret, timeoutMs) {
1191
1930
  assertAttested(session);
1192
1931
  return requestUserSync(session, contractId, syncSecret, timeoutMs);
@@ -1228,10 +1967,12 @@ async function requestRefund(session, args) {
1228
1967
  assertAttested(session);
1229
1968
  assertRecoveryCode(args.recoveryCode);
1230
1969
  return requestRefundOnSession(session, {
1970
+ ...args.swapId !== void 0 && { swapId: args.swapId },
1971
+ ...args.syncSecret !== void 0 && { syncSecret: new Uint8Array(args.syncSecret) },
1231
1972
  recoveryCode: new Uint8Array(args.recoveryCode)
1232
1973
  });
1233
1974
  }
1234
1975
 
1235
- 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, generateRecoveryCode, recoveryCodeFromHex, recoveryCodeFromUserInput, recoveryCodeToHex, requestRefund, sync };
1976
+ 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, generateRecoveryCode, recoveryCodeFromHex, recoveryCodeFromUserInput, recoveryCodeToHex, requestRefund, resumePreDelegation, sync };
1236
1977
  //# sourceMappingURL=user.js.map
1237
1978
  //# sourceMappingURL=user.js.map