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