@dynamic-labs-wallet/browser 1.1.42 → 1.1.43

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/index.cjs CHANGED
@@ -587,6 +587,10 @@ function meetsMinVersion(sdkVersion, { minVersionByNamespace, fallbackMinVersion
587
587
  'invalid_nonce_signature',
588
588
  'invalid_session_signature'
589
589
  ]);
590
+ /** Reasons a fresh nonce fixes, so they may refresh on every attempt, not just once. */ const REFRESHABLE_NONCE_FAILURE_REASONS = new Set([
591
+ 'nonce_already_used',
592
+ 'nonce_not_found_or_expired'
593
+ ]);
590
594
  /** Best-effort extraction of the server error `code`/`message` from a 4xx body. */ const readErrorBody = (error)=>{
591
595
  var _error_response;
592
596
  if (!(error instanceof axios.AxiosError)) {
@@ -617,6 +621,9 @@ function meetsMinVersion(sdkVersion, { minVersionByNamespace, fallbackMinVersion
617
621
  };
618
622
  /** Maps the server error `code`/`message` onto a {@link NonceFailureReason}. */ const classifyNonceFailure = ({ code, message })=>{
619
623
  const haystack = `${code != null ? code : ''} ${message != null ? message : ''}`.toLowerCase();
624
+ if (haystack.includes('not_found_or_expired') || haystack.includes('not found or expired')) {
625
+ return 'nonce_not_found_or_expired';
626
+ }
620
627
  if (haystack.includes('already_used') || haystack.includes('already used') || haystack.includes('consumption failed')) {
621
628
  return 'nonce_already_used';
622
629
  }
@@ -661,7 +668,8 @@ function meetsMinVersion(sdkVersion, { minVersionByNamespace, fallbackMinVersion
661
668
  }
662
669
  /**
663
670
  * Builds a `retryPromise` `shouldRetry` predicate that refreshes a consumed
664
- * single-use nonce via the reverse channel on a replay 400 and retries once.
671
+ * single-use nonce via the reverse channel on a replay 400 and retries once
672
+ * (on every attempt when the body says the nonce was consumed or expired).
665
673
  * The signed session carries a single-use replay nonce: it is valid on its
666
674
  * first use, but once consumed (e.g. by a preceding operation) the server
667
675
  * rejects with a 400, at which point a fresh session is fetched and the call
@@ -673,18 +681,24 @@ function meetsMinVersion(sdkVersion, { minVersionByNamespace, fallbackMinVersion
673
681
  // triggered an `alsoRetry`), so the replay 400 can land on any attempt, not
674
682
  // just the first. Refresh on the first such 400 seen, whichever attempt
675
683
  // it's on, and never again — preserves the single-refresh-per-operation limit.
684
+ // A consumed or expired nonce is the exception: each fresh session may still
685
+ // pop a stale pooled nonce, so it refreshes on every attempt (bounded by the
686
+ // caller's maxAttempts).
676
687
  let hasRefreshed = false;
677
688
  return async (error, attempt)=>{
678
689
  const status = getHttpStatus(error);
690
+ const reason = status === 400 || status === 403 ? classifyNonceFailure(readErrorBody(error)) : undefined;
679
691
  // A 400 is a refreshable replay nonce; a 403 is a session-key
680
692
  // mismatch the server returns to break the refresh loop (DYNT-1282) — logged only
681
693
  // when the body classifies as a genuine session-signature failure so unrelated 403s
682
694
  // (e.g. an exhausted add-signer grant) don't pollute the DYNT-1599 metric.
683
695
  // Only the 400 can actually refresh.
684
- const isSessionSignatureFailure = status === 403 && SESSION_SIGNATURE_FAILURE_REASONS.has(classifyNonceFailure(readErrorBody(error)));
696
+ const isSessionSignatureFailure = status === 403 && reason !== undefined && SESSION_SIGNATURE_FAILURE_REASONS.has(reason);
697
+ const isRefreshableNonceFailure = status === 400 && reason !== undefined && REFRESHABLE_NONCE_FAILURE_REASONS.has(reason);
698
+ const canRefreshNow = isRefreshableNonceFailure || !hasRefreshed;
685
699
  const shouldInstrument = status === 400 || isSessionSignatureFailure;
686
700
  if (shouldInstrument) {
687
- const willRefresh = status === 400 && !hasRefreshed && this.canRefresh && this.supportsReverseChannel();
701
+ const willRefresh = status === 400 && canRefreshNow && this.canRefresh && this.supportsReverseChannel();
688
702
  this.instrumentNonceFailure({
689
703
  error,
690
704
  attempt,
@@ -694,7 +708,7 @@ function meetsMinVersion(sdkVersion, { minVersionByNamespace, fallbackMinVersion
694
708
  logContext
695
709
  });
696
710
  }
697
- if (status === 400 && !hasRefreshed && this.canRefresh) {
711
+ if (status === 400 && canRefreshNow && this.canRefresh) {
698
712
  // canRefresh only proves a local callback exists, not that the host answers it.
699
713
  if (!this.supportsReverseChannel()) {
700
714
  this.logger.warn(`[${operationName}] host SDK version predates the signed-session reverse channel, declining nonce refresh`, _extends({
@@ -901,6 +915,8 @@ const SESSION_KEY_MISMATCH_REASONS = new Set([
901
915
  ]);
902
916
  const NONCE_REASON_TO_BACKUP_REASON = {
903
917
  nonce_already_used: 'nonce_already_used',
918
+ // Same dashboard bucket as before CKS split this code out of nonce_already_used.
919
+ nonce_not_found_or_expired: 'nonce_already_used',
904
920
  invalid_nonce_signature: 'invalid_nonce_signature',
905
921
  invalid_session_signature: 'invalid_session_signature',
906
922
  other: 'signed_session'
@@ -4087,13 +4103,7 @@ class DynamicWalletClient {
4087
4103
  this.restoreWallets()
4088
4104
  ];
4089
4105
  if (this.featureFlags[core.FEATURE_FLAGS.ENABLE_ROOM_CACHE_FLAG] === true) {
4090
- // Restore rooms first, then create rooms if needed (in series, but not blocking other initialization)
4091
- initializePromises.push(this.restoreRooms().then(()=>// assume two of two threshold signature scheme
4092
- this.createRooms({
4093
- roomType: RoomTypeEnum.RoomTypeEnum.Threshold,
4094
- thresholdSignatureScheme: core.ThresholdSignatureScheme.TWO_OF_TWO,
4095
- roomCount: ROOM_CACHE_COUNT
4096
- })));
4106
+ initializePromises.push(this.prefetchRoomPool());
4097
4107
  }
4098
4108
  await Promise.all(initializePromises);
4099
4109
  return {
@@ -4107,6 +4117,25 @@ class DynamicWalletClient {
4107
4117
  };
4108
4118
  }
4109
4119
  }
4120
+ /**
4121
+ * Restores the cached rooms, then creates more if the pool is low. Best effort: the pool is only a
4122
+ * prefetch, and takeRooms() falls back and refills it later, so a failure (e.g. a dropped request)
4123
+ * must not fail init. createRooms already logs its own error.
4124
+ */ async prefetchRoomPool() {
4125
+ try {
4126
+ await this.restoreRooms();
4127
+ // assume two of two threshold signature scheme
4128
+ await this.createRooms({
4129
+ roomType: RoomTypeEnum.RoomTypeEnum.Threshold,
4130
+ thresholdSignatureScheme: core.ThresholdSignatureScheme.TWO_OF_TWO,
4131
+ roomCount: ROOM_CACHE_COUNT
4132
+ });
4133
+ } catch (error) {
4134
+ this.logger.debug('[DynamicWaasWalletClient] Room cache prefetch failed during init', {
4135
+ error
4136
+ });
4137
+ }
4138
+ }
4110
4139
  async serverInitializeKeyGen({ chainName, clientKeygenIds, dynamicRequestId, thresholdSignatureScheme, bitcoinConfig, businessAccountId, idempotencyKey, traceContext, onError, onCeremonyComplete }) {
4111
4140
  // Initialize keygen, create room, and create the wallet account on the server.
4112
4141
  // HTTP transport (flag on + idempotency key present) uses a single JSON
@@ -4831,14 +4860,14 @@ class DynamicWalletClient {
4831
4860
  return data;
4832
4861
  }
4833
4862
  /**
4834
- * The server half of `signAndBroadcast`: asks Dynamic to open the signing rooms and returns the transaction record.
4863
+ * The server half of `signAndBroadcast`: asks Dynamic to open the signing room and returns the transaction record.
4835
4864
  *
4836
4865
  * @param params.walletId - The wallet that signs.
4837
4866
  * @param params.shareSetId - Which of the wallet's key sets to use. Defaults to its active one.
4838
- * @param params.messages - The transaction once per fee attempt, in fee order, as the bytes to sign (RLP hex on EVM, message bytes on Solana). Not hashed.
4839
- * @param params.contexts - What each message does in readable form (chain, recipient, amount), one per message in the same order. Dynamic checks each against the wallet and its policies.
4867
+ * @param params.message - The transaction as the bytes to sign (RLP hex on EVM, message bytes on Solana). Not hashed.
4868
+ * @param params.context - What the message does in readable form (chain, recipient, amount). Dynamic checks it against the wallet and its policies.
4840
4869
  * @param params.broadcastOptions - Solana send settings: other required signers' signatures, and skipping the pre-send dry run.
4841
- * @param params.roomIds - Signing rooms you already opened, one per message. Send all or none; Dynamic opens them when omitted.
4870
+ * @param params.roomId - A signing room you already opened; Dynamic opens one when omitted.
4842
4871
  * @param params.requestedCertainty - How sure to be before calling it confirmed: `fast`, `safe` or `final`, or `chain:` plus a chain-specific value. Defaults to `safe`.
4843
4872
  * @param params.externalReference - Your own label, returned on every event. Not unique.
4844
4873
  * @param params.idempotencyKey - Makes a retry safe: the same key replays the stored answer instead of sending twice.
@@ -4848,15 +4877,15 @@ class DynamicWalletClient {
4848
4877
  * @param params.onError - Called with the failure's message, never the raw error.
4849
4878
  * @param params.dynamicRequestId - Id of this one request, to match client and server logs.
4850
4879
  * @param params.traceContext - Ties this call's logs to the server's, across retries. Created when omitted.
4851
- */ async serverSignAndBroadcast({ walletId, shareSetId, contexts, broadcastOptions, messages, roomIds, requestedCertainty, externalReference, idempotencyKey, mfaToken, elevatedAccessToken, approvalQuorum, onError, dynamicRequestId, traceContext }) {
4852
- // Opens one signing room per message, unless `roomIds` names them, and returns the transaction record.
4880
+ */ async serverSignAndBroadcast({ walletId, shareSetId, context, broadcastOptions, message, roomId, requestedCertainty, externalReference, idempotencyKey, mfaToken, elevatedAccessToken, approvalQuorum, onError, dynamicRequestId, traceContext }) {
4881
+ // Opens a signing room, unless `roomId` names one, and returns the transaction record.
4853
4882
  return await this.apiClient.signAndBroadcast({
4854
4883
  walletId,
4855
4884
  shareSetId,
4856
- contexts: contexts.map((context)=>core.toJsonSafeContext(context)),
4885
+ context: core.toJsonSafeContext(context),
4857
4886
  broadcastOptions,
4858
- messages,
4859
- roomIds,
4887
+ message,
4888
+ roomId,
4860
4889
  isFormatted: false,
4861
4890
  requestedCertainty,
4862
4891
  externalReference,
@@ -5314,12 +5343,12 @@ class DynamicWalletClient {
5314
5343
  }));
5315
5344
  }
5316
5345
  /**
5317
- * Signs a transaction for Dynamic to broadcast, one signing ceremony per message. The signature stays on the server.
5346
+ * Signs a transaction for Dynamic to broadcast. The signature stays on the server.
5318
5347
  *
5319
5348
  * @param params.accountAddress - The wallet's address.
5320
5349
  * @param params.chainName - The wallet's chain, e.g. `EVM`.
5321
- * @param params.messages - The transaction once per fee attempt, in fee order, as the bytes to sign (RLP hex on EVM, message bytes on Solana). Not hashed.
5322
- * @param params.contexts - What each message does in readable form (chain, recipient, amount), one per message in the same order. Dynamic checks each against the wallet and its policies.
5350
+ * @param params.message - The transaction as the bytes to sign (RLP hex on EVM, message bytes on Solana). Not hashed.
5351
+ * @param params.context - What the message does in readable form (chain, recipient, amount). Dynamic checks it against the wallet and its policies.
5323
5352
  * @param params.broadcastOptions - Solana send settings: other required signers' signatures, and skipping the pre-send dry run.
5324
5353
  * @param params.password - The wallet's backup password, if it has one.
5325
5354
  * @param params.signedSessionId - The signed-in session, used to unlock the wallet's key share.
@@ -5332,18 +5361,13 @@ class DynamicWalletClient {
5332
5361
  * @param params.onError - Called with the failure's message, never the raw error.
5333
5362
  * @param params.traceContext - Ties this call's logs to the server's, across retries. Created when omitted.
5334
5363
  * @returns The transaction record. A finished one comes back without signing again.
5335
- * @throws Error when `contexts` and `messages` differ in length.
5336
- */ async signAndBroadcast({ accountAddress, chainName, contexts, broadcastOptions, messages, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, requestedCertainty, externalReference, idempotencyKey = uuid.v4(), onError, traceContext }) {
5337
- // Each fee attempt is one message and one context; a mismatch is the caller's, so it is named here, before any ceremony.
5338
- if (messages.length === 0 || contexts.length !== messages.length) {
5339
- throw new Error(`Sign and broadcast needs one signing context per message, got ${contexts.length} contexts for ${messages.length} messages`);
5340
- }
5341
- return this.queueSign(accountAddress, ({ reportPhase, readingShares })=>this.internalSignAndBroadcast({
5364
+ */ async signAndBroadcast({ accountAddress, chainName, context, broadcastOptions, message, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, requestedCertainty, externalReference, idempotencyKey = uuid.v4(), onError, traceContext }) {
5365
+ return this.queueSign(accountAddress, ({ reportPhase, reportDetails, readingShares })=>this.internalSignAndBroadcast({
5342
5366
  accountAddress,
5343
5367
  chainName,
5344
- contexts,
5368
+ context,
5345
5369
  broadcastOptions,
5346
- messages,
5370
+ message,
5347
5371
  password,
5348
5372
  signedSessionId,
5349
5373
  mfaToken,
@@ -5355,17 +5379,21 @@ class DynamicWalletClient {
5355
5379
  onError,
5356
5380
  traceContext,
5357
5381
  onSignPhase: reportPhase,
5382
+ onSignDetails: reportDetails,
5358
5383
  onReadingShares: readingShares
5359
5384
  }));
5360
5385
  }
5361
5386
  /**
5362
- * Does the work of `signAndBroadcast` once it has its turn in the wallet queue: loads the wallet, asks Dynamic to open the rooms, then signs in each.
5387
+ * Does the work of `signAndBroadcast` once it has its turn in the wallet queue: loads the wallet, asks Dynamic to open the room, then signs in it.
5363
5388
  *
5364
5389
  * Takes the same fields as `signAndBroadcast`.
5365
5390
  *
5366
5391
  * @param params.onSignPhase - Reports which step it is on, so the queue can tell which step used up the time.
5367
- */ async internalSignAndBroadcast({ accountAddress, chainName, contexts, broadcastOptions, messages, password, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, requestedCertainty, externalReference, idempotencyKey, onError, traceContext, onSignPhase, onReadingShares, staleShareRetry = false, roomIdRetry = false }) {
5392
+ */ async internalSignAndBroadcast({ accountAddress, chainName, context, broadcastOptions, message, password, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, requestedCertainty, externalReference, idempotencyKey, onError, traceContext, onSignPhase, onSignDetails, onReadingShares, staleShareRetry = false, roomIdRetry = false }) {
5368
5393
  const dynamicRequestId = uuid.v4();
5394
+ onSignDetails == null ? void 0 : onSignDetails({
5395
+ dynamicRequestId
5396
+ });
5369
5397
  traceContext = core.withTraceId(traceContext);
5370
5398
  let stage = 'prepareWallet';
5371
5399
  let roomIdSource;
@@ -5375,7 +5403,6 @@ class DynamicWalletClient {
5375
5403
  accountAddress,
5376
5404
  chainName,
5377
5405
  dynamicRequestId,
5378
- attemptCount: messages.length,
5379
5406
  staleShareRetry,
5380
5407
  roomIdRetry
5381
5408
  }, this.getTraceContext(traceContext)));
@@ -5389,8 +5416,15 @@ class DynamicWalletClient {
5389
5416
  });
5390
5417
  stage = 'resolveCachedRoom';
5391
5418
  onSignPhase == null ? void 0 : onSignPhase('resolveCachedRoom');
5392
- const roomIds = roomIdRetry ? undefined : await this.getCachedRoomsForSignAndBroadcast(wallet, accountAddress, chainName, messages.length, dynamicRequestId, traceContext);
5393
- roomIdSource = roomIds ? 'cache' : 'server';
5419
+ const cachedRoomId = roomIdRetry ? undefined : await this.getCachedRoom({
5420
+ wallet,
5421
+ accountAddress,
5422
+ chainName,
5423
+ key: 'signAndBroadcast',
5424
+ dynamicRequestId,
5425
+ traceContext
5426
+ });
5427
+ roomIdSource = cachedRoomId ? 'cache' : 'server';
5394
5428
  const roomsResolvedAt = Date.now();
5395
5429
  this.logSlowSignPreflight({
5396
5430
  accountAddress,
@@ -5408,10 +5442,10 @@ class DynamicWalletClient {
5408
5442
  const record = await this.serverSignAndBroadcast({
5409
5443
  walletId: wallet.walletId,
5410
5444
  shareSetId: wallet.shareSetId,
5411
- contexts,
5445
+ context,
5412
5446
  broadcastOptions,
5413
- messages,
5414
- roomIds,
5447
+ message,
5448
+ roomId: cachedRoomId,
5415
5449
  requestedCertainty,
5416
5450
  externalReference,
5417
5451
  idempotencyKey,
@@ -5422,9 +5456,7 @@ class DynamicWalletClient {
5422
5456
  dynamicRequestId,
5423
5457
  traceContext
5424
5458
  });
5425
- var _record_rooms;
5426
- const rooms = (_record_rooms = record.rooms) != null ? _record_rooms : [];
5427
- var _roomIds_length;
5459
+ const room = record.room;
5428
5460
  this.logger.info('[DynamicWaasWalletClient] Sign and broadcast accepted', _extends({
5429
5461
  key: 'signAndBroadcast',
5430
5462
  operation: 'accepted',
@@ -5433,38 +5465,30 @@ class DynamicWalletClient {
5433
5465
  dynamicRequestId,
5434
5466
  transactionId: record.transactionId,
5435
5467
  transactionStatus: record.status,
5436
- attemptCount: messages.length,
5437
- roomIdSource,
5438
- roomsSent: (_roomIds_length = roomIds == null ? void 0 : roomIds.length) != null ? _roomIds_length : 0,
5439
- roomsReturned: rooms.length,
5440
- // A retry of an existing row names that row's rooms, not the ones just sent.
5441
- returnedRoomsMatchSent: roomIds ? roomIds.every((roomId, index)=>{
5442
- var _rooms_index;
5443
- return ((_rooms_index = rooms[index]) == null ? void 0 : _rooms_index.roomId) === roomId;
5444
- }) : undefined
5468
+ roomIdSource
5445
5469
  }, this.getTraceContext(traceContext)));
5446
- // A retry of a finished request returns the record without rooms.
5470
+ // A retry of a finished request returns the record without a room.
5447
5471
  if (record.status !== 'pending_signature') {
5448
5472
  return record;
5449
5473
  }
5450
- if (rooms.length !== messages.length) {
5451
- throw new Error(`Expected ${messages.length} signing rooms, got ${rooms.length} for transaction ${record.transactionId}`);
5474
+ if (!room) {
5475
+ throw new Error(`Expected a signing room for transaction ${record.transactionId}`);
5452
5476
  }
5453
5477
  stage = 'ceremony';
5454
5478
  onSignPhase == null ? void 0 : onSignPhase('ceremony');
5455
- const ceremonies = Promise.all(rooms.map((room, index)=>this.clientSign({
5456
- chainName,
5457
- message: messages[index],
5458
- roomId: room.roomId,
5459
- keyShare: clientKeyShares[0],
5460
- derivationPath,
5461
- isFormatted: false,
5462
- dynamicRequestId,
5463
- traceContext,
5464
- variant: wallet.variant,
5465
- // Nothing mints a replacement room for a broadcast attempt.
5466
- skipForwardMPC: true
5467
- })));
5479
+ const ceremonies = this.clientSign({
5480
+ chainName,
5481
+ message,
5482
+ roomId: room.roomId,
5483
+ keyShare: clientKeyShares[0],
5484
+ derivationPath,
5485
+ isFormatted: false,
5486
+ dynamicRequestId,
5487
+ traceContext,
5488
+ variant: wallet.variant,
5489
+ // Nothing mints a replacement room for a broadcast.
5490
+ skipForwardMPC: true
5491
+ });
5468
5492
  const getClientSignMismatch = this.trackClientSignMismatch(ceremonies, {
5469
5493
  accountAddress,
5470
5494
  chainName,
@@ -5495,7 +5519,7 @@ class DynamicWalletClient {
5495
5519
  this.logger.info('[DynamicWaasWalletClient] Client sign completed', _extends({
5496
5520
  accountAddress,
5497
5521
  chainName,
5498
- roomIds: rooms.map((room)=>room.roomId),
5522
+ roomId: room.roomId,
5499
5523
  dynamicRequestId,
5500
5524
  key: 'signAndBroadcast',
5501
5525
  operation: 'clientSign'
@@ -5505,9 +5529,9 @@ class DynamicWalletClient {
5505
5529
  const retryParams = {
5506
5530
  accountAddress,
5507
5531
  chainName,
5508
- contexts,
5532
+ context,
5509
5533
  broadcastOptions,
5510
- messages,
5534
+ message,
5511
5535
  password,
5512
5536
  signedSessionId,
5513
5537
  mfaToken,
@@ -5519,6 +5543,7 @@ class DynamicWalletClient {
5519
5543
  onError,
5520
5544
  traceContext,
5521
5545
  onSignPhase,
5546
+ onSignDetails,
5522
5547
  onReadingShares
5523
5548
  };
5524
5549
  // The server rejected the local shares as stale before opening any room. Heal them
@@ -5550,9 +5575,9 @@ class DynamicWalletClient {
5550
5575
  }));
5551
5576
  }
5552
5577
  // A cached room was already claimed by an earlier ceremony. Retry once with the same
5553
- // idempotency key and no cached rooms, so the server opens fresh ones.
5578
+ // idempotency key and no cached room, so the server opens a fresh one.
5554
5579
  if (!roomIdRetry && isRoomIdAlreadyUsedError(error)) {
5555
- this.logger.info('[RoomCache] retrying sign and broadcast without cached rooms after 409 ROOM_ID_ALREADY_USED', _extends({
5580
+ this.logger.info('[RoomCache] retrying sign and broadcast without a cached room after 409 ROOM_ID_ALREADY_USED', _extends({
5556
5581
  key: 'signAndBroadcast',
5557
5582
  operation: 'roomIdRetry',
5558
5583
  accountAddress,
@@ -5575,7 +5600,6 @@ class DynamicWalletClient {
5575
5600
  operation: 'signAndBroadcast',
5576
5601
  details: {
5577
5602
  stage,
5578
- attemptCount: messages.length,
5579
5603
  roomIdSource,
5580
5604
  staleShareRetry,
5581
5605
  roomIdRetry
@@ -5613,95 +5637,67 @@ class DynamicWalletClient {
5613
5637
  }, this.getTraceContext(traceContext)));
5614
5638
  }
5615
5639
  /**
5616
- * Takes one already-opened room per message from the local pool, or none: a partial set saves nothing, so the server opens them all instead.
5640
+ * Takes one already-opened room from the local pool for a sign or a sign-and-broadcast, or none when the cache is off or empty, so the server opens one instead.
5617
5641
  *
5618
- * @param wallet - The wallet signing; its key scheme decides which rooms fit.
5619
- * @param accountAddress - The wallet's address, for logs.
5620
- * @param chainName - The wallet's chain, for logs.
5621
- * @param count - How many rooms are needed: one per message.
5622
- * @param dynamicRequestId - Id of this request, so the pool's logs join the sign's.
5623
- * @param traceContext - Trace ids for the same reason.
5624
- * @returns The room ids, or `undefined` when the pool cannot cover every message.
5625
- */ async getCachedRoomsForSignAndBroadcast(wallet, accountAddress, chainName, count, dynamicRequestId, traceContext) {
5642
+ * @param params.wallet - The wallet signing; its key scheme decides which room fits.
5643
+ * @param params.accountAddress - The wallet's address, for logs.
5644
+ * @param params.chainName - The wallet's chain, for logs.
5645
+ * @param params.key - Which operation is asking, for logs.
5646
+ * @param params.dynamicRequestId - Id of this request, so the pool's logs join the operation's.
5647
+ * @param params.traceContext - Trace ids for the same reason.
5648
+ * @returns The room id, or `undefined`.
5649
+ */ async getCachedRoom({ wallet, accountAddress, chainName, key, dynamicRequestId, traceContext }) {
5626
5650
  const logContext = _extends({
5627
5651
  dynamicRequestId
5628
5652
  }, this.getTraceContext(traceContext));
5629
5653
  if (this.featureFlags[core.FEATURE_FLAGS.ENABLE_ROOM_CACHE_FLAG] !== true) {
5630
- this.logCachedRoomsUnavailable({
5654
+ this.logCachedRoomUnavailable({
5631
5655
  reason: 'cache_disabled',
5656
+ key,
5632
5657
  accountAddress,
5633
5658
  chainName,
5634
- count,
5635
5659
  logContext
5636
5660
  });
5637
5661
  return undefined;
5638
5662
  }
5639
- const rooms = await this.takeRooms({
5640
- roomType: RoomTypeEnum.RoomTypeEnum.Threshold,
5641
- thresholdSignatureScheme: wallet.thresholdSignatureScheme,
5642
- count,
5643
- logContext
5644
- });
5645
- if (rooms.length === 0) {
5646
- this.logCachedRoomsUnavailable({
5647
- reason: 'pool_too_small',
5663
+ const room = await this.getRoom(RoomTypeEnum.RoomTypeEnum.Threshold, wallet.thresholdSignatureScheme, logContext);
5664
+ if (!room) {
5665
+ this.logCachedRoomUnavailable({
5666
+ reason: 'pool_empty',
5667
+ key,
5648
5668
  accountAddress,
5649
5669
  chainName,
5650
- count,
5651
5670
  logContext
5652
5671
  });
5653
5672
  return undefined;
5654
5673
  }
5655
- const roomIds = rooms.map((room)=>room.roomId);
5656
- this.logger.info('[DynamicWaasWalletClient] Using cached rooms', _extends({
5657
- key: 'signAndBroadcast',
5658
- operation: 'cachedRoomsFound',
5674
+ this.logger.info('[DynamicWaasWalletClient] Using cached room', _extends({
5675
+ key,
5676
+ operation: 'cachedRoomFound',
5659
5677
  accountAddress,
5660
5678
  chainName,
5661
5679
  walletId: wallet.walletId,
5662
- roomIds
5680
+ roomId: room.roomId
5663
5681
  }, logContext));
5664
- return roomIds;
5682
+ return room.roomId;
5665
5683
  }
5666
5684
  /**
5667
- * Logs why no cached rooms were sent, so how often the cache covers a request can be measured.
5685
+ * Logs why no cached room was sent, so how often the cache covers a request can be measured.
5668
5686
  *
5669
- * @param params.reason - `cache_disabled` or `pool_too_small`.
5687
+ * @param params.reason - `cache_disabled` or `pool_empty`.
5688
+ * @param params.key - Which operation asked.
5670
5689
  * @param params.accountAddress - The wallet's address.
5671
5690
  * @param params.chainName - The wallet's chain.
5672
- * @param params.count - How many rooms were needed.
5673
5691
  * @param params.logContext - Request and trace ids to attach.
5674
- */ logCachedRoomsUnavailable({ reason, accountAddress, chainName, count, logContext }) {
5675
- this.logger.info('[DynamicWaasWalletClient] Cached rooms unavailable; the server opens them', _extends({
5676
- key: 'signAndBroadcast',
5677
- operation: 'cachedRoomsUnavailable',
5692
+ */ logCachedRoomUnavailable({ reason, key, accountAddress, chainName, logContext }) {
5693
+ this.logger.info('[DynamicWaasWalletClient] Cached room unavailable; the server opens one', _extends({
5694
+ key,
5695
+ operation: 'cachedRoomUnavailable',
5678
5696
  reason,
5679
5697
  accountAddress,
5680
- chainName,
5681
- requestedCount: count
5698
+ chainName
5682
5699
  }, logContext));
5683
5700
  }
5684
- async getCachedRoomForSign(wallet, accountAddress, chainName, dynamicRequestId, traceContext) {
5685
- if (this.featureFlags[core.FEATURE_FLAGS.ENABLE_ROOM_CACHE_FLAG] !== true) {
5686
- return undefined;
5687
- }
5688
- const logContext = _extends({
5689
- dynamicRequestId
5690
- }, this.getTraceContext(traceContext));
5691
- const room = await this.getRoom(RoomTypeEnum.RoomTypeEnum.Threshold, wallet.thresholdSignatureScheme, logContext);
5692
- const roomId = room == null ? void 0 : room.roomId;
5693
- if (roomId) {
5694
- this.logger.info('[DynamicWaasWalletClient] Using cached room', _extends({
5695
- key: 'sign',
5696
- operation: 'cachedRoomFound',
5697
- accountAddress,
5698
- chainName,
5699
- walletId: wallet.walletId,
5700
- roomId
5701
- }, logContext));
5702
- }
5703
- return roomId;
5704
- }
5705
5701
  // Without a cached room the client depends on the server reporting one
5706
5702
  // before it can join the ceremony.
5707
5703
  async resolveSignRoom({ cachedRoomId, serverSignPromise, accountAddress, chainName, walletId, dynamicRequestId, traceContext }) {
@@ -5891,7 +5887,14 @@ class DynamicWalletClient {
5891
5887
  onReadingShares
5892
5888
  });
5893
5889
  onSignPhase == null ? void 0 : onSignPhase('resolveCachedRoom');
5894
- const cachedRoomId = roomIdRetry ? undefined : await this.getCachedRoomForSign(wallet, accountAddress, chainName, dynamicRequestId, traceContext);
5890
+ const cachedRoomId = roomIdRetry ? undefined : await this.getCachedRoom({
5891
+ wallet,
5892
+ accountAddress,
5893
+ chainName,
5894
+ key: 'sign',
5895
+ dynamicRequestId,
5896
+ traceContext
5897
+ });
5895
5898
  const roomResolvedAt = Date.now();
5896
5899
  this.logSlowSignPreflight({
5897
5900
  accountAddress,
@@ -11208,6 +11211,17 @@ class DynamicWalletClient {
11208
11211
  }
11209
11212
  this.apiClient.syncAuthToken(authToken);
11210
11213
  }
11214
+ /** Reads the persisted room pool. A corrupt value counts as empty, so the next write replaces it. */ async readStoredRooms() {
11215
+ const rawData = await this.storage.getItem(`${this.storageKey}-rooms`);
11216
+ if (!rawData) {
11217
+ return {};
11218
+ }
11219
+ try {
11220
+ return JSON.parse(rawData);
11221
+ } catch (e) {
11222
+ return {};
11223
+ }
11224
+ }
11211
11225
  async createRooms({ roomType, thresholdSignatureScheme, roomCount = 5 }) {
11212
11226
  const numberOfParties = this.getNumberOfParties(roomType, thresholdSignatureScheme);
11213
11227
  const generation = DynamicWalletClient.roomsGeneration;
@@ -11249,8 +11263,7 @@ class DynamicWalletClient {
11249
11263
  // Re-read from storage under the lock so we don't clobber rooms another
11250
11264
  // tab just wrote, and so we don't lose rooms consumed by getRoom() calls
11251
11265
  // that ran while our API request was in-flight.
11252
- const rawData = await this.storage.getItem(`${this.storageKey}-rooms`);
11253
- const storedRooms = rawData ? JSON.parse(rawData) : {};
11266
+ const storedRooms = await this.readStoredRooms();
11254
11267
  const currentRooms = storedRooms[numberOfParties] || [];
11255
11268
  await this.setRooms(numberOfParties, [
11256
11269
  ...newRooms,
@@ -11367,8 +11380,7 @@ class DynamicWalletClient {
11367
11380
  // already consumed by other tabs. Combined with the Web Locks API below, this
11368
11381
  // makes room consumption atomic across tabs and prevents two tabs from both
11369
11382
  // receiving the same cached room.
11370
- const rawData = await this.storage.getItem(`${this.storageKey}-rooms`);
11371
- const storedRooms = rawData ? JSON.parse(rawData) : {};
11383
+ const storedRooms = await this.readStoredRooms();
11372
11384
  const now = Date.now();
11373
11385
  const schemeRooms = storedRooms[numberOfParties] || [];
11374
11386
  // Filter out expired rooms and remove the next available room.