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