@dynamic-labs-wallet/browser 1.1.33 → 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
@@ -1804,12 +1804,18 @@ const downloadStringAsFile = ({ filename, content, mimeType = 'application/json'
1804
1804
  * The SDK heals (re-syncs local shares from backup) and retries internally.
1805
1805
  *
1806
1806
  * The SSE event-stream layer copies the event's data fields onto the Error
1807
- * object (see createErrorFromEventData), so `code` is available here.
1807
+ * object (see createErrorFromEventData), so `code` is available there. The
1808
+ * HTTP (non-SSE) transport instead rejects with an AxiosError whose server
1809
+ * body carries `code` under `response.data`.
1808
1810
  */ const isStaleClientSharesError = (error)=>{
1809
1811
  if (!error || typeof error !== 'object') {
1810
1812
  return false;
1811
1813
  }
1812
- return error.code === 'STALE_CLIENT_SHARES';
1814
+ if (error.code === 'STALE_CLIENT_SHARES') {
1815
+ return true;
1816
+ }
1817
+ const axiosResponse = error.response;
1818
+ return (axiosResponse == null ? void 0 : axiosResponse.data) != null && axiosResponse.data.code === 'STALE_CLIENT_SHARES';
1813
1819
  };
1814
1820
  /**
1815
1821
  * Detects the server's 409 ROOM_ID_ALREADY_USED rejection. The client
@@ -2594,6 +2600,45 @@ const classifyOne = (error)=>{
2594
2600
  }
2595
2601
  return fallback;
2596
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
+ };
2597
2642
  /**
2598
2643
  * Decides how an operation failure reaches the Datadog logger, whose signature
2599
2644
  * is `(message, context, error)`.
@@ -3029,9 +3074,10 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3029
3074
  * @param accountAddress - The wallet address
3030
3075
  * @param operation - The operation type (must be a valid SignQueueOperation)
3031
3076
  * @param callback - The sign operation to execute
3077
+ * @param options - Optional diagnostics attached to timeout logs
3032
3078
  * @throws Error if operation is not a valid sign queue operation
3033
3079
  * @returns Promise resolving to the sign result
3034
- */ static async queueSignOperation(accountAddress, operation, callback) {
3080
+ */ static async queueSignOperation(accountAddress, operation, callback, options) {
3035
3081
  accountAddress = normalizeAddress(accountAddress);
3036
3082
  // Runtime validation to catch any type assertion bypasses
3037
3083
  if (!isSignQueueOperation(operation)) {
@@ -3054,6 +3100,13 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3054
3100
  let currentPhase = 'queued';
3055
3101
  let phaseStartedAt = Date.now();
3056
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
+ };
3057
3110
  // The task keeps running after the queue timeout rejects `add()`, so keep
3058
3111
  // a reference to observe how it eventually settles.
3059
3112
  let taskPromise;
@@ -3109,7 +3162,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3109
3162
  signQueuePending: signQueue.pending,
3110
3163
  signQueueSize: signQueue.size,
3111
3164
  recoveryInProgress: this.isRecoveryInProgress(accountAddress)
3112
- }, signDetails),
3165
+ }, enclaveContext(), signDetails),
3113
3166
  level: 'warn'
3114
3167
  });
3115
3168
  // The caller already saw an error; record how the orphaned ceremony
@@ -3127,7 +3180,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3127
3180
  operation,
3128
3181
  outcome: 'resolved',
3129
3182
  settledMs: Date.now() - timedOutAt
3130
- }, signDetails),
3183
+ }, enclaveContext(), signDetails),
3131
3184
  level: 'warn'
3132
3185
  });
3133
3186
  }, (lateError)=>{
@@ -3140,7 +3193,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3140
3193
  outcome: 'rejected',
3141
3194
  settledMs: Date.now() - timedOutAt,
3142
3195
  causes: describeCauseChain(lateError)
3143
- }, signDetails),
3196
+ }, enclaveContext(), signDetails),
3144
3197
  level: 'warn'
3145
3198
  });
3146
3199
  });
@@ -3232,6 +3285,46 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3232
3285
  /** Track which wallets are currently executing inside a sign operation.
3233
3286
  * Used to prevent deadlocks when recovery is called from within a sign op. */ WalletQueueManager.walletsWithActiveSignOp = new Map();
3234
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
+
3235
3328
  // A DYNAMIC backup can persist multiple shares (e.g. 2-of-3); the singular
3236
3329
  // externalKeyShareId is a fallback for older single-id shapes.
3237
3330
  const collectDynamicKeyShareIds = (locations)=>{
@@ -3760,6 +3853,11 @@ class DynamicWalletClient {
3760
3853
  });
3761
3854
  });
3762
3855
  }
3856
+ queueSign(accountAddress, task) {
3857
+ return WalletQueueManager.queueSignOperation(accountAddress, core.WalletOperation.SIGN_MESSAGE, task, {
3858
+ getEnclaveSnapshot: this.enclaveSnapshot
3859
+ });
3860
+ }
3763
3861
  connectForwardMPC() {
3764
3862
  const forwardMPCConnectStartedAt = Date.now();
3765
3863
  this.apiClient.forwardMPCClient.connect().then(()=>{
@@ -4661,7 +4759,7 @@ class DynamicWalletClient {
4661
4759
  }
4662
4760
  // Note: the server signing party (wallet-service) selects the ed25519
4663
4761
  // protocol variant from its own persisted EAC, so it is NOT threaded here.
4664
- 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;
4665
4763
  const useHttpTransport = this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT] === true && this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT_SIGNMESSAGE] === true;
4666
4764
  const params = {
4667
4765
  walletId,
@@ -4684,6 +4782,45 @@ class DynamicWalletClient {
4684
4782
  })) : await this.apiClient.signMessage(params);
4685
4783
  return data;
4686
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
+ }
4687
4824
  prepareMessageForForwardMPC({ chainName, message, formattedMessage, signingAlgo, isFormatted }) {
4688
4825
  // BIP340 hash unwrap MUST come first. When `isFormatted=true`, callers wrap
4689
4826
  // the payload in a `MessageHash`; the direct-MPC path then reads
@@ -4772,7 +4909,7 @@ class DynamicWalletClient {
4772
4909
  throw error;
4773
4910
  }
4774
4911
  }
4775
- 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 }) {
4776
4913
  // Reassigned when forward MPC fails and we mint a fresh room for the
4777
4914
  // relay-based fallback below.
4778
4915
  let roomId = initialRoomId;
@@ -4800,7 +4937,7 @@ class DynamicWalletClient {
4800
4937
  isFormatted
4801
4938
  }, this.getTraceContext(traceContext)));
4802
4939
  // Try forward MPC first if enabled, with fallback to relay-based signing on attestation failure
4803
- if (this.forwardMPCEnabled && !this.shouldSkipForwardMPC(chainName)) {
4940
+ if (this.forwardMPCEnabled && !skipForwardMPC && !this.shouldSkipForwardMPC(chainName)) {
4804
4941
  attemptedForwardMPCSign = true;
4805
4942
  const forwardMPCStartedAt = Date.now();
4806
4943
  try {
@@ -5109,7 +5246,7 @@ class DynamicWalletClient {
5109
5246
  }
5110
5247
  //todo: need to modify with imported flag
5111
5248
  async sign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, approvalQuorum, onError, traceContext, bitcoinConfig }) {
5112
- return WalletQueueManager.queueSignOperation(accountAddress, core.WalletOperation.SIGN_MESSAGE, ({ reportPhase, reportDetails })=>this.internalSign({
5249
+ return this.queueSign(accountAddress, ({ reportPhase, reportDetails })=>this.internalSign({
5113
5250
  accountAddress,
5114
5251
  message,
5115
5252
  chainName,
@@ -5128,6 +5265,278 @@ class DynamicWalletClient {
5128
5265
  }));
5129
5266
  }
5130
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
+ /**
5131
5540
  * Reports a sign that spent a long time before reaching the network. Emitted
5132
5541
  * once per slow sign with the phase breakdown, so a stall can be attributed
5133
5542
  * without guessing: a shared dependency shows up as many signs reporting a
@@ -5151,11 +5560,83 @@ class DynamicWalletClient {
5151
5560
  dynamicRequestId
5152
5561
  }, this.getTraceContext(traceContext)));
5153
5562
  }
5154
- 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));
5155
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
+ });
5156
5601
  return undefined;
5157
5602
  }
5158
- 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);
5159
5640
  const roomId = room == null ? void 0 : room.roomId;
5160
5641
  if (roomId) {
5161
5642
  this.logger.info('[DynamicWaasWalletClient] Using cached room', _extends({
@@ -5165,7 +5646,7 @@ class DynamicWalletClient {
5165
5646
  chainName,
5166
5647
  walletId: wallet.walletId,
5167
5648
  roomId
5168
- }, this.getTraceContext(traceContext)));
5649
+ }, logContext));
5169
5650
  }
5170
5651
  return roomId;
5171
5652
  }
@@ -5232,38 +5713,130 @@ class DynamicWalletClient {
5232
5713
  clearTimeout(warningTimer);
5233
5714
  }
5234
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
+ }
5235
5811
  async internalSign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, approvalQuorum, onError, traceContext, bitcoinConfig, onSignPhase, onSignDetails, staleShareRetry = false, roomIdRetry = false }) {
5236
5812
  const dynamicRequestId = uuid.v4();
5237
5813
  onSignDetails == null ? void 0 : onSignDetails({
5238
5814
  dynamicRequestId
5239
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)));
5240
5828
  try {
5241
5829
  // Pre-flight phase boundaries. Nothing reaches the network until
5242
5830
  // serverSign below, so a stall here is invisible in server logs — these
5243
5831
  // timestamps are what attribute it to a phase.
5244
- const preflightStartedAt = Date.now();
5245
- onSignPhase == null ? void 0 : onSignPhase('verifyPassword');
5246
- await this.verifyPassword({
5832
+ const { wallet, derivationPath, clientKeyShares, timings: { startedAt: preflightStartedAt, passwordVerifiedAt, walletLoadedAt, sharesReadyAt } } = await this.prepareWalletForSign({
5247
5833
  accountAddress,
5248
5834
  password,
5249
- signedSessionId
5250
- });
5251
- const passwordVerifiedAt = Date.now();
5252
- onSignPhase == null ? void 0 : onSignPhase('getWallet');
5253
- const wallet = await this.getWallet({
5254
- accountAddress,
5255
- password,
5256
- walletOperation: core.WalletOperation.SIGN_MESSAGE,
5257
- signedSessionId
5835
+ signedSessionId,
5836
+ onSignPhase
5258
5837
  });
5259
- const walletLoadedAt = Date.now();
5260
- const derivationPath = wallet.derivationPath && wallet.derivationPath != '' ? new Uint32Array(Object.values(JSON.parse(wallet.derivationPath))) : undefined;
5261
- // Ensure client key shares exist before hitting the API
5262
- onSignPhase == null ? void 0 : onSignPhase('ensureClientShare');
5263
- const clientKeyShares = await this.ensureClientShare(accountAddress);
5264
- const sharesReadyAt = Date.now();
5265
5838
  onSignPhase == null ? void 0 : onSignPhase('resolveCachedRoom');
5266
- const cachedRoomId = roomIdRetry ? undefined : await this.getCachedRoomForSign(wallet, accountAddress, chainName, traceContext);
5839
+ const cachedRoomId = roomIdRetry ? undefined : await this.getCachedRoomForSign(wallet, accountAddress, chainName, dynamicRequestId, traceContext);
5267
5840
  const roomResolvedAt = Date.now();
5268
5841
  this.logSlowSignPreflight({
5269
5842
  accountAddress,
@@ -5349,9 +5922,7 @@ class DynamicWalletClient {
5349
5922
  operation: 'serverSign'
5350
5923
  }, this.getTraceContext(traceContext)));
5351
5924
  let signature;
5352
- // Set by the clientSign rejection handler below; read in the catch to
5353
- // detect a client-side mismatch masked by a generic SSE race winner.
5354
- let clientSignMismatchError;
5925
+ let getClientSignMismatch = ()=>undefined;
5355
5926
  try {
5356
5927
  // Race clientSign against SSE errors so that server-side failures
5357
5928
  // (e.g. policy violations) immediately reject instead of hanging
@@ -5414,28 +5985,15 @@ class DynamicWalletClient {
5414
5985
  // a stale-local-share failure surfaces, since the server's generic SSE
5415
5986
  // error wins the race and masks it. Schedule recovery regardless of
5416
5987
  // which promise wins or when this rejection settles.
5417
- clientSignPromise.catch((error)=>{
5418
- if (isPublicKeyMismatchError(error)) {
5419
- clientSignMismatchError = error;
5420
- this.logger.warn('[StaleShareCheck] clientSign rejected with public-key mismatch — scheduling share recovery', _extends({
5421
- accountAddress,
5422
- chainName,
5423
- dynamicRequestId
5424
- }, this.getTraceContext(traceContext)));
5425
- void this.healStaleShares({
5426
- accountAddress,
5427
- chainName,
5428
- password,
5429
- signedSessionId,
5430
- walletOperation: core.WalletOperation.SIGN_MESSAGE,
5431
- mfaToken,
5432
- dynamicRequestId,
5433
- traceContext,
5434
- bitcoinConfig,
5435
- trigger: 'clientSign rejection',
5436
- confirmedMismatch: true
5437
- });
5438
- }
5988
+ getClientSignMismatch = this.trackClientSignMismatch(clientSignPromise, {
5989
+ accountAddress,
5990
+ chainName,
5991
+ password,
5992
+ signedSessionId,
5993
+ mfaToken,
5994
+ dynamicRequestId,
5995
+ traceContext,
5996
+ bitcoinConfig
5439
5997
  });
5440
5998
  signature = await Promise.race([
5441
5999
  clientSignPromise,
@@ -5449,23 +6007,17 @@ class DynamicWalletClient {
5449
6007
  if (isStaleClientSharesError(signError) || isRoomIdAlreadyUsedError(signError)) {
5450
6008
  throw signError;
5451
6009
  }
5452
- // Heal before rethrowing so the caller's retry succeeds. Run the
5453
- // ground-truth check whatever the raced error was: a generic SSE
5454
- // error frequently wins the race while the client-side mismatch
5455
- // settles later, so error-string inspection alone is timing-dependent.
5456
- // The check no-ops when shares are fresh.
5457
- await this.healStaleShares({
6010
+ await this.healAfterSignFailure({
6011
+ signError,
6012
+ clientSignMismatch: getClientSignMismatch(),
5458
6013
  accountAddress,
5459
6014
  chainName,
5460
6015
  password,
5461
6016
  signedSessionId,
5462
- walletOperation: core.WalletOperation.SIGN_MESSAGE,
5463
6017
  mfaToken,
5464
6018
  dynamicRequestId,
5465
6019
  traceContext,
5466
- bitcoinConfig,
5467
- trigger: 'sign failure',
5468
- confirmedMismatch: isPublicKeyMismatchError(signError) || clientSignMismatchError !== undefined
6020
+ bitcoinConfig
5469
6021
  });
5470
6022
  throw signError;
5471
6023
  }
@@ -5476,6 +6028,7 @@ class DynamicWalletClient {
5476
6028
  roomId,
5477
6029
  derivationPath,
5478
6030
  isFormatted,
6031
+ dynamicRequestId,
5479
6032
  key: 'sign',
5480
6033
  operation: 'clientSign'
5481
6034
  }, this.getTraceContext(traceContext)));
@@ -5559,15 +6112,21 @@ class DynamicWalletClient {
5559
6112
  roomIdRetry: true
5560
6113
  });
5561
6114
  }
5562
- logError({
5563
- message: 'Error in sign',
5564
- error: error,
5565
- context: _extends({
5566
- accountAddress,
5567
- chainName,
5568
- isFormatted: isFormatted ? 'true' : 'false',
5569
- dynamicRequestId
5570
- }, 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
5571
6130
  });
5572
6131
  throw error;
5573
6132
  }
@@ -7171,9 +7730,16 @@ class DynamicWalletClient {
7171
7730
  accountAddress: wallet.accountAddress
7172
7731
  });
7173
7732
  // Denial arrives as a *later* SSE `error` event, after room_created
7174
- // already resolved below (see internalSign's sseErrorGate).
7733
+ // already resolved below (see internalSign's sseErrorGate). HTTP
7734
+ // transport has no such stream, so a post-init denial surfaces as an
7735
+ // MPC relay timeout there.
7175
7736
  const sseErrorGate = core.createRejectablePromise();
7176
- const data = await this.apiClient.exportKey({
7737
+ const useHttpTransport = this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT] === true && this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT_EXPORT] === true;
7738
+ // Fresh key per invocation: the transparent stale-share retry below is a
7739
+ // NEW ceremony (new shareSetId), so it must not reuse the failed
7740
+ // attempt's key — the dedup would replay that row's terminal error.
7741
+ const idempotencyKey = useHttpTransport ? uuid.v4() : undefined;
7742
+ const exportParams = {
7177
7743
  walletId: wallet.walletId,
7178
7744
  shareSetId: wallet.shareSetId,
7179
7745
  exportId,
@@ -7181,8 +7747,13 @@ class DynamicWalletClient {
7181
7747
  dynamicRequestId,
7182
7748
  mfaToken,
7183
7749
  elevatedAccessToken,
7750
+ traceContext
7751
+ };
7752
+ const data = useHttpTransport ? await this.apiClient.requestExportKeyJson(_extends({}, exportParams, {
7753
+ idempotencyKey: idempotencyKey
7754
+ })) : await this.apiClient.exportKey(_extends({}, exportParams, {
7184
7755
  onError: (error)=>sseErrorGate.reject(error)
7185
- });
7756
+ }));
7186
7757
  this.logger.debug('[DynamicWaasWalletClient] Starting export of private key', _extends({
7187
7758
  accountAddress,
7188
7759
  chainName,
@@ -7190,9 +7761,9 @@ class DynamicWalletClient {
7190
7761
  exportId,
7191
7762
  roomId: data.roomId
7192
7763
  }, this.getTraceContext(traceContext)));
7193
- // Race the MPC ceremony against the SSE error gate opened above so a
7194
- // late policy-violation event aborts immediately rather than waiting
7195
- // out the relay's own timeout.
7764
+ // Race the MPC ceremony against the error gate opened above so a late
7765
+ // policy-violation SSE event aborts immediately rather than waiting out
7766
+ // the relay's own timeout.
7196
7767
  const mpcExportPromise = this.performMPCExport(mpcSigner, reconstructedKeyShare, data.roomId, exportId, accountAddress, chainName);
7197
7768
  // eslint-disable-next-line @typescript-eslint/no-empty-function
7198
7769
  mpcExportPromise.catch(()=>{});
@@ -10659,8 +11230,25 @@ class DynamicWalletClient {
10659
11230
  const MPC_SCHEME_CONFIG = core.MPC_CONFIG[thresholdSignatureScheme];
10660
11231
  return roomType === RoomTypeEnum.RoomTypeEnum.Threshold ? MPC_SCHEME_CONFIG.threshold : MPC_SCHEME_CONFIG.numberOfParties;
10661
11232
  }
10662
- async getRoom(roomType, thresholdSignatureScheme) {
10663
- 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;
10664
11252
  const numberOfParties = this.getNumberOfParties(roomType, thresholdSignatureScheme);
10665
11253
  const consume = async ()=>{
10666
11254
  // Re-read from localStorage rather than the in-memory static so we see rooms
@@ -10673,29 +11261,34 @@ class DynamicWalletClient {
10673
11261
  const schemeRooms = storedRooms[numberOfParties] || [];
10674
11262
  // Filter out expired rooms and remove the next available room.
10675
11263
  const filteredRooms = schemeRooms.filter((room)=>room.expiresAt > now);
10676
- const room = filteredRooms.shift();
11264
+ const taken = filteredRooms.length >= count ? filteredRooms.splice(0, count) : [];
10677
11265
  // Use setRooms() rather than replacing DynamicWalletClient.rooms wholesale.
10678
11266
  // setRooms() only mutates the specific numberOfParties key, so rooms for other
10679
11267
  // party counts and any rooms written in-memory by a concurrent createRooms()
10680
11268
  // call that hasn't flushed to storage yet are preserved. Persistence is also
10681
11269
  // routed through the existing persistRooms() chain to keep writes serialized.
10682
11270
  await this.setRooms(numberOfParties, filteredRooms);
10683
- return room;
11271
+ return taken;
10684
11272
  };
10685
11273
  // Use the Web Locks API for cross-tab mutual exclusion so that two tabs
10686
11274
  // cannot both consume the same cached room. Falls back to a direct call in
10687
11275
  // environments where navigator.locks is unavailable (e.g. Node.js tests).
10688
11276
  const webLocksAvailable = typeof navigator !== 'undefined' && !!navigator.locks;
10689
- 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();
10690
11278
  var _DynamicWalletClient_rooms_numberOfParties_length;
10691
- this.logger.info('[DynamicWaasWalletClient] getRoom result', {
11279
+ this.logger.info('[DynamicWaasWalletClient] getRoom result', _extends({
10692
11280
  key: 'roomCache',
10693
11281
  operation: 'getRoom',
10694
11282
  numberOfParties,
10695
- 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, {
10696
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,
10697
11290
  webLocksAvailable
10698
- });
11291
+ }));
10699
11292
  // If less than or only 1 room left after removing one, trigger async creation for more rooms.
10700
11293
  // The catch prevents an unhandled promise rejection when the API
10701
11294
  // rate-limits this best-effort background call — createRooms already
@@ -10708,7 +11301,7 @@ class DynamicWalletClient {
10708
11301
  roomCount: ROOM_CACHE_COUNT
10709
11302
  }).catch(()=>{});
10710
11303
  }
10711
- return room;
11304
+ return rooms;
10712
11305
  }
10713
11306
  getTraceContext(traceContext) {
10714
11307
  const now = Date.now();
@@ -10813,6 +11406,7 @@ class DynamicWalletClient {
10813
11406
  this.startLoggerContext(authToken);
10814
11407
  }
10815
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;
10816
11410
  if (this.forwardMPCEnabled) {
10817
11411
  this.connectForwardMPC();
10818
11412
  }
@@ -10984,6 +11578,7 @@ exports.resolveLogLevel = resolveLogLevel;
10984
11578
  exports.retryPromise = retryPromise;
10985
11579
  exports.shouldReshareToSameBackups = shouldReshareToSameBackups;
10986
11580
  exports.timeoutPromise = timeoutPromise;
11581
+ exports.trackForwardMpcEnclave = trackForwardMpcEnclave;
10987
11582
  Object.keys(core).forEach(function (k) {
10988
11583
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
10989
11584
  enumerable: true,