@dynamic-labs-wallet/browser 1.1.34 → 1.1.35

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
@@ -2600,6 +2600,45 @@ const classifyOne = (error)=>{
2600
2600
  }
2601
2601
  return fallback;
2602
2602
  };
2603
+ /** The HTTP status and backend code of one link in a `cause` chain, or `undefined` when it is not a rejected request. */ const httpFieldsOf = (error)=>{
2604
+ if (error instanceof axios.AxiosError && error.response) {
2605
+ const errorCode = core.extractServerErrorCode(error);
2606
+ return _extends({
2607
+ httpStatus: error.response.status
2608
+ }, errorCode ? {
2609
+ errorCode
2610
+ } : {});
2611
+ }
2612
+ const status = getWalletApiErrorStatus(error);
2613
+ if (status === undefined) return undefined;
2614
+ const { code } = error;
2615
+ return _extends({
2616
+ httpStatus: status
2617
+ }, typeof code === 'string' ? {
2618
+ errorCode: code
2619
+ } : {});
2620
+ };
2621
+ /**
2622
+ * The fields a failure is filtered by: its class, the HTTP status and the
2623
+ * backend's own code when a request was rejected. Walks the `cause` chain so a
2624
+ * chain client's wrapper does not hide the API error beneath it. Scalars only,
2625
+ * so nothing from a response body can carry key material into a log.
2626
+ */ const describeErrorForLog = (error)=>{
2627
+ const errorName = error instanceof Error ? error.name : typeof error;
2628
+ let current = error;
2629
+ for(let depth = 0; current !== undefined && current !== null && depth < MAX_CAUSE_DEPTH; depth += 1){
2630
+ const httpFields = httpFieldsOf(current);
2631
+ if (httpFields) return _extends({
2632
+ errorName
2633
+ }, httpFields);
2634
+ const next = current instanceof Error ? current.cause : undefined;
2635
+ if (next === current) break;
2636
+ current = next;
2637
+ }
2638
+ return {
2639
+ errorName
2640
+ };
2641
+ };
2603
2642
  /**
2604
2643
  * Decides how an operation failure reaches the Datadog logger, whose signature
2605
2644
  * is `(message, context, error)`.
@@ -3035,9 +3074,10 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3035
3074
  * @param accountAddress - The wallet address
3036
3075
  * @param operation - The operation type (must be a valid SignQueueOperation)
3037
3076
  * @param callback - The sign operation to execute
3077
+ * @param options - Optional diagnostics attached to timeout logs
3038
3078
  * @throws Error if operation is not a valid sign queue operation
3039
3079
  * @returns Promise resolving to the sign result
3040
- */ static async queueSignOperation(accountAddress, operation, callback) {
3080
+ */ static async queueSignOperation(accountAddress, operation, callback, options) {
3041
3081
  accountAddress = normalizeAddress(accountAddress);
3042
3082
  // Runtime validation to catch any type assertion bypasses
3043
3083
  if (!isSignQueueOperation(operation)) {
@@ -3060,6 +3100,13 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3060
3100
  let currentPhase = 'queued';
3061
3101
  let phaseStartedAt = Date.now();
3062
3102
  const signDetails = {};
3103
+ const enclaveContext = ()=>{
3104
+ var _options_getEnclaveSnapshot;
3105
+ const forwardMpcEnclave = options == null ? void 0 : (_options_getEnclaveSnapshot = options.getEnclaveSnapshot) == null ? void 0 : _options_getEnclaveSnapshot.call(options);
3106
+ return forwardMpcEnclave ? {
3107
+ forwardMpcEnclave
3108
+ } : {};
3109
+ };
3063
3110
  // The task keeps running after the queue timeout rejects `add()`, so keep
3064
3111
  // a reference to observe how it eventually settles.
3065
3112
  let taskPromise;
@@ -3115,7 +3162,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3115
3162
  signQueuePending: signQueue.pending,
3116
3163
  signQueueSize: signQueue.size,
3117
3164
  recoveryInProgress: this.isRecoveryInProgress(accountAddress)
3118
- }, signDetails),
3165
+ }, enclaveContext(), signDetails),
3119
3166
  level: 'warn'
3120
3167
  });
3121
3168
  // The caller already saw an error; record how the orphaned ceremony
@@ -3133,7 +3180,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3133
3180
  operation,
3134
3181
  outcome: 'resolved',
3135
3182
  settledMs: Date.now() - timedOutAt
3136
- }, signDetails),
3183
+ }, enclaveContext(), signDetails),
3137
3184
  level: 'warn'
3138
3185
  });
3139
3186
  }, (lateError)=>{
@@ -3146,7 +3193,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3146
3193
  outcome: 'rejected',
3147
3194
  settledMs: Date.now() - timedOutAt,
3148
3195
  causes: describeCauseChain(lateError)
3149
- }, signDetails),
3196
+ }, enclaveContext(), signDetails),
3150
3197
  level: 'warn'
3151
3198
  });
3152
3199
  });
@@ -3238,6 +3285,46 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3238
3285
  /** Track which wallets are currently executing inside a sign operation.
3239
3286
  * Used to prevent deadlocks when recovery is called from within a sign op. */ WalletQueueManager.walletsWithActiveSignOp = new Map();
3240
3287
 
3288
+ const FLAP_WINDOW_MS = 60000;
3289
+ // Bounds memory on a socket that flaps indefinitely; the count saturates here.
3290
+ const MAX_TRACKED_DISCONNECTS = 50;
3291
+ // The iframe shares one socket across every chain client; one history per socket.
3292
+ const readers = new WeakMap();
3293
+ /**
3294
+ * Starts recording enclave disconnects and returns a reader for the current
3295
+ * state. Idempotent per client: call it right after creating the socket (before
3296
+ * `connect()`) so startup disconnects are kept, and later calls reuse the same
3297
+ * history instead of stacking listeners.
3298
+ */ const trackForwardMpcEnclave = (client)=>{
3299
+ const existing = readers.get(client);
3300
+ if (existing) {
3301
+ return existing;
3302
+ }
3303
+ const disconnects = [];
3304
+ client.on('disconnected', (closeCode)=>{
3305
+ disconnects.push({
3306
+ at: Date.now(),
3307
+ closeCode
3308
+ });
3309
+ if (disconnects.length > MAX_TRACKED_DISCONNECTS) {
3310
+ disconnects.shift();
3311
+ }
3312
+ });
3313
+ const reader = ()=>{
3314
+ const now = Date.now();
3315
+ const last = disconnects.at(-1);
3316
+ return _extends({
3317
+ connected: client.connected,
3318
+ disconnectsLastMinute: disconnects.filter(({ at })=>now - at <= FLAP_WINDOW_MS).length
3319
+ }, last && {
3320
+ lastCloseCode: last.closeCode,
3321
+ msSinceLastDisconnect: now - last.at
3322
+ });
3323
+ };
3324
+ readers.set(client, reader);
3325
+ return reader;
3326
+ };
3327
+
3241
3328
  // A DYNAMIC backup can persist multiple shares (e.g. 2-of-3); the singular
3242
3329
  // externalKeyShareId is a fallback for older single-id shapes.
3243
3330
  const collectDynamicKeyShareIds = (locations)=>{
@@ -3766,6 +3853,11 @@ class DynamicWalletClient {
3766
3853
  });
3767
3854
  });
3768
3855
  }
3856
+ queueSign(accountAddress, task) {
3857
+ return WalletQueueManager.queueSignOperation(accountAddress, core.WalletOperation.SIGN_MESSAGE, task, {
3858
+ getEnclaveSnapshot: this.enclaveSnapshot
3859
+ });
3860
+ }
3769
3861
  connectForwardMPC() {
3770
3862
  const forwardMPCConnectStartedAt = Date.now();
3771
3863
  this.apiClient.forwardMPCClient.connect().then(()=>{
@@ -4667,7 +4759,7 @@ class DynamicWalletClient {
4667
4759
  }
4668
4760
  // Note: the server signing party (wallet-service) selects the ed25519
4669
4761
  // protocol variant from its own persisted EAC, so it is NOT threaded here.
4670
- const serializedContext = context ? JSON.parse(JSON.stringify(context, (_key, value)=>typeof value === 'bigint' ? String(value) : value)) : undefined;
4762
+ const serializedContext = context ? core.toJsonSafeContext(context) : undefined;
4671
4763
  const useHttpTransport = this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT] === true && this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT_SIGNMESSAGE] === true;
4672
4764
  const params = {
4673
4765
  walletId,
@@ -4690,6 +4782,45 @@ class DynamicWalletClient {
4690
4782
  })) : await this.apiClient.signMessage(params);
4691
4783
  return data;
4692
4784
  }
4785
+ /**
4786
+ * The server half of `signAndBroadcast`: asks Dynamic to open the signing rooms and returns the transaction record.
4787
+ *
4788
+ * @param params.walletId - The wallet that signs.
4789
+ * @param params.shareSetId - Which of the wallet's key sets to use. Defaults to its active one.
4790
+ * @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.
4791
+ * @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.
4792
+ * @param params.broadcastOptions - Solana send settings: other required signers' signatures, and skipping the pre-send dry run.
4793
+ * @param params.roomIds - Signing rooms you already opened, one per message. Send all or none; Dynamic opens them when omitted.
4794
+ * @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`.
4795
+ * @param params.externalReference - Your own label, returned on every event. Not unique.
4796
+ * @param params.idempotencyKey - Makes a retry safe: the same key replays the stored answer instead of sending twice.
4797
+ * @param params.mfaToken - Proof of the user's multi-factor check, when the wallet needs one.
4798
+ * @param params.elevatedAccessToken - Proof of a recent re-login, when the wallet needs one.
4799
+ * @param params.approvalQuorum - Approvers' consent for a governed wallet, sent on the retry after `intent_required`.
4800
+ * @param params.onError - Called with the failure's message, never the raw error.
4801
+ * @param params.dynamicRequestId - Id of this one request, to match client and server logs.
4802
+ * @param params.traceContext - Ties this call's logs to the server's, across retries. Created when omitted.
4803
+ */ async serverSignAndBroadcast({ walletId, shareSetId, contexts, broadcastOptions, messages, roomIds, requestedCertainty, externalReference, idempotencyKey, mfaToken, elevatedAccessToken, approvalQuorum, onError, dynamicRequestId, traceContext }) {
4804
+ // Opens one signing room per message, unless `roomIds` names them, and returns the transaction record.
4805
+ return await this.apiClient.signAndBroadcast({
4806
+ walletId,
4807
+ shareSetId,
4808
+ contexts: contexts.map((context)=>core.toJsonSafeContext(context)),
4809
+ broadcastOptions,
4810
+ messages,
4811
+ roomIds,
4812
+ isFormatted: false,
4813
+ requestedCertainty,
4814
+ externalReference,
4815
+ idempotencyKey,
4816
+ mfaToken,
4817
+ elevatedAccessToken,
4818
+ approvalQuorum,
4819
+ onError,
4820
+ dynamicRequestId,
4821
+ traceContext
4822
+ });
4823
+ }
4693
4824
  prepareMessageForForwardMPC({ chainName, message, formattedMessage, signingAlgo, isFormatted }) {
4694
4825
  // BIP340 hash unwrap MUST come first. When `isFormatted=true`, callers wrap
4695
4826
  // the payload in a `MessageHash`; the direct-MPC path then reads
@@ -4778,7 +4909,7 @@ class DynamicWalletClient {
4778
4909
  throw error;
4779
4910
  }
4780
4911
  }
4781
- async clientSign({ chainName, message, roomId: initialRoomId, keyShare, derivationPath, isFormatted, dynamicRequestId, traceContext, bitcoinConfig, variant, refreshRoom, onForwardMpcFailure }) {
4912
+ async clientSign({ chainName, message, roomId: initialRoomId, keyShare, derivationPath, isFormatted, dynamicRequestId, traceContext, bitcoinConfig, variant, refreshRoom, skipForwardMPC, onForwardMpcFailure }) {
4782
4913
  // Reassigned when forward MPC fails and we mint a fresh room for the
4783
4914
  // relay-based fallback below.
4784
4915
  let roomId = initialRoomId;
@@ -4806,7 +4937,7 @@ class DynamicWalletClient {
4806
4937
  isFormatted
4807
4938
  }, this.getTraceContext(traceContext)));
4808
4939
  // Try forward MPC first if enabled, with fallback to relay-based signing on attestation failure
4809
- if (this.forwardMPCEnabled && !this.shouldSkipForwardMPC(chainName)) {
4940
+ if (this.forwardMPCEnabled && !skipForwardMPC && !this.shouldSkipForwardMPC(chainName)) {
4810
4941
  attemptedForwardMPCSign = true;
4811
4942
  const forwardMPCStartedAt = Date.now();
4812
4943
  try {
@@ -5115,7 +5246,7 @@ class DynamicWalletClient {
5115
5246
  }
5116
5247
  //todo: need to modify with imported flag
5117
5248
  async sign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, approvalQuorum, onError, traceContext, bitcoinConfig }) {
5118
- return WalletQueueManager.queueSignOperation(accountAddress, core.WalletOperation.SIGN_MESSAGE, ({ reportPhase, reportDetails })=>this.internalSign({
5249
+ return this.queueSign(accountAddress, ({ reportPhase, reportDetails })=>this.internalSign({
5119
5250
  accountAddress,
5120
5251
  message,
5121
5252
  chainName,
@@ -5134,6 +5265,278 @@ class DynamicWalletClient {
5134
5265
  }));
5135
5266
  }
5136
5267
  /**
5268
+ * Signs a transaction for Dynamic to broadcast, one signing ceremony per message. The signature stays on the server.
5269
+ *
5270
+ * @param params.accountAddress - The wallet's address.
5271
+ * @param params.chainName - The wallet's chain, e.g. `EVM`.
5272
+ * @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.
5273
+ * @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.
5274
+ * @param params.broadcastOptions - Solana send settings: other required signers' signatures, and skipping the pre-send dry run.
5275
+ * @param params.password - The wallet's backup password, if it has one.
5276
+ * @param params.signedSessionId - The signed-in session, used to unlock the wallet's key share.
5277
+ * @param params.mfaToken - Proof of the user's multi-factor check, when the wallet needs one.
5278
+ * @param params.elevatedAccessToken - Proof of a recent re-login, when the wallet needs one.
5279
+ * @param params.approvalQuorum - Approvers' consent for a governed wallet, sent on the retry after `intent_required`.
5280
+ * @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`.
5281
+ * @param params.externalReference - Your own label, returned on every event. Not unique.
5282
+ * @param params.idempotencyKey - Makes a retry safe: the same key replays the stored answer instead of sending twice. A new one is made when omitted.
5283
+ * @param params.onError - Called with the failure's message, never the raw error.
5284
+ * @param params.traceContext - Ties this call's logs to the server's, across retries. Created when omitted.
5285
+ * @returns The transaction record. A finished one comes back without signing again.
5286
+ * @throws Error when `contexts` and `messages` differ in length.
5287
+ */ async signAndBroadcast({ accountAddress, chainName, contexts, broadcastOptions, messages, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, requestedCertainty, externalReference, idempotencyKey = uuid.v4(), onError, traceContext }) {
5288
+ // Each fee attempt is one message and one context; a mismatch is the caller's, so it is named here, before any ceremony.
5289
+ if (messages.length === 0 || contexts.length !== messages.length) {
5290
+ throw new Error(`Sign and broadcast needs one signing context per message, got ${contexts.length} contexts for ${messages.length} messages`);
5291
+ }
5292
+ return this.queueSign(accountAddress, ({ reportPhase })=>this.internalSignAndBroadcast({
5293
+ accountAddress,
5294
+ chainName,
5295
+ contexts,
5296
+ broadcastOptions,
5297
+ messages,
5298
+ password,
5299
+ signedSessionId,
5300
+ mfaToken,
5301
+ elevatedAccessToken,
5302
+ approvalQuorum,
5303
+ requestedCertainty,
5304
+ externalReference,
5305
+ idempotencyKey,
5306
+ onError,
5307
+ traceContext,
5308
+ onSignPhase: reportPhase
5309
+ }));
5310
+ }
5311
+ /**
5312
+ * 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.
5313
+ *
5314
+ * Takes the same fields as `signAndBroadcast`.
5315
+ *
5316
+ * @param params.onSignPhase - Reports which step it is on, so the queue can tell which step used up the time.
5317
+ */ async internalSignAndBroadcast({ accountAddress, chainName, contexts, broadcastOptions, messages, password, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, requestedCertainty, externalReference, idempotencyKey, onError, traceContext, onSignPhase, staleShareRetry = false, roomIdRetry = false }) {
5318
+ const dynamicRequestId = uuid.v4();
5319
+ traceContext = core.withTraceId(traceContext);
5320
+ let stage = 'prepareWallet';
5321
+ let roomIdSource;
5322
+ this.logger.info('[DynamicWaasWalletClient] Sign and broadcast started', _extends({
5323
+ key: 'signAndBroadcast',
5324
+ operation: 'start',
5325
+ accountAddress,
5326
+ chainName,
5327
+ dynamicRequestId,
5328
+ attemptCount: messages.length,
5329
+ staleShareRetry,
5330
+ roomIdRetry
5331
+ }, this.getTraceContext(traceContext)));
5332
+ try {
5333
+ const { wallet, derivationPath, clientKeyShares, timings: { startedAt, passwordVerifiedAt, walletLoadedAt, sharesReadyAt } } = await this.prepareWalletForSign({
5334
+ accountAddress,
5335
+ password,
5336
+ signedSessionId,
5337
+ onSignPhase
5338
+ });
5339
+ stage = 'resolveCachedRoom';
5340
+ onSignPhase == null ? void 0 : onSignPhase('resolveCachedRoom');
5341
+ const roomIds = roomIdRetry ? undefined : await this.getCachedRoomsForSignAndBroadcast(wallet, accountAddress, chainName, messages.length, dynamicRequestId, traceContext);
5342
+ roomIdSource = roomIds ? 'cache' : 'server';
5343
+ const roomsResolvedAt = Date.now();
5344
+ this.logSlowSignPreflight({
5345
+ accountAddress,
5346
+ chainName,
5347
+ dynamicRequestId,
5348
+ traceContext,
5349
+ verifyPasswordMs: passwordVerifiedAt - startedAt,
5350
+ getWalletMs: walletLoadedAt - passwordVerifiedAt,
5351
+ ensureClientShareMs: sharesReadyAt - walletLoadedAt,
5352
+ resolveCachedRoomMs: roomsResolvedAt - sharesReadyAt,
5353
+ totalMs: roomsResolvedAt - startedAt
5354
+ });
5355
+ stage = 'serverSign';
5356
+ onSignPhase == null ? void 0 : onSignPhase('serverSign');
5357
+ const record = await this.serverSignAndBroadcast({
5358
+ walletId: wallet.walletId,
5359
+ shareSetId: wallet.shareSetId,
5360
+ contexts,
5361
+ broadcastOptions,
5362
+ messages,
5363
+ roomIds,
5364
+ requestedCertainty,
5365
+ externalReference,
5366
+ idempotencyKey,
5367
+ mfaToken,
5368
+ elevatedAccessToken,
5369
+ approvalQuorum,
5370
+ onError,
5371
+ dynamicRequestId,
5372
+ traceContext
5373
+ });
5374
+ var _record_rooms;
5375
+ const rooms = (_record_rooms = record.rooms) != null ? _record_rooms : [];
5376
+ var _roomIds_length;
5377
+ this.logger.info('[DynamicWaasWalletClient] Sign and broadcast accepted', _extends({
5378
+ key: 'signAndBroadcast',
5379
+ operation: 'accepted',
5380
+ accountAddress,
5381
+ chainName,
5382
+ dynamicRequestId,
5383
+ transactionId: record.transactionId,
5384
+ transactionStatus: record.status,
5385
+ attemptCount: messages.length,
5386
+ roomIdSource,
5387
+ roomsSent: (_roomIds_length = roomIds == null ? void 0 : roomIds.length) != null ? _roomIds_length : 0,
5388
+ roomsReturned: rooms.length,
5389
+ // A retry of an existing row names that row's rooms, not the ones just sent.
5390
+ returnedRoomsMatchSent: roomIds ? roomIds.every((roomId, index)=>{
5391
+ var _rooms_index;
5392
+ return ((_rooms_index = rooms[index]) == null ? void 0 : _rooms_index.roomId) === roomId;
5393
+ }) : undefined
5394
+ }, this.getTraceContext(traceContext)));
5395
+ // A retry of a finished request returns the record without rooms.
5396
+ if (record.status !== 'pending_signature') {
5397
+ return record;
5398
+ }
5399
+ if (rooms.length !== messages.length) {
5400
+ throw new Error(`Expected ${messages.length} signing rooms, got ${rooms.length} for transaction ${record.transactionId}`);
5401
+ }
5402
+ stage = 'ceremony';
5403
+ onSignPhase == null ? void 0 : onSignPhase('ceremony');
5404
+ const ceremonies = Promise.all(rooms.map((room, index)=>this.clientSign({
5405
+ chainName,
5406
+ message: messages[index],
5407
+ roomId: room.roomId,
5408
+ keyShare: clientKeyShares[0],
5409
+ derivationPath,
5410
+ isFormatted: false,
5411
+ dynamicRequestId,
5412
+ traceContext,
5413
+ variant: wallet.variant,
5414
+ // Nothing mints a replacement room for a broadcast attempt.
5415
+ skipForwardMPC: true
5416
+ })));
5417
+ const getClientSignMismatch = this.trackClientSignMismatch(ceremonies, {
5418
+ accountAddress,
5419
+ chainName,
5420
+ password,
5421
+ signedSessionId,
5422
+ mfaToken,
5423
+ dynamicRequestId,
5424
+ traceContext
5425
+ });
5426
+ try {
5427
+ await ceremonies;
5428
+ } catch (signError) {
5429
+ stage = 'healAfterFailure';
5430
+ await this.healAfterSignFailure({
5431
+ signError,
5432
+ clientSignMismatch: getClientSignMismatch(),
5433
+ accountAddress,
5434
+ chainName,
5435
+ password,
5436
+ signedSessionId,
5437
+ mfaToken,
5438
+ dynamicRequestId,
5439
+ traceContext
5440
+ });
5441
+ stage = 'ceremony';
5442
+ throw signError;
5443
+ }
5444
+ this.logger.info('[DynamicWaasWalletClient] Client sign completed', _extends({
5445
+ accountAddress,
5446
+ chainName,
5447
+ roomIds: rooms.map((room)=>room.roomId),
5448
+ dynamicRequestId,
5449
+ key: 'signAndBroadcast',
5450
+ operation: 'clientSign'
5451
+ }, this.getTraceContext(traceContext)));
5452
+ return record;
5453
+ } catch (error) {
5454
+ const retryParams = {
5455
+ accountAddress,
5456
+ chainName,
5457
+ contexts,
5458
+ broadcastOptions,
5459
+ messages,
5460
+ password,
5461
+ signedSessionId,
5462
+ mfaToken,
5463
+ elevatedAccessToken,
5464
+ approvalQuorum,
5465
+ requestedCertainty,
5466
+ externalReference,
5467
+ idempotencyKey,
5468
+ onError,
5469
+ traceContext,
5470
+ onSignPhase
5471
+ };
5472
+ // The server rejected the local shares as stale before opening any room. Heal them
5473
+ // from backup and retry once with the same idempotency key, as plain signing does.
5474
+ if (!staleShareRetry && isStaleClientSharesError(error)) {
5475
+ await this.healStaleShares({
5476
+ accountAddress,
5477
+ chainName,
5478
+ password,
5479
+ signedSessionId,
5480
+ walletOperation: core.WalletOperation.SIGN_MESSAGE,
5481
+ mfaToken,
5482
+ dynamicRequestId,
5483
+ traceContext,
5484
+ trigger: 'server 409 stale-shares preflight (sign and broadcast)',
5485
+ confirmedMismatch: true
5486
+ });
5487
+ this.logger.info('[StaleShareCheck] retrying sign and broadcast after 409 stale-shares preflight', _extends({
5488
+ key: 'signAndBroadcast',
5489
+ operation: 'staleShareRetry',
5490
+ accountAddress,
5491
+ chainName,
5492
+ dynamicRequestId,
5493
+ stage
5494
+ }, this.getTraceContext(traceContext)));
5495
+ return await this.internalSignAndBroadcast(_extends({}, retryParams, {
5496
+ staleShareRetry: true,
5497
+ roomIdRetry
5498
+ }));
5499
+ }
5500
+ // A cached room was already claimed by an earlier ceremony. Retry once with the same
5501
+ // idempotency key and no cached rooms, so the server opens fresh ones.
5502
+ if (!roomIdRetry && isRoomIdAlreadyUsedError(error)) {
5503
+ this.logger.info('[RoomCache] retrying sign and broadcast without cached rooms after 409 ROOM_ID_ALREADY_USED', _extends({
5504
+ key: 'signAndBroadcast',
5505
+ operation: 'roomIdRetry',
5506
+ accountAddress,
5507
+ chainName,
5508
+ dynamicRequestId,
5509
+ stage
5510
+ }, this.getTraceContext(traceContext)));
5511
+ return await this.internalSignAndBroadcast(_extends({}, retryParams, {
5512
+ staleShareRetry,
5513
+ roomIdRetry: true
5514
+ }));
5515
+ }
5516
+ this.reportSignError({
5517
+ error,
5518
+ accountAddress,
5519
+ chainName,
5520
+ isFormatted: false,
5521
+ dynamicRequestId,
5522
+ traceContext,
5523
+ operation: 'signAndBroadcast',
5524
+ details: {
5525
+ stage,
5526
+ attemptCount: messages.length,
5527
+ roomIdSource,
5528
+ staleShareRetry,
5529
+ roomIdRetry
5530
+ }
5531
+ });
5532
+ core.attachRequestIds(error, {
5533
+ traceId: traceContext.traceId,
5534
+ requestId: dynamicRequestId
5535
+ });
5536
+ throw error;
5537
+ }
5538
+ }
5539
+ /**
5137
5540
  * Reports a sign that spent a long time before reaching the network. Emitted
5138
5541
  * once per slow sign with the phase breakdown, so a stall can be attributed
5139
5542
  * without guessing: a shared dependency shows up as many signs reporting a
@@ -5157,11 +5560,83 @@ class DynamicWalletClient {
5157
5560
  dynamicRequestId
5158
5561
  }, this.getTraceContext(traceContext)));
5159
5562
  }
5160
- async getCachedRoomForSign(wallet, accountAddress, chainName, traceContext) {
5563
+ /**
5564
+ * 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.
5565
+ *
5566
+ * @param wallet - The wallet signing; its key scheme decides which rooms fit.
5567
+ * @param accountAddress - The wallet's address, for logs.
5568
+ * @param chainName - The wallet's chain, for logs.
5569
+ * @param count - How many rooms are needed: one per message.
5570
+ * @param dynamicRequestId - Id of this request, so the pool's logs join the sign's.
5571
+ * @param traceContext - Trace ids for the same reason.
5572
+ * @returns The room ids, or `undefined` when the pool cannot cover every message.
5573
+ */ async getCachedRoomsForSignAndBroadcast(wallet, accountAddress, chainName, count, dynamicRequestId, traceContext) {
5574
+ const logContext = _extends({
5575
+ dynamicRequestId
5576
+ }, this.getTraceContext(traceContext));
5161
5577
  if (this.featureFlags[core.FEATURE_FLAGS.ENABLE_ROOM_CACHE_FLAG] !== true) {
5578
+ this.logCachedRoomsUnavailable({
5579
+ reason: 'cache_disabled',
5580
+ accountAddress,
5581
+ chainName,
5582
+ count,
5583
+ logContext
5584
+ });
5585
+ return undefined;
5586
+ }
5587
+ const rooms = await this.takeRooms({
5588
+ roomType: RoomTypeEnum.RoomTypeEnum.Threshold,
5589
+ thresholdSignatureScheme: wallet.thresholdSignatureScheme,
5590
+ count,
5591
+ logContext
5592
+ });
5593
+ if (rooms.length === 0) {
5594
+ this.logCachedRoomsUnavailable({
5595
+ reason: 'pool_too_small',
5596
+ accountAddress,
5597
+ chainName,
5598
+ count,
5599
+ logContext
5600
+ });
5162
5601
  return undefined;
5163
5602
  }
5164
- const room = await this.getRoom(RoomTypeEnum.RoomTypeEnum.Threshold, wallet.thresholdSignatureScheme);
5603
+ const roomIds = rooms.map((room)=>room.roomId);
5604
+ this.logger.info('[DynamicWaasWalletClient] Using cached rooms', _extends({
5605
+ key: 'signAndBroadcast',
5606
+ operation: 'cachedRoomsFound',
5607
+ accountAddress,
5608
+ chainName,
5609
+ walletId: wallet.walletId,
5610
+ roomIds
5611
+ }, logContext));
5612
+ return roomIds;
5613
+ }
5614
+ /**
5615
+ * Logs why no cached rooms were sent, so how often the cache covers a request can be measured.
5616
+ *
5617
+ * @param params.reason - `cache_disabled` or `pool_too_small`.
5618
+ * @param params.accountAddress - The wallet's address.
5619
+ * @param params.chainName - The wallet's chain.
5620
+ * @param params.count - How many rooms were needed.
5621
+ * @param params.logContext - Request and trace ids to attach.
5622
+ */ logCachedRoomsUnavailable({ reason, accountAddress, chainName, count, logContext }) {
5623
+ this.logger.info('[DynamicWaasWalletClient] Cached rooms unavailable; the server opens them', _extends({
5624
+ key: 'signAndBroadcast',
5625
+ operation: 'cachedRoomsUnavailable',
5626
+ reason,
5627
+ accountAddress,
5628
+ chainName,
5629
+ requestedCount: count
5630
+ }, logContext));
5631
+ }
5632
+ async getCachedRoomForSign(wallet, accountAddress, chainName, dynamicRequestId, traceContext) {
5633
+ if (this.featureFlags[core.FEATURE_FLAGS.ENABLE_ROOM_CACHE_FLAG] !== true) {
5634
+ return undefined;
5635
+ }
5636
+ const logContext = _extends({
5637
+ dynamicRequestId
5638
+ }, this.getTraceContext(traceContext));
5639
+ const room = await this.getRoom(RoomTypeEnum.RoomTypeEnum.Threshold, wallet.thresholdSignatureScheme, logContext);
5165
5640
  const roomId = room == null ? void 0 : room.roomId;
5166
5641
  if (roomId) {
5167
5642
  this.logger.info('[DynamicWaasWalletClient] Using cached room', _extends({
@@ -5171,7 +5646,7 @@ class DynamicWalletClient {
5171
5646
  chainName,
5172
5647
  walletId: wallet.walletId,
5173
5648
  roomId
5174
- }, this.getTraceContext(traceContext)));
5649
+ }, logContext));
5175
5650
  }
5176
5651
  return roomId;
5177
5652
  }
@@ -5238,38 +5713,130 @@ class DynamicWalletClient {
5238
5713
  clearTimeout(warningTimer);
5239
5714
  }
5240
5715
  }
5716
+ /** Verifies the password, loads the wallet and makes sure its client share exists: the start of every sign. */ async prepareWalletForSign({ accountAddress, password, signedSessionId, onSignPhase }) {
5717
+ const startedAt = Date.now();
5718
+ onSignPhase == null ? void 0 : onSignPhase('verifyPassword');
5719
+ await this.verifyPassword({
5720
+ accountAddress,
5721
+ password,
5722
+ signedSessionId
5723
+ });
5724
+ const passwordVerifiedAt = Date.now();
5725
+ onSignPhase == null ? void 0 : onSignPhase('getWallet');
5726
+ const wallet = await this.getWallet({
5727
+ accountAddress,
5728
+ password,
5729
+ walletOperation: core.WalletOperation.SIGN_MESSAGE,
5730
+ signedSessionId
5731
+ });
5732
+ const walletLoadedAt = Date.now();
5733
+ const derivationPath = wallet.derivationPath && wallet.derivationPath != '' ? new Uint32Array(Object.values(JSON.parse(wallet.derivationPath))) : undefined;
5734
+ onSignPhase == null ? void 0 : onSignPhase('ensureClientShare');
5735
+ const clientKeyShares = await this.ensureClientShare(accountAddress);
5736
+ const sharesReadyAt = Date.now();
5737
+ return {
5738
+ wallet,
5739
+ derivationPath,
5740
+ clientKeyShares,
5741
+ timings: {
5742
+ startedAt,
5743
+ passwordVerifiedAt,
5744
+ walletLoadedAt,
5745
+ sharesReadyAt
5746
+ }
5747
+ };
5748
+ }
5749
+ /**
5750
+ * Watches a ceremony for a client-side ERROR_PUBLIC_KEY_MISMATCH, the only place a stale
5751
+ * local share surfaces when a generic server error wins the race and masks it. Schedules a
5752
+ * heal as soon as it settles and returns a reader for the mismatch.
5753
+ */ trackClientSignMismatch(clientSignPromise, healContext) {
5754
+ let mismatch;
5755
+ clientSignPromise.catch((error)=>{
5756
+ if (isPublicKeyMismatchError(error)) {
5757
+ mismatch = error;
5758
+ this.logger.warn('[StaleShareCheck] clientSign rejected with public-key mismatch — scheduling share recovery', _extends({
5759
+ accountAddress: healContext.accountAddress,
5760
+ chainName: healContext.chainName,
5761
+ dynamicRequestId: healContext.dynamicRequestId
5762
+ }, this.getTraceContext(healContext.traceContext)));
5763
+ void this.healStaleShares(_extends({}, healContext, {
5764
+ walletOperation: core.WalletOperation.SIGN_MESSAGE,
5765
+ trigger: 'clientSign rejection',
5766
+ confirmedMismatch: true
5767
+ }));
5768
+ }
5769
+ });
5770
+ return ()=>mismatch;
5771
+ }
5772
+ /**
5773
+ * Heals stale shares after a failed ceremony so the caller's retry succeeds. The ground-truth
5774
+ * check runs whatever the raced error was, because a generic server error often wins the race
5775
+ * while the client-side mismatch settles later. It no-ops when shares are fresh.
5776
+ */ async healAfterSignFailure(_param) {
5777
+ var { signError, clientSignMismatch } = _param, healContext = _object_without_properties_loose(_param, [
5778
+ "signError",
5779
+ "clientSignMismatch"
5780
+ ]);
5781
+ await this.healStaleShares(_extends({}, healContext, {
5782
+ walletOperation: core.WalletOperation.SIGN_MESSAGE,
5783
+ trigger: 'sign failure',
5784
+ confirmedMismatch: isPublicKeyMismatchError(signError) || clientSignMismatch !== undefined
5785
+ }));
5786
+ }
5787
+ /**
5788
+ * Logs a failed sign or sign-and-broadcast with the same fields on both, so failures can be filtered by operation, HTTP status and backend code.
5789
+ *
5790
+ * @param params.error - What failed.
5791
+ * @param params.accountAddress - The wallet's address.
5792
+ * @param params.chainName - The wallet's chain.
5793
+ * @param params.isFormatted - Whether the message was a hash.
5794
+ * @param params.dynamicRequestId - Id of the failed attempt.
5795
+ * @param params.traceContext - Ids shared by every attempt of this operation.
5796
+ * @param params.operation - Which call failed. Defaults to `sign`.
5797
+ * @param params.details - Where and how it failed, as short values (never request or response bodies).
5798
+ */ reportSignError({ error, accountAddress, chainName, isFormatted, dynamicRequestId, traceContext, operation = 'sign', details }) {
5799
+ logError({
5800
+ message: `Error in ${operation}`,
5801
+ error: error,
5802
+ context: _extends({
5803
+ accountAddress,
5804
+ chainName,
5805
+ isFormatted: isFormatted ? 'true' : 'false',
5806
+ dynamicRequestId,
5807
+ operation
5808
+ }, describeErrorForLog(error), details, this.getTraceContext(traceContext))
5809
+ });
5810
+ }
5241
5811
  async internalSign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, approvalQuorum, onError, traceContext, bitcoinConfig, onSignPhase, onSignDetails, staleShareRetry = false, roomIdRetry = false }) {
5242
5812
  const dynamicRequestId = uuid.v4();
5243
5813
  onSignDetails == null ? void 0 : onSignDetails({
5244
5814
  dynamicRequestId
5245
5815
  });
5816
+ // Retries below reuse this, so every attempt of one sign shares a trace id.
5817
+ traceContext = core.withTraceId(traceContext);
5818
+ this.logger.info('[DynamicWaasWalletClient] Sign started', _extends({
5819
+ key: 'sign',
5820
+ operation: 'start',
5821
+ accountAddress,
5822
+ chainName,
5823
+ isFormatted,
5824
+ dynamicRequestId,
5825
+ staleShareRetry,
5826
+ roomIdRetry
5827
+ }, this.getTraceContext(traceContext)));
5246
5828
  try {
5247
5829
  // Pre-flight phase boundaries. Nothing reaches the network until
5248
5830
  // serverSign below, so a stall here is invisible in server logs — these
5249
5831
  // timestamps are what attribute it to a phase.
5250
- const preflightStartedAt = Date.now();
5251
- onSignPhase == null ? void 0 : onSignPhase('verifyPassword');
5252
- await this.verifyPassword({
5253
- accountAddress,
5254
- password,
5255
- signedSessionId
5256
- });
5257
- const passwordVerifiedAt = Date.now();
5258
- onSignPhase == null ? void 0 : onSignPhase('getWallet');
5259
- const wallet = await this.getWallet({
5832
+ const { wallet, derivationPath, clientKeyShares, timings: { startedAt: preflightStartedAt, passwordVerifiedAt, walletLoadedAt, sharesReadyAt } } = await this.prepareWalletForSign({
5260
5833
  accountAddress,
5261
5834
  password,
5262
- walletOperation: core.WalletOperation.SIGN_MESSAGE,
5263
- signedSessionId
5835
+ signedSessionId,
5836
+ onSignPhase
5264
5837
  });
5265
- const walletLoadedAt = Date.now();
5266
- const derivationPath = wallet.derivationPath && wallet.derivationPath != '' ? new Uint32Array(Object.values(JSON.parse(wallet.derivationPath))) : undefined;
5267
- // Ensure client key shares exist before hitting the API
5268
- onSignPhase == null ? void 0 : onSignPhase('ensureClientShare');
5269
- const clientKeyShares = await this.ensureClientShare(accountAddress);
5270
- const sharesReadyAt = Date.now();
5271
5838
  onSignPhase == null ? void 0 : onSignPhase('resolveCachedRoom');
5272
- const cachedRoomId = roomIdRetry ? undefined : await this.getCachedRoomForSign(wallet, accountAddress, chainName, traceContext);
5839
+ const cachedRoomId = roomIdRetry ? undefined : await this.getCachedRoomForSign(wallet, accountAddress, chainName, dynamicRequestId, traceContext);
5273
5840
  const roomResolvedAt = Date.now();
5274
5841
  this.logSlowSignPreflight({
5275
5842
  accountAddress,
@@ -5355,9 +5922,7 @@ class DynamicWalletClient {
5355
5922
  operation: 'serverSign'
5356
5923
  }, this.getTraceContext(traceContext)));
5357
5924
  let signature;
5358
- // Set by the clientSign rejection handler below; read in the catch to
5359
- // detect a client-side mismatch masked by a generic SSE race winner.
5360
- let clientSignMismatchError;
5925
+ let getClientSignMismatch = ()=>undefined;
5361
5926
  try {
5362
5927
  // Race clientSign against SSE errors so that server-side failures
5363
5928
  // (e.g. policy violations) immediately reject instead of hanging
@@ -5420,28 +5985,15 @@ class DynamicWalletClient {
5420
5985
  // a stale-local-share failure surfaces, since the server's generic SSE
5421
5986
  // error wins the race and masks it. Schedule recovery regardless of
5422
5987
  // which promise wins or when this rejection settles.
5423
- clientSignPromise.catch((error)=>{
5424
- if (isPublicKeyMismatchError(error)) {
5425
- clientSignMismatchError = error;
5426
- this.logger.warn('[StaleShareCheck] clientSign rejected with public-key mismatch — scheduling share recovery', _extends({
5427
- accountAddress,
5428
- chainName,
5429
- dynamicRequestId
5430
- }, this.getTraceContext(traceContext)));
5431
- void this.healStaleShares({
5432
- accountAddress,
5433
- chainName,
5434
- password,
5435
- signedSessionId,
5436
- walletOperation: core.WalletOperation.SIGN_MESSAGE,
5437
- mfaToken,
5438
- dynamicRequestId,
5439
- traceContext,
5440
- bitcoinConfig,
5441
- trigger: 'clientSign rejection',
5442
- confirmedMismatch: true
5443
- });
5444
- }
5988
+ getClientSignMismatch = this.trackClientSignMismatch(clientSignPromise, {
5989
+ accountAddress,
5990
+ chainName,
5991
+ password,
5992
+ signedSessionId,
5993
+ mfaToken,
5994
+ dynamicRequestId,
5995
+ traceContext,
5996
+ bitcoinConfig
5445
5997
  });
5446
5998
  signature = await Promise.race([
5447
5999
  clientSignPromise,
@@ -5455,23 +6007,17 @@ class DynamicWalletClient {
5455
6007
  if (isStaleClientSharesError(signError) || isRoomIdAlreadyUsedError(signError)) {
5456
6008
  throw signError;
5457
6009
  }
5458
- // Heal before rethrowing so the caller's retry succeeds. Run the
5459
- // ground-truth check whatever the raced error was: a generic SSE
5460
- // error frequently wins the race while the client-side mismatch
5461
- // settles later, so error-string inspection alone is timing-dependent.
5462
- // The check no-ops when shares are fresh.
5463
- await this.healStaleShares({
6010
+ await this.healAfterSignFailure({
6011
+ signError,
6012
+ clientSignMismatch: getClientSignMismatch(),
5464
6013
  accountAddress,
5465
6014
  chainName,
5466
6015
  password,
5467
6016
  signedSessionId,
5468
- walletOperation: core.WalletOperation.SIGN_MESSAGE,
5469
6017
  mfaToken,
5470
6018
  dynamicRequestId,
5471
6019
  traceContext,
5472
- bitcoinConfig,
5473
- trigger: 'sign failure',
5474
- confirmedMismatch: isPublicKeyMismatchError(signError) || clientSignMismatchError !== undefined
6020
+ bitcoinConfig
5475
6021
  });
5476
6022
  throw signError;
5477
6023
  }
@@ -5482,6 +6028,7 @@ class DynamicWalletClient {
5482
6028
  roomId,
5483
6029
  derivationPath,
5484
6030
  isFormatted,
6031
+ dynamicRequestId,
5485
6032
  key: 'sign',
5486
6033
  operation: 'clientSign'
5487
6034
  }, this.getTraceContext(traceContext)));
@@ -5565,15 +6112,21 @@ class DynamicWalletClient {
5565
6112
  roomIdRetry: true
5566
6113
  });
5567
6114
  }
5568
- logError({
5569
- message: 'Error in sign',
5570
- error: error,
5571
- context: _extends({
5572
- accountAddress,
5573
- chainName,
5574
- isFormatted: isFormatted ? 'true' : 'false',
5575
- dynamicRequestId
5576
- }, this.getTraceContext(traceContext))
6115
+ this.reportSignError({
6116
+ error,
6117
+ accountAddress,
6118
+ chainName,
6119
+ isFormatted,
6120
+ dynamicRequestId,
6121
+ traceContext,
6122
+ details: {
6123
+ staleShareRetry,
6124
+ roomIdRetry
6125
+ }
6126
+ });
6127
+ core.attachRequestIds(error, {
6128
+ traceId: traceContext == null ? void 0 : traceContext.traceId,
6129
+ requestId: dynamicRequestId
5577
6130
  });
5578
6131
  throw error;
5579
6132
  }
@@ -10677,8 +11230,25 @@ class DynamicWalletClient {
10677
11230
  const MPC_SCHEME_CONFIG = core.MPC_CONFIG[thresholdSignatureScheme];
10678
11231
  return roomType === RoomTypeEnum.RoomTypeEnum.Threshold ? MPC_SCHEME_CONFIG.threshold : MPC_SCHEME_CONFIG.numberOfParties;
10679
11232
  }
10680
- async getRoom(roomType, thresholdSignatureScheme) {
10681
- var _DynamicWalletClient_rooms_numberOfParties;
11233
+ async getRoom(roomType, thresholdSignatureScheme, logContext) {
11234
+ const [room] = await this.takeRooms({
11235
+ roomType,
11236
+ thresholdSignatureScheme,
11237
+ count: 1,
11238
+ logContext
11239
+ });
11240
+ return room;
11241
+ }
11242
+ /**
11243
+ * Takes `count` cached rooms, or none when the pool holds fewer: a caller that needs every room gains nothing from some of them, and would spend them.
11244
+ *
11245
+ * @param params.roomType - Which kind of room.
11246
+ * @param params.thresholdSignatureScheme - The key scheme; it decides how many parties the room needs.
11247
+ * @param params.count - How many rooms to take.
11248
+ * @param params.logContext - Request and trace ids, so the pool's log joins the operation's.
11249
+ * @returns The rooms taken, or an empty list.
11250
+ */ async takeRooms({ roomType, thresholdSignatureScheme, count, logContext }) {
11251
+ var _rooms_, _DynamicWalletClient_rooms_numberOfParties;
10682
11252
  const numberOfParties = this.getNumberOfParties(roomType, thresholdSignatureScheme);
10683
11253
  const consume = async ()=>{
10684
11254
  // Re-read from localStorage rather than the in-memory static so we see rooms
@@ -10691,29 +11261,34 @@ class DynamicWalletClient {
10691
11261
  const schemeRooms = storedRooms[numberOfParties] || [];
10692
11262
  // Filter out expired rooms and remove the next available room.
10693
11263
  const filteredRooms = schemeRooms.filter((room)=>room.expiresAt > now);
10694
- const room = filteredRooms.shift();
11264
+ const taken = filteredRooms.length >= count ? filteredRooms.splice(0, count) : [];
10695
11265
  // Use setRooms() rather than replacing DynamicWalletClient.rooms wholesale.
10696
11266
  // setRooms() only mutates the specific numberOfParties key, so rooms for other
10697
11267
  // party counts and any rooms written in-memory by a concurrent createRooms()
10698
11268
  // call that hasn't flushed to storage yet are preserved. Persistence is also
10699
11269
  // routed through the existing persistRooms() chain to keep writes serialized.
10700
11270
  await this.setRooms(numberOfParties, filteredRooms);
10701
- return room;
11271
+ return taken;
10702
11272
  };
10703
11273
  // Use the Web Locks API for cross-tab mutual exclusion so that two tabs
10704
11274
  // cannot both consume the same cached room. Falls back to a direct call in
10705
11275
  // environments where navigator.locks is unavailable (e.g. Node.js tests).
10706
11276
  const webLocksAvailable = typeof navigator !== 'undefined' && !!navigator.locks;
10707
- const room = webLocksAvailable ? await navigator.locks.request(`${this.storageKey}-room-cache`, consume) : await consume();
11277
+ const rooms = webLocksAvailable ? await navigator.locks.request(`${this.storageKey}-room-cache`, consume) : await consume();
10708
11278
  var _DynamicWalletClient_rooms_numberOfParties_length;
10709
- this.logger.info('[DynamicWaasWalletClient] getRoom result', {
11279
+ this.logger.info('[DynamicWaasWalletClient] getRoom result', _extends({
10710
11280
  key: 'roomCache',
10711
11281
  operation: 'getRoom',
10712
11282
  numberOfParties,
10713
- roomId: room == null ? void 0 : room.roomId,
11283
+ roomId: (_rooms_ = rooms[0]) == null ? void 0 : _rooms_.roomId,
11284
+ requestedCount: count,
11285
+ takenCount: rooms.length
11286
+ }, count > 1 ? {
11287
+ roomIds: rooms.map((room)=>room.roomId)
11288
+ } : {}, logContext, {
10714
11289
  remainingInMemory: (_DynamicWalletClient_rooms_numberOfParties_length = (_DynamicWalletClient_rooms_numberOfParties = DynamicWalletClient.rooms[numberOfParties]) == null ? void 0 : _DynamicWalletClient_rooms_numberOfParties.length) != null ? _DynamicWalletClient_rooms_numberOfParties_length : 0,
10715
11290
  webLocksAvailable
10716
- });
11291
+ }));
10717
11292
  // If less than or only 1 room left after removing one, trigger async creation for more rooms.
10718
11293
  // The catch prevents an unhandled promise rejection when the API
10719
11294
  // rate-limits this best-effort background call — createRooms already
@@ -10726,7 +11301,7 @@ class DynamicWalletClient {
10726
11301
  roomCount: ROOM_CACHE_COUNT
10727
11302
  }).catch(()=>{});
10728
11303
  }
10729
- return room;
11304
+ return rooms;
10730
11305
  }
10731
11306
  getTraceContext(traceContext) {
10732
11307
  const now = Date.now();
@@ -10831,6 +11406,7 @@ class DynamicWalletClient {
10831
11406
  this.startLoggerContext(authToken);
10832
11407
  }
10833
11408
  this.forwardMPCEnabled = this.featureFlags && this.featureFlags[core.FEATURE_FLAGS.ENABLE_FORWARD_MPC_CLIENT_FLAG] === true;
11409
+ this.enclaveSnapshot = this.forwardMPCEnabled ? trackForwardMpcEnclave(this.apiClient.forwardMPCClient) : undefined;
10834
11410
  if (this.forwardMPCEnabled) {
10835
11411
  this.connectForwardMPC();
10836
11412
  }
@@ -11002,6 +11578,7 @@ exports.resolveLogLevel = resolveLogLevel;
11002
11578
  exports.retryPromise = retryPromise;
11003
11579
  exports.shouldReshareToSameBackups = shouldReshareToSameBackups;
11004
11580
  exports.timeoutPromise = timeoutPromise;
11581
+ exports.trackForwardMpcEnclave = trackForwardMpcEnclave;
11005
11582
  Object.keys(core).forEach(function (k) {
11006
11583
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
11007
11584
  enumerable: true,