@invisible-labs/sdk 0.6.0-devnet.4 → 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 (46) hide show
  1. package/README.md +70 -13
  2. package/dist/.invisible-sdk-build-target.json +2 -2
  3. package/dist/{chunk-UADJCUPK.js → chunk-3MDHAS4D.js} +6 -5
  4. package/dist/chunk-3MDHAS4D.js.map +1 -0
  5. package/dist/{chunk-E4DCEDAG.js → chunk-3TGQ4CZ3.js} +178 -2
  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-ZNLGAYXD.js → coordinator-PEWHNZ7I.js} +3 -3
  18. package/dist/{coordinator-ZNLGAYXD.js.map → coordinator-PEWHNZ7I.js.map} +1 -1
  19. package/dist/events.js +3 -3
  20. package/dist/index.d.ts +14 -4
  21. package/dist/index.js +74 -11
  22. package/dist/index.js.map +1 -1
  23. package/dist/lp.d.ts +33 -9
  24. package/dist/lp.js +305 -45
  25. package/dist/lp.js.map +1 -1
  26. package/dist/presets.d.ts +6 -2
  27. package/dist/presets.js +8 -8
  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/{types.generated-DJBTjLpP.d.ts → types.generated-YhFu9ZBR.d.ts} +29 -4
  33. package/dist/user.d.ts +31 -4
  34. package/dist/user.js +409 -57
  35. package/dist/user.js.map +1 -1
  36. package/package.json +4 -1
  37. package/dist/chunk-5NNEO7IK.js +0 -1150
  38. package/dist/chunk-5NNEO7IK.js.map +0 -1
  39. package/dist/chunk-BRDLLGCZ.js +0 -59
  40. package/dist/chunk-BRDLLGCZ.js.map +0 -1
  41. package/dist/chunk-DIA6U2CV.js +0 -1927
  42. package/dist/chunk-DIA6U2CV.js.map +0 -1
  43. package/dist/chunk-E4DCEDAG.js.map +0 -1
  44. package/dist/chunk-GHBQF3A7.js.map +0 -1
  45. package/dist/chunk-UADJCUPK.js.map +0 -1
  46. package/dist/chunk-V5HXJRBW.js.map +0 -1
package/dist/user.js CHANGED
@@ -1,12 +1,13 @@
1
- import { scheduledPayoutWindowSeconds } from './chunk-E4DCEDAG.js';
2
- import { validatePayoutPolicyShape, formatAjvErrors, createSession, createExponentialBackoff } from './chunk-5NNEO7IK.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-UADJCUPK.js';
6
- import { getSessionState } from './chunk-V5HXJRBW.js';
7
- import './chunk-DIA6U2CV.js';
8
- import { PolicyValidationError, CommandError, TransportError, InvalidDepositAmountError, normalizeError, AttestationError, RoutingError } 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-ZNLGAYXD.js');
175
+ protocolPromise ??= import('./coordinator-PEWHNZ7I.js');
175
176
  return protocolPromise;
176
177
  }
177
178
  function loadFrostRuntime() {
@@ -212,6 +213,16 @@ function readDkgComplete(protocol, envelope) {
212
213
  coordinatorTimestampMs: envelope.timestamp_ms
213
214
  };
214
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
+ }
215
226
  function tagBenignTransportDrop(err) {
216
227
  err[BENIGN_TRANSPORT_DROP] = true;
217
228
  return err;
@@ -241,9 +252,12 @@ var UserCoordinatorSession = class {
241
252
  this.refundExecutedListeners = /* @__PURE__ */ new Set();
242
253
  this.normalUserActorSyncListeners = /* @__PURE__ */ new Set();
243
254
  this.coordinatorErrorListeners = /* @__PURE__ */ new Set();
255
+ this.storageErrorListeners = /* @__PURE__ */ new Set();
244
256
  this.emittedPayoutKeys = /* @__PURE__ */ new Set();
245
257
  this.emittedRefundKeys = /* @__PURE__ */ new Set();
246
258
  this.statusReconciliationStops = /* @__PURE__ */ new Map();
259
+ this.pushProcessing = /* @__PURE__ */ new Map();
260
+ this.terminalOutcomes = /* @__PURE__ */ new Map();
247
261
  this.coordinator = options.coordinator;
248
262
  this.createSessionImpl = options.createSession ?? createSession;
249
263
  this.closeSessionAfterOperationFailure = closeSessionAfterRefund;
@@ -272,6 +286,10 @@ var UserCoordinatorSession = class {
272
286
  this.coordinatorErrorListeners.add(listener);
273
287
  return () => this.coordinatorErrorListeners.delete(listener);
274
288
  }
289
+ onStorageError(listener) {
290
+ this.storageErrorListeners.add(listener);
291
+ return () => this.storageErrorListeners.delete(listener);
292
+ }
275
293
  get currentSwapState() {
276
294
  return this.swapState;
277
295
  }
@@ -282,6 +300,9 @@ var UserCoordinatorSession = class {
282
300
  get currentSwapId() {
283
301
  return this.swapId;
284
302
  }
303
+ clientPersistenceState() {
304
+ return this.session ? getSessionState(this.session).clientPersistence : disabledClientPersistence();
305
+ }
285
306
  beginOperation() {
286
307
  this.activeOperations += 1;
287
308
  if (this.activeOperations === 1) this.setSessionStatus("connecting");
@@ -297,6 +318,7 @@ var UserCoordinatorSession = class {
297
318
  if (!context) {
298
319
  context = {
299
320
  swapId,
321
+ requestedAmountLamports: null,
300
322
  swapState: this.swapId === swapId ? this.swapState : "new",
301
323
  syncSecret: this.swapId === swapId && this.syncSecret ? new Uint8Array(this.syncSecret) : null,
302
324
  resumeSecret: this.swapId === swapId && this.resumeSecret ? new Uint8Array(this.resumeSecret) : null,
@@ -321,16 +343,19 @@ var UserCoordinatorSession = class {
321
343
  if (this.swapId === context.swapId) {
322
344
  this.swapState = state;
323
345
  }
324
- this.emitStateChange(context.swapId, state);
325
346
  if (TERMINAL_SWAP_STATES.has(state)) {
326
347
  this.evictSwapContext(context.swapId);
327
348
  }
349
+ this.emitStateChange(context.swapId, state);
328
350
  }
329
351
  evictSwapContext(swapId) {
330
352
  this.statusReconciliationStops.get(swapId)?.();
331
353
  this.statusReconciliationStops.delete(swapId);
332
354
  const context = this.swapContexts.get(swapId);
333
355
  if (context) {
356
+ if (isTerminalSwapState(context.swapState)) {
357
+ this.rememberTerminalOutcome(swapId, context.swapState);
358
+ }
334
359
  context.syncSecret?.fill(0);
335
360
  context.resumeSecret?.fill(0);
336
361
  context.bufferedDepositConfirmed = null;
@@ -347,9 +372,26 @@ var UserCoordinatorSession = class {
347
372
  clearSwapContexts() {
348
373
  for (const swapId of this.swapContexts.keys()) this.evictSwapContext(swapId);
349
374
  this.swapContexts.clear();
375
+ this.pushProcessing.clear();
376
+ this.terminalOutcomes.clear();
350
377
  this.emittedPayoutKeys.clear();
351
378
  this.emittedRefundKeys.clear();
352
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
+ }
353
395
  async runSwap(params) {
354
396
  assertCurrentUserPayoutSpecRuntimeLimits(params.payoutSpec);
355
397
  this.beginOperation();
@@ -433,14 +475,38 @@ var UserCoordinatorSession = class {
433
475
  this.setSessionStatus("running");
434
476
  const context = this.ensureSwapContext(params.swapId);
435
477
  context.swapState = params.delegateShare ? "awaiting_deposit" : "awaiting_dkg";
478
+ context.bufferDepositConfirmedUntilDelegateAck = true;
436
479
  this.setCurrentContext(context);
437
480
  let jointPublicKey;
481
+ let depositAddress;
438
482
  let depositExpiresAtMs;
439
483
  let coordinatorTimestampMs;
440
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
+ }
441
502
  if (!delegateShare) {
442
- const dkg = await this.runDkg(context, params.onDelegateShareReady);
503
+ const dkg = await this.runDkg(
504
+ context,
505
+ params.onDelegateShareReady,
506
+ persistedResumeRecord !== null
507
+ );
443
508
  jointPublicKey = dkg.jointPublicKey;
509
+ depositAddress = solanaAddressFromPublicKeyBytes(jointPublicKey);
444
510
  depositExpiresAtMs = dkg.depositExpiresAtMs;
445
511
  coordinatorTimestampMs = dkg.coordinatorTimestampMs;
446
512
  delegateShare = dkg.keyHandle.secretShareBytes();
@@ -476,8 +542,35 @@ var UserCoordinatorSession = class {
476
542
  "DelegateAck spending_policy does not match persisted policy_snapshot"
477
543
  );
478
544
  }
479
- context.bufferDepositConfirmedUntilDelegateAck = false;
480
- this.updateContextState(context, "awaiting_deposit");
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
+ }
481
574
  completed = true;
482
575
  terminalStatus = "done";
483
576
  return {
@@ -518,6 +611,7 @@ var UserCoordinatorSession = class {
518
611
  this.statusReconciliationStops.clear();
519
612
  }
520
613
  restoreSwapContext(params) {
614
+ if (this.terminalOutcomes.has(params.swapId)) return;
521
615
  if (params.syncSecret !== void 0) {
522
616
  if (params.syncSecret.length !== SYNC_SECRET_BYTE_LENGTH) {
523
617
  throw new CommandError(
@@ -558,7 +652,13 @@ var UserCoordinatorSession = class {
558
652
  }
559
653
  );
560
654
  const status = this.requireProtocol().asSyncUserResponsePayload(env);
561
- this.applySwapStatus(context, 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
+ });
562
662
  return status;
563
663
  }
564
664
  startSwapStatusReconciliation(intervalMs = DEFAULT_SWAP_STATUS_RECONCILE_INTERVAL_MS) {
@@ -675,10 +775,12 @@ var UserCoordinatorSession = class {
675
775
  throw new CommandError("REJECTED_STATE", "syncSecret requires swapId");
676
776
  }
677
777
  this.restoreSwapContext({ swapId, syncSecret: params.syncSecret });
778
+ this.ensureRefundContext(swapId);
678
779
  requestSwapId = swapId;
679
780
  } else if (params.swapId) {
680
781
  const swapId = params.swapId;
681
782
  this.restoreSwapContext({ swapId });
783
+ this.ensureRefundContext(swapId);
682
784
  requestSwapId = swapId;
683
785
  }
684
786
  openedForRefund = !this.hasUsableSession();
@@ -703,9 +805,21 @@ var UserCoordinatorSession = class {
703
805
  }
704
806
  if (env.type === CoordinatorMessageType.WithdrawExecuted) {
705
807
  const resolvedSwapId2 = env.swap_id ?? requestSwapId;
706
- const resolvedContext2 = resolvedSwapId2 ? this.ensureSwapContext(resolvedSwapId2) : null;
707
- if (resolvedContext2) this.setCurrentContext(resolvedContext2);
708
- return { kind: "executed", refund: this.requireProtocol().asWithdrawExecutedPayload(env) };
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
+ );
821
+ }
822
+ return { kind: "executed", refund };
709
823
  }
710
824
  if (env.type !== CoordinatorMessageType.WithdrawAccepted) {
711
825
  throw new CommandError(
@@ -715,10 +829,28 @@ var UserCoordinatorSession = class {
715
829
  }
716
830
  const accepted = this.requireProtocol().asWithdrawAcceptedPayload(env);
717
831
  const resolvedSwapId = env.swap_id ?? requestSwapId;
718
- const resolvedContext = resolvedSwapId ? this.ensureSwapContext(resolvedSwapId) : null;
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
+ }
719
848
  if (!options.waitForFinality) {
720
849
  return { kind: "accepted", accepted };
721
850
  }
851
+ if (terminalOutcome?.refund !== void 0) {
852
+ return { kind: "executed", refund: terminalOutcome.refund };
853
+ }
722
854
  if (!resolvedContext) {
723
855
  throw new CommandError("REJECTED_STALE_JOB", REFUND_PENDING_FINALIZATION_MESSAGE);
724
856
  }
@@ -734,10 +866,21 @@ var UserCoordinatorSession = class {
734
866
  recoveryCode.fill(0);
735
867
  this.endOperation();
736
868
  if (openedForRefund && this.closeSessionAfterOperationFailure && this.activeOperations === 0) {
869
+ await this.waitForPushProcessing(requestSwapId);
737
870
  this.close();
738
871
  }
739
872
  }
740
873
  }
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
+ }
741
884
  async awaitRefundOutcomeFromSwapStatus(context) {
742
885
  const deadline = Date.now() + DEFAULT_REFUND_OUTCOME_POLL_BUDGET_MS;
743
886
  for (; ; ) {
@@ -765,13 +908,11 @@ var UserCoordinatorSession = class {
765
908
  const protocol = this.requireProtocol();
766
909
  const prepared = await this.prepareContract(params);
767
910
  const depositEnv = await prepared.depositConfirmedPromise;
768
- const depositPayload = protocol.asDepositConfirmedPayload(depositEnv);
769
- if (depositPayload.amount_lamports !== params.amountLamports) {
770
- throw new InvalidDepositAmountError(
771
- BigInt(depositPayload.amount_lamports),
772
- BigInt(params.amountLamports)
773
- );
774
- }
911
+ const depositPayload = assertDepositConfirmedAmount(
912
+ protocol,
913
+ depositEnv,
914
+ params.amountLamports
915
+ );
775
916
  this.log(
776
917
  `deposit confirmed tx=${depositPayload.tx_signature} amount=${depositPayload.amount_lamports}`,
777
918
  "in"
@@ -849,6 +990,7 @@ var UserCoordinatorSession = class {
849
990
  const swapId = swapPayload.swap_id;
850
991
  const context = {
851
992
  swapId,
993
+ requestedAmountLamports: params.amountLamports,
852
994
  swapState: "awaiting_dkg",
853
995
  syncSecret: new Uint8Array(syncSecret),
854
996
  resumeSecret: new Uint8Array(resumeSecret),
@@ -857,6 +999,14 @@ var UserCoordinatorSession = class {
857
999
  };
858
1000
  this.swapContexts.set(swapId, context);
859
1001
  this.setCurrentContext(context);
1002
+ await persistTransferAllocation(this.clientPersistenceState(), {
1003
+ swapId,
1004
+ amountLamports: params.amountLamports,
1005
+ payoutSpec: params.payoutSpec,
1006
+ policySnapshot: swapPayload.policy_snapshot,
1007
+ syncSecret,
1008
+ coordinatorTimestampMs: swapResp.timestamp_ms
1009
+ });
860
1010
  await params.onSwapAllocated?.({
861
1011
  swapId,
862
1012
  syncSecretHex,
@@ -881,6 +1031,11 @@ var UserCoordinatorSession = class {
881
1031
  recoveryCommitment = generated.commitment;
882
1032
  try {
883
1033
  recoveryCodeHex = recoveryCodeToHex(generated.code);
1034
+ await persistTransferRecoveryCode(
1035
+ this.clientPersistenceState(),
1036
+ context.swapId,
1037
+ generated.code
1038
+ );
884
1039
  await params.onRecoveryCodeReady?.({ codeHex: recoveryCodeHex });
885
1040
  } finally {
886
1041
  generated.code.fill(0);
@@ -917,8 +1072,22 @@ var UserCoordinatorSession = class {
917
1072
  "DelegateAck spending_policy does not match ContractRequest policy_snapshot"
918
1073
  );
919
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
+ });
920
1089
  this.log(`delegated at ${delegateAck.delegated_at_ms}`, "in");
921
- this.releaseDepositConfirmedGate(context);
1090
+ await this.releaseDepositConfirmedGate(context);
922
1091
  await params.onDepositAddressReady?.({
923
1092
  swapId,
924
1093
  jointPublicKey,
@@ -950,7 +1119,7 @@ var UserCoordinatorSession = class {
950
1119
  }
951
1120
  }
952
1121
  }
953
- async runDkg(context, onDelegateShareReady) {
1122
+ async runDkg(context, onDelegateShareReady, persistDkgMetadata = true) {
954
1123
  await ensureFrostWasm();
955
1124
  const protocol = this.requireProtocol();
956
1125
  const { dkgRound1 } = await loadFrost();
@@ -1007,6 +1176,14 @@ var UserCoordinatorSession = class {
1007
1176
  "DKG joint public key mismatch between Alice and coordinator"
1008
1177
  );
1009
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
+ }
1010
1187
  const pubkeyHexPrefix = Array.from(alicePubkey.slice(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join("");
1011
1188
  this.log(`DKG complete - joint pubkey ${pubkeyHexPrefix}`, "info");
1012
1189
  return {
@@ -1081,6 +1258,18 @@ var UserCoordinatorSession = class {
1081
1258
  handleEnvelope(env) {
1082
1259
  const protocol = this.requireProtocol();
1083
1260
  this.log(`<- ${env.type} kind=${env.kind}`, "in");
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
+ }
1084
1273
  const context = env.swap_id ? this.swapContexts.get(env.swap_id) ?? null : null;
1085
1274
  if (env.swap_id === null && env.kind !== EnvelopeKind.ERROR) {
1086
1275
  this.log(`ignored ${env.type} without an explicit swap_id`, "info");
@@ -1090,27 +1279,8 @@ var UserCoordinatorSession = class {
1090
1279
  this.log(`ignored ${env.type} for unknown swap ${env.swap_id}`, "info");
1091
1280
  return;
1092
1281
  }
1093
- const payoutExecuted = env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.PayoutExecuted ? protocol.asPayoutExecutedPayload(env) : null;
1094
- const payoutMatchesCurrentSwap = payoutExecuted !== null && context !== null;
1095
- const refundExecuted = env.kind === EnvelopeKind.PUSH && env.type === CoordinatorMessageType.WithdrawExecuted ? protocol.asWithdrawExecutedPayload(env) : null;
1096
- if (this.shouldBufferDepositConfirmed(env)) {
1097
- this.bufferDepositConfirmed(env);
1098
- return;
1099
- }
1100
1282
  const stateForEnvelope = context?.swapState ?? this.swapState;
1101
- const next = payoutExecuted !== null && !payoutMatchesCurrentSwap ? null : protocol.nextState(
1102
- stateForEnvelope,
1103
- env.kind,
1104
- env.type,
1105
- payoutExecuted ? { payoutFinal: payoutExecuted.final } : void 0
1106
- );
1107
- if (env.kind === EnvelopeKind.PUSH && next === null) {
1108
- this.log(
1109
- `ignored ${env.type} for swap ${env.swap_id ?? "null"} while client context is ${context?.swapId ?? "none"} in state ${stateForEnvelope}`,
1110
- "info"
1111
- );
1112
- return;
1113
- }
1283
+ const next = protocol.nextState(stateForEnvelope, env.kind, env.type);
1114
1284
  if (next !== null) {
1115
1285
  if (context) {
1116
1286
  this.updateContextState(context, next);
@@ -1128,18 +1298,165 @@ var UserCoordinatorSession = class {
1128
1298
  }
1129
1299
  return;
1130
1300
  }
1131
- if (env.kind === EnvelopeKind.PUSH) {
1132
- if (payoutExecuted && payoutMatchesCurrentSwap && context) {
1133
- this.emitPayoutExecuted(payoutExecuted, context.swapId);
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;
1332
+ if (this.shouldBufferDepositConfirmed(env)) {
1333
+ this.bufferDepositConfirmed(env);
1334
+ return;
1335
+ }
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,
1357
+ env.kind,
1358
+ env.type,
1359
+ payoutExecuted ? { payoutFinal: payoutExecuted.final } : void 0
1360
+ );
1361
+ if (next === null) {
1362
+ this.log(
1363
+ `ignored ${env.type} for swap ${env.swap_id ?? "null"} while client context is ${context.swapId} in state ${context.swapState}`,
1364
+ "info"
1365
+ );
1366
+ return;
1367
+ }
1368
+ if (env.type === CoordinatorMessageType.DelegateAck) {
1369
+ if (!this.isActiveSwapContext(context)) {
1370
+ context.swapState = next;
1371
+ return;
1372
+ }
1373
+ this.updateContextState(context, next);
1374
+ this.settlePushListener(env);
1375
+ return;
1376
+ }
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);
1134
1413
  }
1135
- if (refundExecuted && context) {
1136
- this.emitRefundExecuted(refundExecuted, context.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);
1137
1448
  }
1138
- this.resolvePushListener(env);
1139
1449
  }
1140
1450
  }
1451
+ emitStorageError(error) {
1452
+ for (const listener of this.storageErrorListeners) listener(error);
1453
+ this.log(`client persistence failed: ${normalizeError(error)}`, "info");
1454
+ }
1141
1455
  applySwapStatus(context, status, coordinatorTimestampMs) {
1142
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
+ }
1143
1460
  if (!keepTerminalState && context.swapState !== status.state) {
1144
1461
  this.updateContextState(context, status.state);
1145
1462
  }
@@ -1237,7 +1554,7 @@ var UserCoordinatorSession = class {
1237
1554
  }
1238
1555
  this.log("ignored duplicate DepositConfirmed while waiting for DelegateAck", "info");
1239
1556
  }
1240
- releaseDepositConfirmedGate(context) {
1557
+ async releaseDepositConfirmedGate(context) {
1241
1558
  const protocol = this.requireProtocol();
1242
1559
  context.bufferDepositConfirmedUntilDelegateAck = false;
1243
1560
  const buffered = context.bufferedDepositConfirmed;
@@ -1248,10 +1565,26 @@ var UserCoordinatorSession = class {
1248
1565
  this.log(`ignored buffered DepositConfirmed in state ${context.swapState}`, "info");
1249
1566
  return;
1250
1567
  }
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;
1251
1584
  this.updateContextState(context, next);
1252
- this.resolvePushListener(buffered);
1585
+ this.settlePushListener(buffered);
1253
1586
  }
1254
- resolvePushListener(env) {
1587
+ settlePushListener(env, error) {
1255
1588
  const listeners = this.pushListeners.get(env.type);
1256
1589
  if (!listeners) return;
1257
1590
  const index = listeners.findIndex(
@@ -1261,7 +1594,8 @@ var UserCoordinatorSession = class {
1261
1594
  const listener = listeners.splice(index, 1)[0];
1262
1595
  if (listeners.length === 0) this.pushListeners.delete(env.type);
1263
1596
  if (listener.timer) clearTimeout(listener.timer);
1264
- listener.resolve(env);
1597
+ if (error) listener.reject(error);
1598
+ else listener.resolve(env);
1265
1599
  }
1266
1600
  clearPushListenerTimer(type, swapId) {
1267
1601
  const listener = this.pushListeners.get(type)?.find((candidate) => candidate.swapId === void 0 || candidate.swapId === swapId);
@@ -1420,6 +1754,12 @@ async function requestUserSync(session, swapId, syncSecret, timeoutMs) {
1420
1754
  );
1421
1755
  const payload = (await loadProtocol()).asSyncUserResponsePayload(env);
1422
1756
  assertSyncResponseContractId(env, payload, swapId);
1757
+ await persistTransferStatus(
1758
+ getSessionState(session).clientPersistence,
1759
+ swapId,
1760
+ payload,
1761
+ env.timestamp_ms
1762
+ );
1423
1763
  return {
1424
1764
  ...payload,
1425
1765
  coordinatorTimestampMs: env.timestamp_ms
@@ -1503,6 +1843,18 @@ function isRetryableSwapStatusReconciliationError(err) {
1503
1843
  if (err instanceof AttestationError) return false;
1504
1844
  return err instanceof CommandError && err.code === "REJECTED_STALE_JOB";
1505
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
+ }
1506
1858
  function coordinatorErrorFromEnvelope(protocol, env) {
1507
1859
  try {
1508
1860
  const payload = protocol.asErrorPayload(env);