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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/user.js CHANGED
@@ -1,11 +1,11 @@
1
- import { scheduledPayoutWindowSeconds } from './chunk-NKRVQXRP.js';
2
- import { validatePayoutPolicyShape, formatAjvErrors, createSession, createExponentialBackoff } from './chunk-BWCGNTN5.js';
1
+ import { scheduledPayoutWindowSeconds } from './chunk-E4DCEDAG.js';
2
+ import { validatePayoutPolicyShape, formatAjvErrors, createSession, createExponentialBackoff } from './chunk-5NNEO7IK.js';
3
3
  import { PRODUCT_MAX_ENTRY_LAMPORTS, PRODUCT_MIN_ENTRY_LAMPORTS } from './chunk-4X5OCCQW.js';
4
4
  import { assertAttested, publicKey, solanaAddressFromPublicKeyBytes } from './chunk-NHEHCUS5.js';
5
- import { requestEnvelope, CoordinatorMessageType, onEnvelope, closeProtocolRuntime, EnvelopeKind } from './chunk-QQSI3ZD7.js';
5
+ import { requestEnvelope, CoordinatorMessageType, onEnvelope, closeProtocolRuntime, EnvelopeKind } from './chunk-UADJCUPK.js';
6
6
  import { getSessionState } from './chunk-V5HXJRBW.js';
7
7
  import './chunk-DIA6U2CV.js';
8
- import { PolicyValidationError, CommandError, TransportError, InvalidDepositAmountError, normalizeError, RoutingError, AttestationError } from './chunk-GHBQF3A7.js';
8
+ import { PolicyValidationError, CommandError, TransportError, InvalidDepositAmountError, normalizeError, AttestationError, RoutingError } from './chunk-GHBQF3A7.js';
9
9
  export { InvalidDepositAmountError } from './chunk-GHBQF3A7.js';
10
10
  import './chunk-VR6T6OJS.js';
11
11
 
@@ -171,7 +171,7 @@ function recoveryCodeFromUserInput(input) {
171
171
  var protocolPromise = null;
172
172
  var frostRuntimePromise = null;
173
173
  function loadProtocol() {
174
- protocolPromise ??= import('./coordinator-VVJ33WF2.js');
174
+ protocolPromise ??= import('./coordinator-ZNLGAYXD.js');
175
175
  return protocolPromise;
176
176
  }
177
177
  function loadFrostRuntime() {
@@ -185,20 +185,22 @@ var BENIGN_TRANSPORT_DROP = /* @__PURE__ */ Symbol("benignTransportDrop");
185
185
  var ALICE_ID = 1;
186
186
  var COORDINATOR_ID = 2;
187
187
  var SYNC_SECRET_BYTE_LENGTH = 32;
188
+ var RESUME_SECRET_BYTE_LENGTH = SYNC_SECRET_BYTE_LENGTH;
188
189
  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
190
  var DEFAULT_SWAP_STATUS_RECONCILE_INTERVAL_MS = 5e3;
191
191
  var MAX_SWAP_STATUS_RETRY_DELAY_MS = 3e4;
192
192
  var SWAP_STATUS_RETRY_MULTIPLIER = 2;
193
+ var MAX_EVENT_DEDUP_ENTRIES_PER_KIND = 1024;
193
194
  var SWAP_STATUS_RETRY_JITTER_RATIO = 0.2;
194
195
  var DEFAULT_REFUND_OUTCOME_POLL_BUDGET_MS = 5 * 60 * 1e3;
195
196
  var REFUND_OUTCOME_POLL_INTERVAL_MS = 3e3;
196
197
  var REFUND_OUTCOME_PER_POLL_TIMEOUT_MS = 5e3;
198
+ var UNBOUND_RECONCILIATION_KEY = "__unbound__";
197
199
  var REQUEST_TIMEOUT_MESSAGE = "coordinator request timed out";
200
+ var REFUND_PENDING_FINALIZATION_MESSAGE = "refund accepted and pending finalization; check status again shortly";
198
201
  var CURRENT_SCHEDULED_PAYOUT_WINDOW_SECONDS = new Set(scheduledPayoutWindowSeconds());
199
202
  var CURRENT_SCHEDULED_PAYOUT_WINDOW_MESSAGE = `scheduled payout_spec.window_seconds must be one of [${scheduledPayoutWindowSeconds().join(", ")}]`;
200
203
  var TERMINAL_SWAP_STATES = /* @__PURE__ */ new Set(["completed", "refunded", "failed"]);
201
- var activeFunctionalContractRequests = /* @__PURE__ */ new WeakSet();
202
204
  function isBenignTransportDrop(err) {
203
205
  return typeof err === "object" && err !== null && err[BENIGN_TRANSPORT_DROP] === true;
204
206
  }
@@ -227,8 +229,12 @@ var UserCoordinatorSession = class {
227
229
  this.sessionStatus = "idle";
228
230
  this.swapId = null;
229
231
  this.syncSecret = null;
232
+ this.resumeSecret = null;
230
233
  this.attested = false;
231
234
  this.pushListeners = /* @__PURE__ */ new Map();
235
+ this.swapContexts = /* @__PURE__ */ new Map();
236
+ this.openingSession = null;
237
+ this.activeOperations = 0;
232
238
  this.stateListeners = /* @__PURE__ */ new Set();
233
239
  this.logListeners = /* @__PURE__ */ new Set();
234
240
  this.payoutExecutedListeners = /* @__PURE__ */ new Set();
@@ -237,12 +243,10 @@ var UserCoordinatorSession = class {
237
243
  this.coordinatorErrorListeners = /* @__PURE__ */ new Set();
238
244
  this.emittedPayoutKeys = /* @__PURE__ */ new Set();
239
245
  this.emittedRefundKeys = /* @__PURE__ */ new Set();
240
- this.stopStatusReconciliation = null;
241
- this.bufferDepositConfirmedUntilDelegateAck = false;
242
- this.bufferedDepositConfirmed = null;
246
+ this.statusReconciliationStops = /* @__PURE__ */ new Map();
243
247
  this.coordinator = options.coordinator;
244
248
  this.createSessionImpl = options.createSession ?? createSession;
245
- this.closeSessionAfterRefund = closeSessionAfterRefund;
249
+ this.closeSessionAfterOperationFailure = closeSessionAfterRefund;
246
250
  }
247
251
  onStateChange(listener) {
248
252
  this.stateListeners.add(listener);
@@ -278,51 +282,226 @@ var UserCoordinatorSession = class {
278
282
  get currentSwapId() {
279
283
  return this.swapId;
280
284
  }
285
+ beginOperation() {
286
+ this.activeOperations += 1;
287
+ if (this.activeOperations === 1) this.setSessionStatus("connecting");
288
+ }
289
+ endOperation() {
290
+ this.activeOperations = Math.max(0, this.activeOperations - 1);
291
+ if (this.activeOperations === 0 && this.sessionStatus === "connecting") {
292
+ this.setSessionStatus("idle");
293
+ }
294
+ }
295
+ ensureSwapContext(swapId, syncSecret, resumeSecret) {
296
+ let context = this.swapContexts.get(swapId);
297
+ if (!context) {
298
+ context = {
299
+ swapId,
300
+ swapState: this.swapId === swapId ? this.swapState : "new",
301
+ syncSecret: this.swapId === swapId && this.syncSecret ? new Uint8Array(this.syncSecret) : null,
302
+ resumeSecret: this.swapId === swapId && this.resumeSecret ? new Uint8Array(this.resumeSecret) : null,
303
+ bufferDepositConfirmedUntilDelegateAck: false,
304
+ bufferedDepositConfirmed: null
305
+ };
306
+ this.swapContexts.set(swapId, context);
307
+ }
308
+ if (syncSecret !== void 0) context.syncSecret = new Uint8Array(syncSecret);
309
+ if (resumeSecret !== void 0) context.resumeSecret = new Uint8Array(resumeSecret);
310
+ this.setCurrentContext(context);
311
+ return context;
312
+ }
313
+ setCurrentContext(context) {
314
+ this.swapId = context.swapId;
315
+ this.swapState = context.swapState;
316
+ this.syncSecret = context.syncSecret ? new Uint8Array(context.syncSecret) : null;
317
+ this.resumeSecret = context.resumeSecret ? new Uint8Array(context.resumeSecret) : null;
318
+ }
319
+ updateContextState(context, state) {
320
+ context.swapState = state;
321
+ if (this.swapId === context.swapId) {
322
+ this.swapState = state;
323
+ }
324
+ this.emitStateChange(context.swapId, state);
325
+ if (TERMINAL_SWAP_STATES.has(state)) {
326
+ this.evictSwapContext(context.swapId);
327
+ }
328
+ }
329
+ evictSwapContext(swapId) {
330
+ this.statusReconciliationStops.get(swapId)?.();
331
+ this.statusReconciliationStops.delete(swapId);
332
+ const context = this.swapContexts.get(swapId);
333
+ if (context) {
334
+ context.syncSecret?.fill(0);
335
+ context.resumeSecret?.fill(0);
336
+ context.bufferedDepositConfirmed = null;
337
+ context.bufferDepositConfirmedUntilDelegateAck = false;
338
+ this.swapContexts.delete(swapId);
339
+ }
340
+ if (this.swapId === swapId) {
341
+ this.syncSecret?.fill(0);
342
+ this.resumeSecret?.fill(0);
343
+ this.syncSecret = null;
344
+ this.resumeSecret = null;
345
+ }
346
+ }
347
+ clearSwapContexts() {
348
+ for (const swapId of this.swapContexts.keys()) this.evictSwapContext(swapId);
349
+ this.swapContexts.clear();
350
+ this.emittedPayoutKeys.clear();
351
+ this.emittedRefundKeys.clear();
352
+ }
281
353
  async runSwap(params) {
282
354
  assertCurrentUserPayoutSpecRuntimeLimits(params.payoutSpec);
283
- this.assertNoActiveContract();
284
- this.setSessionStatus("connecting");
355
+ this.beginOperation();
285
356
  let completed = false;
357
+ let terminalStatus = null;
286
358
  try {
287
359
  await ensureFrostWasm();
288
360
  await this.openSession();
289
361
  this.setSessionStatus("running");
290
362
  const result = await this.runProtocol(params);
291
363
  completed = true;
292
- this.setSessionStatus("done");
364
+ terminalStatus = "done";
293
365
  return result;
294
366
  } catch (err) {
295
367
  const error = this.normalizeSessionError(err);
296
368
  if (!isBenignTransportDrop(error)) {
297
- this.setSessionStatus("failed");
369
+ terminalStatus = "failed";
298
370
  }
299
371
  throw error;
300
372
  } finally {
301
- if (!completed) {
373
+ this.endOperation();
374
+ if (this.activeOperations === 0 && terminalStatus !== null) {
375
+ this.setSessionStatus(terminalStatus);
376
+ }
377
+ if (!completed && this.activeOperations === 0 && this.closeSessionAfterOperationFailure) {
302
378
  this.close();
303
379
  }
304
380
  }
305
381
  }
306
382
  async contractRequest(params) {
307
383
  assertCurrentUserPayoutSpecRuntimeLimits(params.payoutSpec);
308
- this.assertNoActiveContract();
309
- this.setSessionStatus("connecting");
384
+ this.beginOperation();
385
+ let completed = false;
386
+ let terminalStatus = null;
310
387
  try {
311
388
  await ensureFrostWasm();
312
389
  await this.openSession();
313
390
  this.setSessionStatus("running");
314
391
  const prepared = await this.prepareContract(params);
392
+ completed = true;
393
+ terminalStatus = "done";
315
394
  return toContractRequestResult(prepared);
316
395
  } catch (err) {
317
396
  const error = this.normalizeSessionError(err);
318
- this.setSessionStatus("failed");
397
+ terminalStatus = "failed";
319
398
  throw error;
399
+ } finally {
400
+ this.endOperation();
401
+ if (this.activeOperations === 0 && terminalStatus !== null) {
402
+ this.setSessionStatus(terminalStatus);
403
+ }
404
+ if (!completed && this.activeOperations === 0 && this.closeSessionAfterOperationFailure) {
405
+ this.close();
406
+ }
407
+ }
408
+ }
409
+ async resumePreDelegation(params) {
410
+ if (!params.policySnapshot) {
411
+ throw new CommandError(
412
+ "REJECTED_GUARD",
413
+ "policySnapshot is required to resume pre-delegation"
414
+ );
415
+ }
416
+ if (params.recoveryCommitment.length !== SYNC_SECRET_BYTE_LENGTH) {
417
+ throw new CommandError(
418
+ "REJECTED_GUARD",
419
+ `recoveryCommitment must be ${SYNC_SECRET_BYTE_LENGTH} bytes`
420
+ );
421
+ }
422
+ this.beginOperation();
423
+ let completed = false;
424
+ let terminalStatus = null;
425
+ try {
426
+ await ensureFrostWasm();
427
+ this.restoreSwapContext({
428
+ swapId: params.swapId,
429
+ syncSecret: params.syncSecret,
430
+ resumeSecret: params.resumeSecret
431
+ });
432
+ await this.openSession();
433
+ this.setSessionStatus("running");
434
+ const context = this.ensureSwapContext(params.swapId);
435
+ context.swapState = params.delegateShare ? "awaiting_deposit" : "awaiting_dkg";
436
+ this.setCurrentContext(context);
437
+ let jointPublicKey;
438
+ let depositExpiresAtMs;
439
+ let coordinatorTimestampMs;
440
+ let delegateShare = params.delegateShare ? new Uint8Array(params.delegateShare) : void 0;
441
+ if (!delegateShare) {
442
+ const dkg = await this.runDkg(context, params.onDelegateShareReady);
443
+ jointPublicKey = dkg.jointPublicKey;
444
+ depositExpiresAtMs = dkg.depositExpiresAtMs;
445
+ coordinatorTimestampMs = dkg.coordinatorTimestampMs;
446
+ delegateShare = dkg.keyHandle.secretShareBytes();
447
+ this.updateContextState(context, "awaiting_deposit");
448
+ }
449
+ const delegateAckPromise = this.awaitPush(CoordinatorMessageType.DelegateAck, context.swapId);
450
+ let delegateResp;
451
+ try {
452
+ delegateResp = await this.request(
453
+ CoordinatorMessageType.DelegateShare,
454
+ {
455
+ share: delegateShare,
456
+ recovery_commitment: new Uint8Array(params.recoveryCommitment),
457
+ resume_secret: context.resumeSecret
458
+ },
459
+ { swapId: context.swapId }
460
+ );
461
+ } catch (err) {
462
+ void delegateAckPromise.catch(() => void 0);
463
+ throw err;
464
+ }
465
+ const delegatedAtMs = this.requireProtocol().asDelegateShareResponsePayload(delegateResp).delegated_at_ms;
466
+ const delegateAck = this.requireProtocol().asDelegateAckPayload(await delegateAckPromise);
467
+ if (delegateAck.delegated_at_ms !== delegatedAtMs) {
468
+ throw new CommandError(
469
+ "REJECTED_GUARD",
470
+ "DelegateAck delegated_at_ms does not match DelegateShare response"
471
+ );
472
+ }
473
+ if (!spendingPoliciesEqual(delegateAck.spending_policy, params.policySnapshot)) {
474
+ throw new CommandError(
475
+ "REJECTED_GUARD",
476
+ "DelegateAck spending_policy does not match persisted policy_snapshot"
477
+ );
478
+ }
479
+ context.bufferDepositConfirmedUntilDelegateAck = false;
480
+ this.updateContextState(context, "awaiting_deposit");
481
+ completed = true;
482
+ terminalStatus = "done";
483
+ return {
484
+ swapId: context.swapId,
485
+ jointPublicKey,
486
+ depositExpiresAtMs,
487
+ coordinatorTimestampMs,
488
+ delegatedAtMs
489
+ };
490
+ } catch (err) {
491
+ terminalStatus = "failed";
492
+ throw this.normalizeSessionError(err);
493
+ } finally {
494
+ if (!completed) this.cancelPushWaitersForSwap(params.swapId);
495
+ this.endOperation();
496
+ if (this.activeOperations === 0 && terminalStatus !== null) {
497
+ this.setSessionStatus(terminalStatus);
498
+ }
320
499
  }
321
500
  }
322
501
  /** @internal Used by the functional Session API without closing its caller-owned session. */
323
502
  releaseWithoutClosing() {
324
- this.stopStatusReconciliation?.();
325
- this.stopStatusReconciliation = null;
503
+ this.stopAllStatusReconciliations();
504
+ this.clearSwapContexts();
326
505
  this.detachSession(
327
506
  new TransportError("CONNECTION_LOST", "coordinator session operation released"),
328
507
  false,
@@ -330,12 +509,15 @@ var UserCoordinatorSession = class {
330
509
  );
331
510
  }
332
511
  close() {
333
- this.stopStatusReconciliation?.();
334
- this.stopStatusReconciliation = null;
512
+ this.stopAllStatusReconciliations();
513
+ this.clearSwapContexts();
335
514
  this.detachSession(new TransportError("CONNECTION_LOST", "coordinator session closed"), true);
336
515
  }
516
+ stopAllStatusReconciliations() {
517
+ for (const stop of [...this.statusReconciliationStops.values()]) stop();
518
+ this.statusReconciliationStops.clear();
519
+ }
337
520
  restoreSwapContext(params) {
338
- this.swapId = params.swapId;
339
521
  if (params.syncSecret !== void 0) {
340
522
  if (params.syncSecret.length !== SYNC_SECRET_BYTE_LENGTH) {
341
523
  throw new CommandError(
@@ -343,28 +525,46 @@ var UserCoordinatorSession = class {
343
525
  `syncSecret must be ${SYNC_SECRET_BYTE_LENGTH} bytes`
344
526
  );
345
527
  }
346
- this.syncSecret = new Uint8Array(params.syncSecret);
347
528
  }
529
+ if (params.resumeSecret !== void 0) {
530
+ if (params.resumeSecret.length !== RESUME_SECRET_BYTE_LENGTH) {
531
+ throw new CommandError(
532
+ "REJECTED_GUARD",
533
+ `resumeSecret must be ${RESUME_SECRET_BYTE_LENGTH} bytes`
534
+ );
535
+ }
536
+ }
537
+ this.ensureSwapContext(params.swapId, params.syncSecret, params.resumeSecret);
348
538
  }
349
539
  async requestSwapStatus(timeoutMs) {
350
540
  if (!this.swapId) {
351
541
  throw new CommandError("REJECTED_STATE", "Sync requires swapId");
352
542
  }
543
+ const context = this.ensureSwapContext(this.swapId, this.syncSecret ?? void 0);
544
+ return this.requestSwapStatusForContext(context, timeoutMs);
545
+ }
546
+ async requestSwapStatusForContext(context, timeoutMs) {
353
547
  await this.openSession();
354
548
  const env = await this.request(
355
549
  CoordinatorMessageType.Sync,
356
550
  {
357
551
  actor: "user",
358
- ...this.syncSecret ? { sync_secret: this.syncSecret } : {}
552
+ ...context.syncSecret ? { sync_secret: context.syncSecret } : {}
359
553
  },
360
- timeoutMs
554
+ {
555
+ swapId: context.swapId,
556
+ timeoutMs,
557
+ rejectMatchingOutOfBandErrors: true
558
+ }
361
559
  );
362
560
  const status = this.requireProtocol().asSyncUserResponsePayload(env);
363
- this.applySwapStatus(status, env.timestamp_ms);
561
+ this.applySwapStatus(context, status, env.timestamp_ms);
364
562
  return status;
365
563
  }
366
564
  startSwapStatusReconciliation(intervalMs = DEFAULT_SWAP_STATUS_RECONCILE_INTERVAL_MS) {
367
- this.stopStatusReconciliation?.();
565
+ const reconciliationContext = this.swapId ? this.ensureSwapContext(this.swapId, this.syncSecret ?? void 0) : null;
566
+ const reconciliationKey = reconciliationContext?.swapId ?? UNBOUND_RECONCILIATION_KEY;
567
+ this.statusReconciliationStops.get(reconciliationKey)?.();
368
568
  let stopped = false;
369
569
  let reconciling = false;
370
570
  let timer = null;
@@ -389,20 +589,18 @@ var UserCoordinatorSession = class {
389
589
  if (stopped || reconciling || !isVisible()) return;
390
590
  reconciling = true;
391
591
  try {
392
- const status = await this.requestSwapStatus();
592
+ const status = reconciliationContext ? await this.requestSwapStatusForContext(reconciliationContext) : await this.requestSwapStatus();
393
593
  retryBackoff.reset();
394
594
  if (status.state === "completed" || status.state === "refunded" || status.state === "failed") {
395
- stopped = true;
396
- if (timer) clearTimeout(timer);
595
+ stopReconciliation();
397
596
  } else {
398
597
  schedule(intervalMs);
399
598
  }
400
599
  } catch (err) {
401
600
  this.log(`SwapStatus reconcile failed: ${normalizeError(err)}`, "info");
402
601
  if (!isRetryableSwapStatusReconciliationError(err)) {
403
- stopped = true;
404
- if (timer) clearTimeout(timer);
405
- this.close();
602
+ stopReconciliation();
603
+ if (err instanceof AttestationError) this.close();
406
604
  return;
407
605
  }
408
606
  schedule(retryBackoff.nextDelayMs());
@@ -438,7 +636,8 @@ var UserCoordinatorSession = class {
438
636
  browserDocument?.addEventListener("freeze", handleFreeze);
439
637
  browserDocument?.addEventListener("resume", reconcileWhenVisible);
440
638
  reconcileWhenVisible();
441
- this.stopStatusReconciliation = () => {
639
+ const stopReconciliation = () => {
640
+ if (stopped) return;
442
641
  stopped = true;
443
642
  if (timer) clearTimeout(timer);
444
643
  browserWindow?.removeEventListener("focus", reconcileWhenVisible);
@@ -447,8 +646,12 @@ var UserCoordinatorSession = class {
447
646
  browserDocument?.removeEventListener("visibilitychange", handleVisibilityChange);
448
647
  browserDocument?.removeEventListener("freeze", handleFreeze);
449
648
  browserDocument?.removeEventListener("resume", reconcileWhenVisible);
649
+ if (this.statusReconciliationStops.get(reconciliationKey) === stopReconciliation) {
650
+ this.statusReconciliationStops.delete(reconciliationKey);
651
+ }
450
652
  };
451
- return this.stopStatusReconciliation;
653
+ this.statusReconciliationStops.set(reconciliationKey, stopReconciliation);
654
+ return stopReconciliation;
452
655
  }
453
656
  async requestRefund(params) {
454
657
  const result = await this.requestRefundIntentInternal(params, { waitForFinality: true });
@@ -460,9 +663,10 @@ var UserCoordinatorSession = class {
460
663
  }
461
664
  async requestRefundIntentInternal(params, options) {
462
665
  assertRecoveryCode(params.recoveryCode);
666
+ this.beginOperation();
463
667
  const recoveryCode = new Uint8Array(params.recoveryCode);
464
668
  const ackTimeout = params.timeoutMs ?? 3e4;
465
- let ackTimer;
669
+ let requestSwapId = params.swapId ?? null;
466
670
  let openedForRefund = false;
467
671
  try {
468
672
  if (params.syncSecret !== void 0) {
@@ -471,33 +675,36 @@ var UserCoordinatorSession = class {
471
675
  throw new CommandError("REJECTED_STATE", "syncSecret requires swapId");
472
676
  }
473
677
  this.restoreSwapContext({ swapId, syncSecret: params.syncSecret });
678
+ requestSwapId = swapId;
474
679
  } else if (params.swapId) {
475
680
  const swapId = params.swapId;
476
681
  this.restoreSwapContext({ swapId });
682
+ requestSwapId = swapId;
477
683
  }
478
684
  openedForRefund = !this.hasUsableSession();
479
685
  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
686
+ let env;
687
+ try {
688
+ env = await this.request(
689
+ CoordinatorMessageType.WithdrawRequest,
690
+ {
691
+ recovery_code: recoveryCode
692
+ },
693
+ { swapId: requestSwapId, timeoutMs: ackTimeout }
694
+ );
695
+ } catch (err) {
696
+ if (isRequestTimeoutError(err)) {
697
+ throw new CommandError(
698
+ "REJECTED_STALE_JOB",
699
+ `WithdrawRequest acknowledgement timed out after ${ackTimeout}ms`
493
700
  );
494
- })
495
- ]);
496
- if (ackTimer) clearTimeout(ackTimer);
497
- if (env.type === CoordinatorMessageType.WithdrawExecuted) {
498
- if (env.swap_id && !this.swapId) {
499
- this.swapId = env.swap_id;
500
701
  }
702
+ throw err;
703
+ }
704
+ if (env.type === CoordinatorMessageType.WithdrawExecuted) {
705
+ const resolvedSwapId2 = env.swap_id ?? requestSwapId;
706
+ const resolvedContext2 = resolvedSwapId2 ? this.ensureSwapContext(resolvedSwapId2) : null;
707
+ if (resolvedContext2) this.setCurrentContext(resolvedContext2);
501
708
  return { kind: "executed", refund: this.requireProtocol().asWithdrawExecutedPayload(env) };
502
709
  }
503
710
  if (env.type !== CoordinatorMessageType.WithdrawAccepted) {
@@ -506,29 +713,32 @@ var UserCoordinatorSession = class {
506
713
  `Expected WithdrawAccepted or WithdrawExecuted response, got ${env.type}`
507
714
  );
508
715
  }
509
- if (env.swap_id && !this.swapId) {
510
- this.swapId = env.swap_id;
511
- }
512
716
  const accepted = this.requireProtocol().asWithdrawAcceptedPayload(env);
717
+ const resolvedSwapId = env.swap_id ?? requestSwapId;
718
+ const resolvedContext = resolvedSwapId ? this.ensureSwapContext(resolvedSwapId) : null;
513
719
  if (!options.waitForFinality) {
514
720
  return { kind: "accepted", accepted };
515
721
  }
516
- if (!this.swapId) {
517
- throw new CommandError(
518
- "REJECTED_STALE_JOB",
519
- "refund accepted and pending finalization; check status again shortly"
520
- );
722
+ if (!resolvedContext) {
723
+ throw new CommandError("REJECTED_STALE_JOB", REFUND_PENDING_FINALIZATION_MESSAGE);
724
+ }
725
+ if (!resolvedContext.syncSecret) {
726
+ throw new CommandError("REJECTED_STALE_JOB", REFUND_PENDING_FINALIZATION_MESSAGE);
521
727
  }
522
- return { kind: "executed", refund: await this.awaitRefundOutcomeFromSwapStatus() };
728
+ this.setCurrentContext(resolvedContext);
729
+ return {
730
+ kind: "executed",
731
+ refund: await this.awaitRefundOutcomeFromSwapStatus(resolvedContext)
732
+ };
523
733
  } finally {
524
- if (ackTimer) clearTimeout(ackTimer);
525
734
  recoveryCode.fill(0);
526
- if (openedForRefund && this.closeSessionAfterRefund) {
735
+ this.endOperation();
736
+ if (openedForRefund && this.closeSessionAfterOperationFailure && this.activeOperations === 0) {
527
737
  this.close();
528
738
  }
529
739
  }
530
740
  }
531
- async awaitRefundOutcomeFromSwapStatus() {
741
+ async awaitRefundOutcomeFromSwapStatus(context) {
532
742
  const deadline = Date.now() + DEFAULT_REFUND_OUTCOME_POLL_BUDGET_MS;
533
743
  for (; ; ) {
534
744
  const perPollTimeout = Math.min(
@@ -536,7 +746,7 @@ var UserCoordinatorSession = class {
536
746
  Math.max(0, deadline - Date.now())
537
747
  );
538
748
  try {
539
- const status = await this.requestSwapStatus(perPollTimeout);
749
+ const status = await this.requestSwapStatusForContext(context, perPollTimeout);
540
750
  if (status.withdraw_execution) {
541
751
  return status.withdraw_execution;
542
752
  }
@@ -546,10 +756,7 @@ var UserCoordinatorSession = class {
546
756
  }
547
757
  }
548
758
  if (Date.now() >= deadline) {
549
- throw new CommandError(
550
- "REJECTED_STALE_JOB",
551
- "refund accepted and pending finalization; check status again shortly"
552
- );
759
+ throw new CommandError("REJECTED_STALE_JOB", REFUND_PENDING_FINALIZATION_MESSAGE);
553
760
  }
554
761
  await delay(REFUND_OUTCOME_POLL_INTERVAL_MS);
555
762
  }
@@ -570,13 +777,19 @@ var UserCoordinatorSession = class {
570
777
  "in"
571
778
  );
572
779
  await params.onDepositConfirmed?.({ originAddress: depositPayload.origin_address });
573
- await this.requestSwapStatus();
780
+ const context = this.swapContexts.get(prepared.swapId);
781
+ if (!context) {
782
+ throw new CommandError("REJECTED_STATE", `missing client context for ${prepared.swapId}`);
783
+ }
784
+ await this.requestSwapStatusForContext(context);
574
785
  return {
575
786
  swapId: prepared.swapId,
576
787
  jointPublicKey: prepared.jointPublicKey,
577
788
  depositTxSignature: depositPayload.tx_signature,
578
789
  depositObservedAtMs: depositPayload.observed_at_ms,
579
790
  delegatedAtMs: prepared.delegatedAtMs,
791
+ resumeSecret: new Uint8Array(prepared.resumeSecret),
792
+ resumeSecretHex: prepared.resumeSecretHex,
580
793
  recoveryCodeHex: prepared.recoveryCodeHex,
581
794
  policySnapshot: prepared.policySnapshot,
582
795
  delegatePolicySnapshot: prepared.delegatePolicySnapshot
@@ -587,6 +800,7 @@ var UserCoordinatorSession = class {
587
800
  const requestedPayoutWindowMs = params.payoutWindowMs ?? payoutWindowMsFromSpec(params.payoutSpec);
588
801
  const payoutDeadlineMs = requestedPayoutWindowMs !== void 0 ? Date.now() + requestedPayoutWindowMs : void 0;
589
802
  let generatedSyncSecret = null;
803
+ let generatedResumeSecret = null;
590
804
  const syncSecret = params.syncSecret ? new Uint8Array(params.syncSecret) : (() => {
591
805
  generatedSyncSecret = randomSecret32();
592
806
  return generatedSyncSecret;
@@ -597,105 +811,165 @@ var UserCoordinatorSession = class {
597
811
  `syncSecret must be ${SYNC_SECRET_BYTE_LENGTH} bytes`
598
812
  );
599
813
  }
600
- this.syncSecret = new Uint8Array(syncSecret);
601
814
  const syncSecretHex = bytesToHex(syncSecret);
602
815
  const syncCommitment = await sha256Commitment(syncSecret);
603
816
  if (generatedSyncSecret) {
604
817
  await params.onSyncSecretReady?.({ secretHex: syncSecretHex });
605
818
  }
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) {
819
+ const resumeSecret = params.resumeSecret ? new Uint8Array(params.resumeSecret) : (() => {
820
+ generatedResumeSecret = randomSecret32();
821
+ return generatedResumeSecret;
822
+ })();
823
+ if (resumeSecret.length !== RESUME_SECRET_BYTE_LENGTH) {
652
824
  throw new CommandError(
653
825
  "REJECTED_GUARD",
654
- "DelegateAck delegated_at_ms does not match DelegateShare response"
826
+ `resumeSecret must be ${RESUME_SECRET_BYTE_LENGTH} bytes`
655
827
  );
656
828
  }
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
- );
829
+ const resumeSecretHex = bytesToHex(resumeSecret);
830
+ const resumeCommitment = await sha256Commitment(resumeSecret);
831
+ if (generatedResumeSecret) {
832
+ await params.onResumeSecretReady?.({ secretHex: resumeSecretHex });
662
833
  }
663
- this.log(`delegated at ${delegateAck.delegated_at_ms}`, "in");
664
- this.releaseDepositConfirmedGate();
665
- await params.onDepositAddressReady?.({
834
+ const swapResp = await this.request(
835
+ CoordinatorMessageType.ContractRequest,
836
+ {
837
+ actor: "user",
838
+ amount_lamports: params.amountLamports,
839
+ payout_spec: params.payoutSpec,
840
+ sync_commitment: syncCommitment,
841
+ resume_commitment: resumeCommitment,
842
+ ...(params.payoutDeadlineMs !== void 0 || payoutDeadlineMs !== void 0) && {
843
+ payout_deadline_ms: params.payoutDeadlineMs ?? payoutDeadlineMs
844
+ }
845
+ },
846
+ { swapId: null, requireNonNullResponseSwapId: true }
847
+ );
848
+ const swapPayload = protocol.asSwapRequestResponsePayload(swapResp);
849
+ const swapId = swapPayload.swap_id;
850
+ const context = {
666
851
  swapId,
667
- jointPublicKey,
668
- amountLamports: params.amountLamports,
669
- depositExpiresAtMs,
670
- coordinatorTimestampMs
671
- });
672
- return {
673
- requestId: swapId,
852
+ swapState: "awaiting_dkg",
853
+ syncSecret: new Uint8Array(syncSecret),
854
+ resumeSecret: new Uint8Array(resumeSecret),
855
+ bufferDepositConfirmedUntilDelegateAck: true,
856
+ bufferedDepositConfirmed: null
857
+ };
858
+ this.swapContexts.set(swapId, context);
859
+ this.setCurrentContext(context);
860
+ await params.onSwapAllocated?.({
674
861
  swapId,
675
- amountLamports: params.amountLamports,
676
- jointPublicKey,
677
- depositExpiresAtMs,
678
- coordinatorTimestampMs,
679
- delegatedAtMs: delegateAck.delegated_at_ms,
680
- syncSecret,
681
862
  syncSecretHex,
682
- recoveryCodeHex,
683
- policySnapshot: swapPayload.policy_snapshot,
684
- delegatePolicySnapshot: delegateAck.spending_policy,
685
- depositConfirmedPromise
686
- };
863
+ resumeSecretHex
864
+ });
865
+ params.onPolicySnapshot?.(swapPayload.policy_snapshot);
866
+ this.log(`swap accepted - id=${swapId}`, "in");
867
+ let preparedSuccessfully = false;
868
+ try {
869
+ const depositConfirmedPromise = this.awaitPush(
870
+ CoordinatorMessageType.DepositConfirmed,
871
+ context.swapId,
872
+ null
873
+ );
874
+ void depositConfirmedPromise.catch(() => void 0);
875
+ const { jointPublicKey, keyHandle, depositExpiresAtMs, coordinatorTimestampMs } = await this.runDkg(context, params.delegateShare ? void 0 : params.onDelegateShareReady);
876
+ this.updateContextState(context, "awaiting_deposit");
877
+ let recoveryCommitment = params.recoveryCommitment;
878
+ let recoveryCodeHex;
879
+ if (!recoveryCommitment) {
880
+ const generated = await generateRecoveryCode();
881
+ recoveryCommitment = generated.commitment;
882
+ try {
883
+ recoveryCodeHex = recoveryCodeToHex(generated.code);
884
+ await params.onRecoveryCodeReady?.({ codeHex: recoveryCodeHex });
885
+ } finally {
886
+ generated.code.fill(0);
887
+ }
888
+ }
889
+ const delegateAckPromise = this.awaitPush(CoordinatorMessageType.DelegateAck, context.swapId);
890
+ const share = params.delegateShare ?? keyHandle.secretShareBytes();
891
+ let delegateResp;
892
+ try {
893
+ delegateResp = await this.request(
894
+ CoordinatorMessageType.DelegateShare,
895
+ {
896
+ share,
897
+ recovery_commitment: recoveryCommitment,
898
+ resume_secret: context.resumeSecret
899
+ },
900
+ { swapId: context.swapId }
901
+ );
902
+ } catch (err) {
903
+ void delegateAckPromise.catch(() => void 0);
904
+ throw err;
905
+ }
906
+ const delegatePayload = protocol.asDelegateShareResponsePayload(delegateResp);
907
+ const delegateAck = protocol.asDelegateAckPayload(await delegateAckPromise);
908
+ if (delegateAck.delegated_at_ms !== delegatePayload.delegated_at_ms) {
909
+ throw new CommandError(
910
+ "REJECTED_GUARD",
911
+ "DelegateAck delegated_at_ms does not match DelegateShare response"
912
+ );
913
+ }
914
+ if (!spendingPoliciesEqual(delegateAck.spending_policy, swapPayload.policy_snapshot)) {
915
+ throw new CommandError(
916
+ "REJECTED_GUARD",
917
+ "DelegateAck spending_policy does not match ContractRequest policy_snapshot"
918
+ );
919
+ }
920
+ this.log(`delegated at ${delegateAck.delegated_at_ms}`, "in");
921
+ this.releaseDepositConfirmedGate(context);
922
+ await params.onDepositAddressReady?.({
923
+ swapId,
924
+ jointPublicKey,
925
+ amountLamports: params.amountLamports,
926
+ depositExpiresAtMs,
927
+ coordinatorTimestampMs
928
+ });
929
+ preparedSuccessfully = true;
930
+ return {
931
+ requestId: swapId,
932
+ swapId,
933
+ amountLamports: params.amountLamports,
934
+ jointPublicKey,
935
+ depositExpiresAtMs,
936
+ coordinatorTimestampMs,
937
+ delegatedAtMs: delegateAck.delegated_at_ms,
938
+ syncSecret,
939
+ syncSecretHex,
940
+ resumeSecret,
941
+ resumeSecretHex,
942
+ recoveryCodeHex,
943
+ policySnapshot: swapPayload.policy_snapshot,
944
+ delegatePolicySnapshot: delegateAck.spending_policy,
945
+ depositConfirmedPromise
946
+ };
947
+ } finally {
948
+ if (!preparedSuccessfully) {
949
+ this.cancelPushWaitersForSwap(context.swapId);
950
+ }
951
+ }
687
952
  }
688
- async runDkg() {
953
+ async runDkg(context, onDelegateShareReady) {
689
954
  await ensureFrostWasm();
690
955
  const protocol = this.requireProtocol();
691
956
  const { dkgRound1 } = await loadFrost();
692
- await this.request(CoordinatorMessageType.DkgInit, {});
957
+ await this.request(
958
+ CoordinatorMessageType.DkgInit,
959
+ { resume_secret: context.resumeSecret },
960
+ { swapId: context.swapId }
961
+ );
693
962
  this.log("DKG initialised", "info");
694
963
  const aliceRound1State = dkgRound1(ALICE_ID);
695
964
  const aliceRound1Bytes = aliceRound1State.round1PackageBytes();
696
- const round1Resp = await this.request(CoordinatorMessageType.DkgRound1, {
697
- package: aliceRound1Bytes
698
- });
965
+ const round1Resp = await this.request(
966
+ CoordinatorMessageType.DkgRound1,
967
+ {
968
+ package: aliceRound1Bytes,
969
+ resume_secret: context.resumeSecret
970
+ },
971
+ { swapId: context.swapId }
972
+ );
699
973
  const round1Payload = protocol.asDkgRound1ResponsePayload(round1Resp);
700
974
  this.log(`DKG round1 complete - coord pkg ${round1Payload.package.byteLength}B`, "in");
701
975
  const aliceRound2State = aliceRound1State.intoRound2(COORDINATOR_ID, round1Payload.package);
@@ -710,10 +984,20 @@ var UserCoordinatorSession = class {
710
984
  } catch (err) {
711
985
  throw new CommandError("REJECTED_GUARD", `Alice DKG finalize failed: ${normalizeError(err)}`);
712
986
  }
713
- const dkgCompletePromise = this.awaitPush(CoordinatorMessageType.DkgComplete);
714
- await this.request(CoordinatorMessageType.DkgRound2, {
715
- package: aliceRound2Bytes
987
+ await onDelegateShareReady?.({
988
+ swapId: context.swapId,
989
+ delegateShare: new Uint8Array(aliceKeyHandle.secretShareBytes())
716
990
  });
991
+ const dkgCompletePromise = this.awaitPush(CoordinatorMessageType.DkgComplete, context.swapId);
992
+ void dkgCompletePromise.catch(() => void 0);
993
+ await this.request(
994
+ CoordinatorMessageType.DkgRound2,
995
+ {
996
+ package: aliceRound2Bytes,
997
+ resume_secret: context.resumeSecret
998
+ },
999
+ { swapId: context.swapId }
1000
+ );
717
1001
  const completeEnvelope = await dkgCompletePromise;
718
1002
  const complete = readDkgComplete(protocol, completeEnvelope);
719
1003
  const alicePubkey = aliceKeyHandle.groupPublicKeyBytes();
@@ -733,6 +1017,20 @@ var UserCoordinatorSession = class {
733
1017
  };
734
1018
  }
735
1019
  async openSession() {
1020
+ if (this.hasUsableSession()) return;
1021
+ if (this.openingSession !== null) {
1022
+ await this.openingSession;
1023
+ return;
1024
+ }
1025
+ const opening = this.openSessionOnce();
1026
+ this.openingSession = opening;
1027
+ try {
1028
+ await opening;
1029
+ } finally {
1030
+ if (this.openingSession === opening) this.openingSession = null;
1031
+ }
1032
+ }
1033
+ async openSessionOnce() {
736
1034
  if (this.hasUsableSession()) return;
737
1035
  if (this.session !== null) {
738
1036
  this.detachSession(
@@ -758,11 +1056,6 @@ var UserCoordinatorSession = class {
758
1056
  this.log(`connected to ${this.coordinator.endpoints[0]?.wsUrl ?? "coordinator"}`, "info");
759
1057
  this.log("attestation verified", "info");
760
1058
  }
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
1059
  hasUsableSession() {
767
1060
  if (this.session === null) return false;
768
1061
  const state = getSessionState(this.session);
@@ -788,30 +1081,43 @@ var UserCoordinatorSession = class {
788
1081
  handleEnvelope(env) {
789
1082
  const protocol = this.requireProtocol();
790
1083
  this.log(`<- ${env.type} kind=${env.kind}`, "in");
1084
+ const context = env.swap_id ? this.swapContexts.get(env.swap_id) ?? null : null;
1085
+ if (env.swap_id === null && env.kind !== EnvelopeKind.ERROR) {
1086
+ this.log(`ignored ${env.type} without an explicit swap_id`, "info");
1087
+ return;
1088
+ }
1089
+ if (env.swap_id !== null && context === null) {
1090
+ this.log(`ignored ${env.type} for unknown swap ${env.swap_id}`, "info");
1091
+ return;
1092
+ }
791
1093
  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);
1094
+ const payoutMatchesCurrentSwap = payoutExecuted !== null && context !== null;
793
1095
  const refundExecuted = env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.WithdrawExecuted ? protocol.asWithdrawExecutedPayload(env) : null;
794
1096
  if (this.shouldBufferDepositConfirmed(env)) {
795
1097
  this.bufferDepositConfirmed(env);
796
1098
  return;
797
1099
  }
798
- const foreignPayout = payoutExecuted !== null && !payoutMatchesCurrentSwap;
799
- const next = foreignPayout ? null : protocol.nextState(
800
- this.swapState,
1100
+ const stateForEnvelope = context?.swapState ?? this.swapState;
1101
+ const next = payoutExecuted !== null && !payoutMatchesCurrentSwap ? null : protocol.nextState(
1102
+ stateForEnvelope,
801
1103
  env.kind,
802
1104
  env.type,
803
1105
  payoutExecuted ? { payoutFinal: payoutExecuted.final } : void 0
804
1106
  );
805
1107
  if (env.kind === EnvelopeKind.PUSH && next === null) {
806
1108
  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}`,
1109
+ `ignored ${env.type} for swap ${env.swap_id ?? "null"} while client context is ${context?.swapId ?? "none"} in state ${stateForEnvelope}`,
808
1110
  "info"
809
1111
  );
810
1112
  return;
811
1113
  }
812
1114
  if (next !== null) {
813
- this.swapState = next;
814
- this.emitStateChange();
1115
+ if (context) {
1116
+ this.updateContextState(context, next);
1117
+ } else {
1118
+ this.swapState = next;
1119
+ this.emitStateChange(this.swapId, next);
1120
+ }
815
1121
  }
816
1122
  if (env.kind === EnvelopeKind.ERROR) {
817
1123
  const coordinatorError = coordinatorErrorFromEnvelope(protocol, env);
@@ -823,26 +1129,25 @@ var UserCoordinatorSession = class {
823
1129
  return;
824
1130
  }
825
1131
  if (env.kind === EnvelopeKind.PUSH) {
826
- if (payoutExecuted && payoutMatchesCurrentSwap) {
827
- this.emitPayoutExecuted(payoutExecuted, env.swap_id ?? this.swapId);
1132
+ if (payoutExecuted && payoutMatchesCurrentSwap && context) {
1133
+ this.emitPayoutExecuted(payoutExecuted, context.swapId);
828
1134
  }
829
- if (refundExecuted) {
830
- this.emitRefundExecuted(refundExecuted, env.swap_id ?? this.swapId);
1135
+ if (refundExecuted && context) {
1136
+ this.emitRefundExecuted(refundExecuted, context.swapId);
831
1137
  }
832
1138
  this.resolvePushListener(env);
833
1139
  }
834
1140
  }
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();
1141
+ applySwapStatus(context, status, coordinatorTimestampMs) {
1142
+ const keepTerminalState = TERMINAL_SWAP_STATES.has(context.swapState) && status.state !== context.swapState;
1143
+ if (!keepTerminalState && context.swapState !== status.state) {
1144
+ this.updateContextState(context, status.state);
840
1145
  }
841
1146
  for (const fragment of status.fragments_executed) {
842
- this.emitPayoutExecuted(fragment, this.swapId);
1147
+ this.emitPayoutExecuted(fragment, context.swapId);
843
1148
  }
844
1149
  if (status.withdraw_execution) {
845
- this.emitRefundExecuted(status.withdraw_execution, this.swapId);
1150
+ this.emitRefundExecuted(status.withdraw_execution, context.swapId);
846
1151
  }
847
1152
  if (status.actor_sync !== void 0) {
848
1153
  for (const listener of this.normalUserActorSyncListeners) {
@@ -853,8 +1158,7 @@ var UserCoordinatorSession = class {
853
1158
  emitPayoutExecuted(payload, swapId) {
854
1159
  if (!swapId) return;
855
1160
  const key = `${swapId}:${payload.index}:${payload.tx_signature}`;
856
- if (this.emittedPayoutKeys.has(key)) return;
857
- this.emittedPayoutKeys.add(key);
1161
+ if (!this.rememberEventKey(this.emittedPayoutKeys, key)) return;
858
1162
  this.log(
859
1163
  `payout executed tx=${payload.tx_signature} amount=${payload.amount_lamports} final=${payload.final}`,
860
1164
  "in"
@@ -866,88 +1170,133 @@ var UserCoordinatorSession = class {
866
1170
  emitRefundExecuted(payload, swapId) {
867
1171
  if (!swapId) return;
868
1172
  const key = `${swapId}:${payload.tx_signature}`;
869
- if (this.emittedRefundKeys.has(key)) return;
870
- this.emittedRefundKeys.add(key);
1173
+ if (!this.rememberEventKey(this.emittedRefundKeys, key)) return;
871
1174
  this.log(`refund executed tx=${payload.tx_signature} amount=${payload.amount_lamports}`, "in");
872
1175
  for (const listener of this.refundExecutedListeners) {
873
1176
  listener(payload, swapId);
874
1177
  }
875
1178
  }
876
- request(type, payload, timeoutMs) {
1179
+ rememberEventKey(keys, key) {
1180
+ if (keys.has(key)) return false;
1181
+ keys.add(key);
1182
+ if (keys.size > MAX_EVENT_DEDUP_ENTRIES_PER_KIND) {
1183
+ const oldest = keys.values().next().value;
1184
+ if (oldest !== void 0) keys.delete(oldest);
1185
+ }
1186
+ return true;
1187
+ }
1188
+ request(type, payload, options = {}) {
877
1189
  if (this.session === null) {
878
1190
  throw new TransportError("CONNECTION_LOST", "coordinator session is not connected");
879
1191
  }
880
1192
  return requestEnvelope(this.session, type, payload, {
881
- swapId: this.swapId,
882
- timeoutMs,
1193
+ swapId: options.swapId === void 0 ? this.swapId : options.swapId,
1194
+ timeoutMs: options.timeoutMs,
1195
+ rejectMatchingOutOfBandErrors: options.rejectMatchingOutOfBandErrors,
1196
+ requireNonNullResponseSwapId: options.requireNonNullResponseSwapId,
883
1197
  onLog: (message, direction) => this.log(message, direction)
884
1198
  });
885
1199
  }
886
- awaitPush(type, timeoutMs = 3e4) {
1200
+ awaitPush(type, swapId, timeoutMs = 3e4) {
887
1201
  return new Promise((resolve, reject) => {
888
1202
  const timer = timeoutMs === null ? null : setTimeout(() => {
889
- this.pushListeners.delete(type);
1203
+ this.removePushListener(type, listener);
890
1204
  reject(new CommandError("REJECTED_STALE_JOB", `timed out waiting for ${type} push`));
891
1205
  }, timeoutMs);
892
- this.pushListeners.set(type, { resolve, reject, timer });
1206
+ const listener = { swapId, resolve, reject, timer };
1207
+ const listeners = this.pushListeners.get(type) ?? [];
1208
+ listeners.push(listener);
1209
+ this.pushListeners.set(type, listeners);
893
1210
  });
894
1211
  }
895
1212
  rejectAllPushListeners(err) {
896
- for (const listener of this.pushListeners.values()) {
897
- if (listener.timer) clearTimeout(listener.timer);
898
- listener.reject(err);
1213
+ for (const listeners of this.pushListeners.values()) {
1214
+ for (const listener of listeners) {
1215
+ if (listener.timer) clearTimeout(listener.timer);
1216
+ listener.reject(err);
1217
+ }
899
1218
  }
900
1219
  this.pushListeners.clear();
901
- this.bufferDepositConfirmedUntilDelegateAck = false;
902
- this.bufferedDepositConfirmed = null;
1220
+ for (const context of this.swapContexts.values()) {
1221
+ context.bufferDepositConfirmedUntilDelegateAck = false;
1222
+ context.bufferedDepositConfirmed = null;
1223
+ }
903
1224
  }
904
1225
  shouldBufferDepositConfirmed(env) {
905
- return env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.DepositConfirmed && this.bufferDepositConfirmedUntilDelegateAck;
1226
+ const context = env.swap_id ? this.swapContexts.get(env.swap_id) : null;
1227
+ return env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.DepositConfirmed && context?.bufferDepositConfirmedUntilDelegateAck === true;
906
1228
  }
907
1229
  bufferDepositConfirmed(env) {
908
- if (!this.bufferedDepositConfirmed) {
909
- this.bufferedDepositConfirmed = env;
910
- this.clearPushListenerTimer(CoordinatorMessageType.DepositConfirmed);
1230
+ const context = env.swap_id ? this.swapContexts.get(env.swap_id) : null;
1231
+ if (!context) return;
1232
+ if (!context.bufferedDepositConfirmed) {
1233
+ context.bufferedDepositConfirmed = env;
1234
+ this.clearPushListenerTimer(CoordinatorMessageType.DepositConfirmed, context.swapId);
911
1235
  this.log("buffered DepositConfirmed until DelegateAck is verified", "info");
912
1236
  return;
913
1237
  }
914
1238
  this.log("ignored duplicate DepositConfirmed while waiting for DelegateAck", "info");
915
1239
  }
916
- releaseDepositConfirmedGate() {
1240
+ releaseDepositConfirmedGate(context) {
917
1241
  const protocol = this.requireProtocol();
918
- this.bufferDepositConfirmedUntilDelegateAck = false;
919
- const buffered = this.bufferedDepositConfirmed;
1242
+ context.bufferDepositConfirmedUntilDelegateAck = false;
1243
+ const buffered = context.bufferedDepositConfirmed;
920
1244
  if (!buffered) return;
921
- this.bufferedDepositConfirmed = null;
922
- const next = protocol.nextState(this.swapState, buffered.kind, buffered.type);
1245
+ context.bufferedDepositConfirmed = null;
1246
+ const next = protocol.nextState(context.swapState, buffered.kind, buffered.type);
923
1247
  if (next === null) {
924
- this.log(`ignored buffered DepositConfirmed in state ${this.swapState}`, "info");
1248
+ this.log(`ignored buffered DepositConfirmed in state ${context.swapState}`, "info");
925
1249
  return;
926
1250
  }
927
- this.swapState = next;
928
- this.emitStateChange();
1251
+ this.updateContextState(context, next);
929
1252
  this.resolvePushListener(buffered);
930
1253
  }
931
1254
  resolvePushListener(env) {
932
- const listener = this.pushListeners.get(env.type);
933
- if (!listener) return;
934
- this.pushListeners.delete(env.type);
1255
+ const listeners = this.pushListeners.get(env.type);
1256
+ if (!listeners) return;
1257
+ const index = listeners.findIndex(
1258
+ (candidate) => candidate.swapId === void 0 || candidate.swapId === env.swap_id
1259
+ );
1260
+ if (index < 0) return;
1261
+ const listener = listeners.splice(index, 1)[0];
1262
+ if (listeners.length === 0) this.pushListeners.delete(env.type);
935
1263
  if (listener.timer) clearTimeout(listener.timer);
936
1264
  listener.resolve(env);
937
1265
  }
938
- clearPushListenerTimer(type) {
939
- const listener = this.pushListeners.get(type);
1266
+ clearPushListenerTimer(type, swapId) {
1267
+ const listener = this.pushListeners.get(type)?.find((candidate) => candidate.swapId === void 0 || candidate.swapId === swapId);
940
1268
  if (!listener?.timer) return;
941
1269
  clearTimeout(listener.timer);
942
1270
  listener.timer = null;
943
1271
  }
1272
+ removePushListener(type, listener) {
1273
+ const listeners = this.pushListeners.get(type);
1274
+ if (!listeners) return;
1275
+ const index = listeners.indexOf(listener);
1276
+ if (index >= 0) listeners.splice(index, 1);
1277
+ if (listeners.length === 0) this.pushListeners.delete(type);
1278
+ }
1279
+ cancelPushWaitersForSwap(swapId) {
1280
+ for (const [type, listeners] of this.pushListeners.entries()) {
1281
+ const remaining = listeners.filter((listener) => {
1282
+ if (listener.swapId !== swapId) return true;
1283
+ if (listener.timer) clearTimeout(listener.timer);
1284
+ return false;
1285
+ });
1286
+ if (remaining.length === 0) {
1287
+ this.pushListeners.delete(type);
1288
+ } else if (remaining.length !== listeners.length) {
1289
+ this.pushListeners.set(type, remaining);
1290
+ }
1291
+ }
1292
+ }
944
1293
  setSessionStatus(status) {
945
1294
  this.sessionStatus = status;
946
1295
  this.emitStateChange();
947
1296
  }
948
- emitStateChange() {
1297
+ emitStateChange(swapId = this.swapId, swapState = this.swapState) {
949
1298
  for (const listener of this.stateListeners) {
950
- listener(this.swapState, this.sessionStatus);
1299
+ listener(swapState, this.sessionStatus, swapId);
951
1300
  }
952
1301
  }
953
1302
  log(msg, direction) {
@@ -983,28 +1332,42 @@ function toContractRequestResult(prepared) {
983
1332
  delegatedAtMs: prepared.delegatedAtMs,
984
1333
  syncSecret: new Uint8Array(prepared.syncSecret),
985
1334
  syncSecretHex: prepared.syncSecretHex,
1335
+ resumeSecret: new Uint8Array(prepared.resumeSecret),
1336
+ resumeSecretHex: prepared.resumeSecretHex,
986
1337
  recoveryCodeHex: prepared.recoveryCodeHex,
987
1338
  policySnapshot: prepared.policySnapshot,
988
1339
  delegatePolicySnapshot: prepared.delegatePolicySnapshot
989
1340
  };
990
1341
  }
991
1342
  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
1343
  let client = null;
997
1344
  try {
998
- client = new UserCoordinatorSession({
999
- coordinator: getSessionState(session).pool,
1000
- createSession: async () => session
1001
- });
1345
+ client = new UserCoordinatorSession(
1346
+ {
1347
+ coordinator: getSessionState(session).pool,
1348
+ createSession: async () => session
1349
+ },
1350
+ false
1351
+ );
1002
1352
  return await client.contractRequest(params);
1003
1353
  } finally {
1004
- activeFunctionalContractRequests.delete(session);
1005
1354
  client?.releaseWithoutClosing();
1006
1355
  }
1007
1356
  }
1357
+ async function resumePreDelegationOnSession(session, params) {
1358
+ const client = new UserCoordinatorSession(
1359
+ {
1360
+ coordinator: getSessionState(session).pool,
1361
+ createSession: async () => session
1362
+ },
1363
+ false
1364
+ );
1365
+ try {
1366
+ return await client.resumePreDelegation(params);
1367
+ } finally {
1368
+ client.releaseWithoutClosing();
1369
+ }
1370
+ }
1008
1371
  async function requestRefundOnSession(session, params) {
1009
1372
  const client = new UserCoordinatorSession(
1010
1373
  {
@@ -1182,11 +1545,35 @@ async function contractRequest(session, args) {
1182
1545
  ...args.onSyncSecretReady !== void 0 && {
1183
1546
  onSyncSecretReady: args.onSyncSecretReady
1184
1547
  },
1548
+ ...args.resumeSecret !== void 0 && { resumeSecret: args.resumeSecret },
1549
+ ...args.onResumeSecretReady !== void 0 && {
1550
+ onResumeSecretReady: args.onResumeSecretReady
1551
+ },
1552
+ ...args.onSwapAllocated !== void 0 && {
1553
+ onSwapAllocated: args.onSwapAllocated
1554
+ },
1185
1555
  ...args.onRecoveryCodeReady !== void 0 && {
1186
1556
  onRecoveryCodeReady: args.onRecoveryCodeReady
1557
+ },
1558
+ ...args.onDelegateShareReady !== void 0 && {
1559
+ onDelegateShareReady: args.onDelegateShareReady
1187
1560
  }
1188
1561
  });
1189
1562
  }
1563
+ async function resumePreDelegation(session, args) {
1564
+ assertAttested(session);
1565
+ return resumePreDelegationOnSession(session, {
1566
+ swapId: args.swapId,
1567
+ resumeSecret: new Uint8Array(args.resumeSecret),
1568
+ ...args.syncSecret !== void 0 && { syncSecret: new Uint8Array(args.syncSecret) },
1569
+ recoveryCommitment: new Uint8Array(args.recoveryCommitment),
1570
+ policySnapshot: args.policySnapshot,
1571
+ ...args.onDelegateShareReady !== void 0 && {
1572
+ onDelegateShareReady: args.onDelegateShareReady
1573
+ },
1574
+ ...args.delegateShare !== void 0 && { delegateShare: new Uint8Array(args.delegateShare) }
1575
+ });
1576
+ }
1190
1577
  async function sync(session, contractId, syncSecret, timeoutMs) {
1191
1578
  assertAttested(session);
1192
1579
  return requestUserSync(session, contractId, syncSecret, timeoutMs);
@@ -1228,10 +1615,12 @@ async function requestRefund(session, args) {
1228
1615
  assertAttested(session);
1229
1616
  assertRecoveryCode(args.recoveryCode);
1230
1617
  return requestRefundOnSession(session, {
1618
+ ...args.swapId !== void 0 && { swapId: args.swapId },
1619
+ ...args.syncSecret !== void 0 && { syncSecret: new Uint8Array(args.syncSecret) },
1231
1620
  recoveryCode: new Uint8Array(args.recoveryCode)
1232
1621
  });
1233
1622
  }
1234
1623
 
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 };
1624
+ 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
1625
  //# sourceMappingURL=user.js.map
1237
1626
  //# sourceMappingURL=user.js.map