@dynamic-labs-wallet/browser 1.1.28 → 1.1.30

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, handleAxiosError, WalletOperation, ThresholdSignatureScheme, WalletReadyState, classifyForwardMpcError, isRecoveredForwardMpcConnectionChurn, FEATURE_FLAGS, getClientThreshold, MPC_CONFIG, normalizeAndValidatePrivateKey, getTSSConfig, serializeMessageForForwardMPC, isEvmCompatibleChain, serializeError, createRejectablePromise, 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, WalletOperation, ThresholdSignatureScheme, WalletReadyState, classifyForwardMpcError, isRecoveredForwardMpcConnectionChurn, FEATURE_FLAGS, getClientThreshold, MPC_CONFIG, normalizeAndValidatePrivateKey, getTSSConfig, serializeMessageForForwardMPC, isEvmCompatibleChain, serializeError, getReshareConfig, isBusinessAccountActionRequired, getRequiredExternalKeyShareId, verifiedCredentialNameToChainEnum, AuthMode, NoopLogger, DynamicApiClient, 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';
@@ -11,6 +11,7 @@ import { AxiosError } from 'axios';
11
11
  import gte from 'semver/functions/gte';
12
12
  import { ProviderEnum } from '@dynamic-labs/sdk-api-core/models/ProviderEnum';
13
13
  import PQueue from 'p-queue';
14
+ import { redactSecretsFromText, KEY_MATERIAL_TEXT_PATTERNS } from '@dynamic-labs/redaction';
14
15
  import { ed25519 } from '@noble/curves/ed25519';
15
16
  import { bytesToHex, numberToBytesLE, hexToBytes, bytesToNumberLE } from '@noble/curves/abstract/utils';
16
17
 
@@ -777,6 +778,7 @@ const ERROR_INCORRECT_PASSWORD = '[DynamicWaasWalletClient]: Incorrect password
777
778
  const ERROR_PASSWORD_REQUIRED_FOR_ENCRYPTED_WALLET = '[DynamicWaasWalletClient]: Password is required for refresh/reshare of a password-encrypted wallet.';
778
779
  const ERROR_EXISTING_PASSWORD_REQUIRED = '[DynamicWaasWalletClient]: Existing password is required to update the password for encrypted wallets.';
779
780
  const ERROR_WALLETS_ALREADY_ENCRYPTED = '[DynamicWaasWalletClient]: Cannot set password: wallets are already password-encrypted. Use updatePassword instead.';
781
+ const ERROR_EXISTING_WALLETS_NOT_ENCRYPTED = '[DynamicWaasWalletClient]: Cannot create a wallet with a password: existing wallets are not password-encrypted. Call setPassword first so all wallets share one password.';
780
782
  /**
781
783
  * Checks if password consistency validation is needed.
782
784
  * Returns false if no password provided or password is the environmentId default.
@@ -801,6 +803,18 @@ const ERROR_WALLETS_ALREADY_ENCRYPTED = '[DynamicWaasWalletClient]: Cannot set p
801
803
  }
802
804
  return undefined;
803
805
  }
806
+ /**
807
+ * Checks if the walletMap has a wallet with a recorded backup that is not password-encrypted.
808
+ * Wallets with no recorded backup are ignored: getWallets reports them as not
809
+ * password-encrypted, but their encryption state is unknown.
810
+ */ function hasPasswordlessWallet(walletMap) {
811
+ return Object.values(walletMap).some((wallet)=>{
812
+ const backupInfo = wallet.clientKeySharesBackupInfo;
813
+ if (!backupInfo || backupInfo.passwordEncrypted) return false;
814
+ var _backupInfo_backups;
815
+ return Object.values((_backupInfo_backups = backupInfo.backups) != null ? _backupInfo_backups : {}).some((locations)=>locations.length > 0);
816
+ });
817
+ }
804
818
  /**
805
819
  * Extracts the first encrypted key share credential from cached encrypted data.
806
820
  * Returns the base64-encoded encrypted credential string, or undefined if none found.
@@ -1635,6 +1649,7 @@ const NON_RETRYABLE_CEREMONY_ERROR_MESSAGES = [
1635
1649
  ERROR_PUBLIC_KEY_MISMATCH,
1636
1650
  ERROR_PASSWORD_MISMATCH,
1637
1651
  ERROR_INCORRECT_PASSWORD,
1652
+ ERROR_EXISTING_WALLETS_NOT_ENCRYPTED,
1638
1653
  ERROR_MULTIPLE_WALLETS_PER_CHAIN
1639
1654
  ];
1640
1655
  /**
@@ -2366,6 +2381,132 @@ const buildHttpCeremonyMeta = (fields)=>_extends({
2366
2381
  transport: WAAS_HTTPS_TRANSPORT
2367
2382
  }, Object.fromEntries(Object.entries(fields).filter(([, value])=>value !== undefined && value !== null)));
2368
2383
 
2384
+ /**
2385
+ * Lets a client-side ceremony fail as soon as the server's create stream
2386
+ * reports an error. The stream resolves early on `keygen_complete`, so a later
2387
+ * `error` event (e.g. a lock-contention 429) only reaches `onError`; without
2388
+ * this the client keeps waiting in a relay room the server never joined, or
2389
+ * (after the relay keygen) for a `ceremony_complete` that will never arrive.
2390
+ *
2391
+ * Usage:
2392
+ * 1. `watchStream()` for every create stream -> pass the result as its `onError`
2393
+ * 2. `watchCompletion(cb)` once -> pass the result as `onCeremonyComplete`
2394
+ * 3. `raceAgainstStreamError(work)` -> rejects early if a stream errors mid-work
2395
+ * 4. `waitForCompletion()` -> resolves on `ceremony_complete`
2396
+ */ const createCeremonyErrorWatch = ()=>{
2397
+ const streamError = createRejectablePromise();
2398
+ const completion = createResolvablePromise();
2399
+ const streams = {
2400
+ latestId: 0
2401
+ };
2402
+ return {
2403
+ /**
2404
+ * Registers a new create stream and returns its `onError` handler.
2405
+ * A fallback re-creates the stream, so only the newest stream's errors count.
2406
+ */ watchStream: ()=>{
2407
+ streams.latestId += 1;
2408
+ const streamId = streams.latestId;
2409
+ return (error)=>{
2410
+ if (streamId === streams.latestId) {
2411
+ streamError.reject(error);
2412
+ }
2413
+ };
2414
+ },
2415
+ /** Wraps the caller's `onCeremonyComplete` so `waitForCompletion` can observe it. */ watchCompletion: (onCeremonyComplete)=>onCeremonyComplete && ((...args)=>{
2416
+ onCeremonyComplete(...args);
2417
+ completion.resolve();
2418
+ }),
2419
+ /** Settles like `work`, but rejects early if the current stream errors first. */ raceAgainstStreamError: (work)=>Promise.race([
2420
+ work,
2421
+ streamError.promise
2422
+ ]),
2423
+ /** Resolves on `ceremony_complete`; rejects if the current stream errors first. */ waitForCompletion: ()=>Promise.race([
2424
+ completion.promise,
2425
+ streamError.promise
2426
+ ])
2427
+ };
2428
+ };
2429
+ const createResolvablePromise = ()=>{
2430
+ const handle = {
2431
+ resolve: ()=>undefined
2432
+ };
2433
+ const promise = new Promise((resolve)=>{
2434
+ handle.resolve = resolve;
2435
+ });
2436
+ return {
2437
+ promise,
2438
+ resolve: ()=>handle.resolve()
2439
+ };
2440
+ };
2441
+
2442
+ /** Bounded so a self-referencing or cyclic `cause` chain cannot spin. */ const MAX_CAUSE_DEPTH = 5;
2443
+ const MAX_DESCRIBED_LENGTH = 200;
2444
+ const redactErrorMessage = (message)=>redactSecretsFromText(message, KEY_MATERIAL_TEXT_PATTERNS).slice(0, MAX_DESCRIBED_LENGTH);
2445
+ /**
2446
+ * Keys safe to echo when a thrown object exposes no recognised message field.
2447
+ * Deliberately an allowlist: the alternative — serialising the whole object —
2448
+ * cannot be made safe, because a thrown MPC signer rejection or API body may
2449
+ * carry key-share material, a raw transaction body or an auth token.
2450
+ */ const SAFE_DETAIL_KEYS = [
2451
+ 'status',
2452
+ 'statusCode',
2453
+ 'code',
2454
+ 'error_code',
2455
+ 'errorCode',
2456
+ 'name',
2457
+ 'type'
2458
+ ];
2459
+ const isScalar = (value)=>typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
2460
+ /**
2461
+ * Builds a human-readable message for a thrown value that is not an `Error` —
2462
+ * most often an API response body, e.g.
2463
+ * `{ existingWalletId, error: 'Multiple wallets per chain not allowed' }`.
2464
+ *
2465
+ * `String(value)` would collapse that to `[object Object]`, losing the only
2466
+ * diagnostic content it had, so a recognised message field is preferred.
2467
+ *
2468
+ * Never serialises an unrecognised object wholesale. Where no message field
2469
+ * exists, only `SAFE_DETAIL_KEYS` scalars are echoed, and failing that a fixed
2470
+ * placeholder — so a value carrying secrets can never reach the logging backend
2471
+ * just because it lacked a `message`.
2472
+ */ const describeNonError = (value)=>{
2473
+ if (typeof value === 'string') return value.slice(0, MAX_DESCRIBED_LENGTH);
2474
+ if (value !== null && typeof value === 'object') {
2475
+ const record = value;
2476
+ const named = [
2477
+ record['error'],
2478
+ record['message'],
2479
+ record['detail']
2480
+ ].find((candidate)=>typeof candidate === 'string' && candidate.length > 0);
2481
+ if (named) return named.slice(0, MAX_DESCRIBED_LENGTH);
2482
+ const safeDetails = SAFE_DETAIL_KEYS.filter((key)=>isScalar(record[key])).map((key)=>`${key}=${String(record[key])}`);
2483
+ return safeDetails.length > 0 ? safeDetails.join(' ').slice(0, MAX_DESCRIBED_LENGTH) : 'Non-Error value thrown with no recognised message field';
2484
+ }
2485
+ return String(value).slice(0, MAX_DESCRIBED_LENGTH);
2486
+ };
2487
+ /**
2488
+ * `{ name, message }` for each link of an error's `cause` chain, outermost
2489
+ * first, so a wrapper like `new Error(ERROR_SIGN_MESSAGE, { cause })` still
2490
+ * shows the failure it wraps. Bounded in depth and length, and never echoes
2491
+ * raw fields (they may carry key material); non-Errors use `describeNonError`.
2492
+ */ const describeCauseChain = (error, depth = 0)=>{
2493
+ if (error === undefined || error === null || depth >= MAX_CAUSE_DEPTH) return [];
2494
+ const link = error instanceof Error ? {
2495
+ name: error.name,
2496
+ message: redactErrorMessage(error.message)
2497
+ } : {
2498
+ name: typeof error,
2499
+ message: describeNonError(error)
2500
+ };
2501
+ const next = error instanceof Error ? error.cause : undefined;
2502
+ return next === error ? [
2503
+ link
2504
+ ] : [
2505
+ link,
2506
+ ...describeCauseChain(next, depth + 1)
2507
+ ];
2508
+ };
2509
+
2369
2510
  /**
2370
2511
  * Returns true for errors that represent expected user-facing conditions
2371
2512
  * (e.g. wrong password). These are logged at `info` instead of `error`
@@ -2430,7 +2571,6 @@ const classifyOne = (error)=>{
2430
2571
  }
2431
2572
  return undefined;
2432
2573
  };
2433
- /** Bounded so a self-referencing or cyclic `cause` chain cannot spin. */ const MAX_CAUSE_DEPTH = 5;
2434
2574
  /**
2435
2575
  * Single source of truth for "how bad is this error". Exported so callers
2436
2576
  * classify identically instead of hard-coding `logger.error` for every failure.
@@ -2455,49 +2595,6 @@ const classifyOne = (error)=>{
2455
2595
  }
2456
2596
  return fallback;
2457
2597
  };
2458
- const MAX_DESCRIBED_LENGTH = 200;
2459
- /**
2460
- * Keys safe to echo when a thrown object exposes no recognised message field.
2461
- * Deliberately an allowlist: the alternative — serialising the whole object —
2462
- * cannot be made safe, because a thrown MPC signer rejection or API body may
2463
- * carry key-share material, a raw transaction body or an auth token.
2464
- */ const SAFE_DETAIL_KEYS = [
2465
- 'status',
2466
- 'statusCode',
2467
- 'code',
2468
- 'error_code',
2469
- 'errorCode',
2470
- 'name',
2471
- 'type'
2472
- ];
2473
- const isScalar = (value)=>typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
2474
- /**
2475
- * Builds a human-readable message for a thrown value that is not an `Error` —
2476
- * most often an API response body, e.g.
2477
- * `{ existingWalletId, error: 'Multiple wallets per chain not allowed' }`.
2478
- *
2479
- * `String(value)` would collapse that to `[object Object]`, losing the only
2480
- * diagnostic content it had, so a recognised message field is preferred.
2481
- *
2482
- * Never serialises an unrecognised object wholesale. Where no message field
2483
- * exists, only `SAFE_DETAIL_KEYS` scalars are echoed, and failing that a fixed
2484
- * placeholder — so a value carrying secrets can never reach the logging backend
2485
- * just because it lacked a `message`.
2486
- */ const describeNonError = (value)=>{
2487
- if (typeof value === 'string') return value.slice(0, MAX_DESCRIBED_LENGTH);
2488
- if (value !== null && typeof value === 'object') {
2489
- const record = value;
2490
- const named = [
2491
- record['error'],
2492
- record['message'],
2493
- record['detail']
2494
- ].find((candidate)=>typeof candidate === 'string' && candidate.length > 0);
2495
- if (named) return named.slice(0, MAX_DESCRIBED_LENGTH);
2496
- const safeDetails = SAFE_DETAIL_KEYS.filter((key)=>isScalar(record[key])).map((key)=>`${key}=${String(record[key])}`);
2497
- return safeDetails.length > 0 ? safeDetails.join(' ').slice(0, MAX_DESCRIBED_LENGTH) : 'Non-Error value thrown with no recognised message field';
2498
- }
2499
- return String(value).slice(0, MAX_DESCRIBED_LENGTH);
2500
- };
2501
2598
  /**
2502
2599
  * Decides how an operation failure reaches the Datadog logger, whose signature
2503
2600
  * is `(message, context, error)`.
@@ -2957,6 +3054,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
2957
3054
  // queue timeout to attribute the budget to the phase that consumed it.
2958
3055
  let currentPhase = 'queued';
2959
3056
  let phaseStartedAt = Date.now();
3057
+ const signDetails = {};
2960
3058
  // The task keeps running after the queue timeout rejects `add()`, so keep
2961
3059
  // a reference to observe how it eventually settles.
2962
3060
  let taskPromise;
@@ -2968,6 +3066,16 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
2968
3066
  reportPhase: (phase)=>{
2969
3067
  currentPhase = phase;
2970
3068
  phaseStartedAt = Date.now();
3069
+ },
3070
+ reportDetails: (details)=>{
3071
+ if (details.dynamicRequestId && details.dynamicRequestId !== signDetails.dynamicRequestId) {
3072
+ if (details.forwardMpcFailure) {
3073
+ return;
3074
+ }
3075
+ signDetails.dynamicRequestId = details.dynamicRequestId;
3076
+ delete signDetails.forwardMpcFailure;
3077
+ }
3078
+ Object.assign(signDetails, details);
2971
3079
  }
2972
3080
  };
2973
3081
  taskPromise = (async ()=>{
@@ -2991,7 +3099,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
2991
3099
  logError({
2992
3100
  message: '[WalletQueueManager] Sign queue timed out',
2993
3101
  error: error,
2994
- context: {
3102
+ context: _extends({
2995
3103
  accountAddress,
2996
3104
  operation,
2997
3105
  phase: currentPhase,
@@ -3002,7 +3110,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3002
3110
  signQueuePending: signQueue.pending,
3003
3111
  signQueueSize: signQueue.size,
3004
3112
  recoveryInProgress: this.isRecoveryInProgress(accountAddress)
3005
- },
3113
+ }, signDetails),
3006
3114
  level: 'warn'
3007
3115
  });
3008
3116
  // The caller already saw an error; record how the orphaned ceremony
@@ -3015,24 +3123,25 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
3015
3123
  logError({
3016
3124
  message: '[SignLateResult] Sign settled after queue timeout',
3017
3125
  error: new Error('signature produced after the caller saw a timeout'),
3018
- context: {
3126
+ context: _extends({
3019
3127
  accountAddress,
3020
3128
  operation,
3021
3129
  outcome: 'resolved',
3022
3130
  settledMs: Date.now() - timedOutAt
3023
- },
3131
+ }, signDetails),
3024
3132
  level: 'warn'
3025
3133
  });
3026
3134
  }, (lateError)=>{
3027
3135
  logError({
3028
3136
  message: '[SignLateResult] Sign settled after queue timeout',
3029
3137
  error: lateError instanceof Error ? lateError : new Error(String(lateError)),
3030
- context: {
3138
+ context: _extends({
3031
3139
  accountAddress,
3032
3140
  operation,
3033
3141
  outcome: 'rejected',
3034
- settledMs: Date.now() - timedOutAt
3035
- },
3142
+ settledMs: Date.now() - timedOutAt,
3143
+ causes: describeCauseChain(lateError)
3144
+ }, signDetails),
3036
3145
  level: 'warn'
3037
3146
  });
3038
3147
  });
@@ -4281,6 +4390,8 @@ class DynamicWalletClient {
4281
4390
  durationMs: Date.now() - clientInitStartedAt
4282
4391
  }, this.getTraceContext(traceContext)));
4283
4392
  const serverInitStartedAt = Date.now();
4393
+ const ceremonyWatch = createCeremonyErrorWatch();
4394
+ const watchedOnCeremonyComplete = ceremonyWatch.watchCompletion(onCeremonyComplete);
4284
4395
  const serverKeygenInit = await this.serverInitializeKeyGen({
4285
4396
  chainName,
4286
4397
  clientKeygenIds,
@@ -4290,7 +4401,8 @@ class DynamicWalletClient {
4290
4401
  businessAccountId,
4291
4402
  idempotencyKey,
4292
4403
  traceContext,
4293
- onCeremonyComplete
4404
+ onCeremonyComplete: watchedOnCeremonyComplete,
4405
+ onError: ceremonyWatch.watchStream()
4294
4406
  });
4295
4407
  const emittedAtMs = serverKeygenInit.emittedAtMs;
4296
4408
  let roomId = serverKeygenInit.roomId;
@@ -4309,7 +4421,7 @@ class DynamicWalletClient {
4309
4421
  } : {}, this.getTraceContext(traceContext)));
4310
4422
  const clientKeyGenStartedAt = Date.now();
4311
4423
  let refreshRoomInvoked = false;
4312
- const { rawPublicKey, clientKeygenResults: clientKeyShares, operationSource } = await this.clientKeyGen({
4424
+ const { rawPublicKey, clientKeygenResults: clientKeyShares, operationSource } = await ceremonyWatch.raceAgainstStreamError(this.clientKeyGen({
4313
4425
  chainName,
4314
4426
  roomId,
4315
4427
  serverKeygenIds,
@@ -4332,7 +4444,8 @@ class DynamicWalletClient {
4332
4444
  bitcoinConfig,
4333
4445
  businessAccountId,
4334
4446
  traceContext,
4335
- onCeremonyComplete
4447
+ onCeremonyComplete: watchedOnCeremonyComplete,
4448
+ onError: ceremonyWatch.watchStream()
4336
4449
  });
4337
4450
  // Track the relay ceremony's room and keygen ids so the logs below
4338
4451
  // report the ceremony that actually produced the wallet.
@@ -4340,7 +4453,7 @@ class DynamicWalletClient {
4340
4453
  serverKeygenIds = refreshed.serverKeygenIds;
4341
4454
  return refreshed;
4342
4455
  }
4343
- });
4456
+ }));
4344
4457
  this.logger.info('[DynamicWaasWalletClient] Client key generation completed', _extends({
4345
4458
  roomId,
4346
4459
  serverKeygenIds: serverKeygenIds.join(', '),
@@ -4360,7 +4473,8 @@ class DynamicWalletClient {
4360
4473
  // the wallet, then fire onCeremonyComplete manually.
4361
4474
  // Skip when refreshRoom was invoked: the SSE fallback already delivered
4362
4475
  // onCeremonyComplete via the event stream.
4363
- if (this.featureFlags[FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT] === true && idempotencyKey && onCeremonyComplete && !refreshRoomInvoked) {
4476
+ const pollsHttpCompletion = this.featureFlags[FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT] === true && idempotencyKey && onCeremonyComplete && !refreshRoomInvoked;
4477
+ if (pollsHttpCompletion) {
4364
4478
  const completion = await this.pollHttpCeremonyCompletion({
4365
4479
  chainName,
4366
4480
  clientKeygenIds,
@@ -4372,6 +4486,10 @@ class DynamicWalletClient {
4372
4486
  traceContext
4373
4487
  });
4374
4488
  onCeremonyComplete(completion.accountAddress, completion.walletId, completion.shareSetId);
4489
+ } else if (onCeremonyComplete) {
4490
+ // Wait for the stream's ceremony_complete here (not in each chain client)
4491
+ // so a stream error after the relay keygen rejects instead of hanging.
4492
+ await ceremonyWatch.waitForCompletion();
4375
4493
  }
4376
4494
  return {
4377
4495
  rawPublicKey,
@@ -4655,7 +4773,7 @@ class DynamicWalletClient {
4655
4773
  throw error;
4656
4774
  }
4657
4775
  }
4658
- async clientSign({ chainName, message, roomId: initialRoomId, keyShare, derivationPath, isFormatted, dynamicRequestId, traceContext, bitcoinConfig, variant, refreshRoom }) {
4776
+ async clientSign({ chainName, message, roomId: initialRoomId, keyShare, derivationPath, isFormatted, dynamicRequestId, traceContext, bitcoinConfig, variant, refreshRoom, onForwardMpcFailure }) {
4659
4777
  // Reassigned when forward MPC fails and we mint a fresh room for the
4660
4778
  // relay-based fallback below.
4661
4779
  let roomId = initialRoomId;
@@ -4718,6 +4836,14 @@ class DynamicWalletClient {
4718
4836
  return forwardMPCSignature;
4719
4837
  } catch (error) {
4720
4838
  const errorInfo = classifyForwardMpcError(error);
4839
+ try {
4840
+ onForwardMpcFailure == null ? void 0 : onForwardMpcFailure(errorInfo);
4841
+ } catch (observerError) {
4842
+ this.logger.warn('Forward MPC failure observer threw', {
4843
+ dynamicRequestId,
4844
+ observerErrorType: observerError instanceof Error ? observerError.name : typeof observerError
4845
+ });
4846
+ }
4721
4847
  this.logger.warn('Forward MPC signing failed', _extends({}, errorInfo, {
4722
4848
  chainName,
4723
4849
  environmentId: this.environmentId,
@@ -4984,7 +5110,7 @@ class DynamicWalletClient {
4984
5110
  }
4985
5111
  //todo: need to modify with imported flag
4986
5112
  async sign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, approvalQuorum, onError, traceContext, bitcoinConfig }) {
4987
- return WalletQueueManager.queueSignOperation(accountAddress, WalletOperation.SIGN_MESSAGE, ({ reportPhase })=>this.internalSign({
5113
+ return WalletQueueManager.queueSignOperation(accountAddress, WalletOperation.SIGN_MESSAGE, ({ reportPhase, reportDetails })=>this.internalSign({
4988
5114
  accountAddress,
4989
5115
  message,
4990
5116
  chainName,
@@ -4998,7 +5124,8 @@ class DynamicWalletClient {
4998
5124
  onError,
4999
5125
  traceContext,
5000
5126
  bitcoinConfig,
5001
- onSignPhase: reportPhase
5127
+ onSignPhase: reportPhase,
5128
+ onSignDetails: reportDetails
5002
5129
  }));
5003
5130
  }
5004
5131
  /**
@@ -5106,8 +5233,11 @@ class DynamicWalletClient {
5106
5233
  clearTimeout(warningTimer);
5107
5234
  }
5108
5235
  }
5109
- async internalSign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, approvalQuorum, onError, traceContext, bitcoinConfig, onSignPhase, staleShareRetry = false, roomIdRetry = false }) {
5236
+ async internalSign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, approvalQuorum, onError, traceContext, bitcoinConfig, onSignPhase, onSignDetails, staleShareRetry = false, roomIdRetry = false }) {
5110
5237
  const dynamicRequestId = v4();
5238
+ onSignDetails == null ? void 0 : onSignDetails({
5239
+ dynamicRequestId
5240
+ });
5111
5241
  try {
5112
5242
  // Pre-flight phase boundaries. Nothing reaches the network until
5113
5243
  // serverSign below, so a stall here is invisible in server logs — these
@@ -5239,6 +5369,14 @@ class DynamicWalletClient {
5239
5369
  traceContext,
5240
5370
  bitcoinConfig,
5241
5371
  variant: wallet.variant,
5372
+ onForwardMpcFailure: ({ errorType, errorCode, sessionEstablished })=>onSignDetails == null ? void 0 : onSignDetails({
5373
+ dynamicRequestId,
5374
+ forwardMpcFailure: {
5375
+ errorType,
5376
+ errorCode,
5377
+ sessionEstablished
5378
+ }
5379
+ }),
5242
5380
  // On forward-MPC fallback, re-run the server sign with no roomId so a
5243
5381
  // brand-new server-joined room is created for the relay ceremony.
5244
5382
  refreshRoom: async ()=>{
@@ -5388,6 +5526,7 @@ class DynamicWalletClient {
5388
5526
  traceContext,
5389
5527
  bitcoinConfig,
5390
5528
  onSignPhase,
5529
+ onSignDetails,
5391
5530
  staleShareRetry: true,
5392
5531
  roomIdRetry
5393
5532
  });
@@ -5416,6 +5555,7 @@ class DynamicWalletClient {
5416
5555
  traceContext,
5417
5556
  bitcoinConfig,
5418
5557
  onSignPhase,
5558
+ onSignDetails,
5419
5559
  staleShareRetry,
5420
5560
  roomIdRetry: true
5421
5561
  });
@@ -8485,17 +8625,23 @@ class DynamicWalletClient {
8485
8625
  sessionPublicKey,
8486
8626
  passwordUpdateBatchId
8487
8627
  });
8628
+ let backupData;
8488
8629
  try {
8489
- await backup();
8630
+ backupData = await backup();
8490
8631
  } catch (error) {
8491
8632
  if (!(error instanceof StaleLocalSharesError) || !error.localSharesRefreshed) throw error;
8492
- await backup();
8633
+ backupData = await backup();
8493
8634
  }
8635
+ // Not named passwordUpdateStatus: the logger redacts string values of password* keys.
8494
8636
  this.logPasswordOperationSuccess('updatePassword', {
8495
8637
  accountAddress,
8496
8638
  walletId: wallet == null ? void 0 : wallet.walletId,
8497
- passwordUpdateBatchId
8639
+ passwordUpdateBatchId,
8640
+ batchStatus: backupData.passwordUpdateStatus
8498
8641
  });
8642
+ return {
8643
+ passwordUpdateStatus: backupData.passwordUpdateStatus
8644
+ };
8499
8645
  } catch (error) {
8500
8646
  var _this_storage_removeItem, _this_storage;
8501
8647
  // The pending encrypted-shares cache entry may have been written before the failure.
@@ -8525,7 +8671,7 @@ class DynamicWalletClient {
8525
8671
  if (isWalletPasswordEncrypted(wallet)) {
8526
8672
  throw new Error(ERROR_WALLETS_ALREADY_ENCRYPTED);
8527
8673
  }
8528
- await this.storeEncryptedBackupByWallet({
8674
+ const backupData = await this.storeEncryptedBackupByWallet({
8529
8675
  accountAddress,
8530
8676
  password: newPassword,
8531
8677
  signedSessionId,
@@ -8535,8 +8681,12 @@ class DynamicWalletClient {
8535
8681
  this.logPasswordOperationSuccess('setPassword', {
8536
8682
  accountAddress,
8537
8683
  walletId: wallet == null ? void 0 : wallet.walletId,
8538
- passwordUpdateBatchId
8684
+ passwordUpdateBatchId,
8685
+ batchStatus: backupData.passwordUpdateStatus
8539
8686
  });
8687
+ return {
8688
+ passwordUpdateStatus: backupData.passwordUpdateStatus
8689
+ };
8540
8690
  } catch (error) {
8541
8691
  var _this_storage_removeItem, _this_storage;
8542
8692
  // A pending cache entry may have been written if passwordUpdateBatchId was set.
@@ -8686,7 +8836,12 @@ class DynamicWalletClient {
8686
8836
  // Find first password-encrypted wallet
8687
8837
  const found = findPasswordEncryptedWallet(this.walletMap);
8688
8838
  if (!found) {
8689
- return; // No existing encrypted wallets
8839
+ // A password-encrypted wallet next to passwordless ones leaves a mixed state that
8840
+ // password updates cannot finish. setPassword encrypts all existing wallets at once.
8841
+ if (hasPasswordlessWallet(this.walletMap)) {
8842
+ throw new Error(ERROR_EXISTING_WALLETS_NOT_ENCRYPTED);
8843
+ }
8844
+ return; // No existing wallets
8690
8845
  }
8691
8846
  const [address, wallet] = found;
8692
8847
  try {
@@ -10056,6 +10211,41 @@ class DynamicWalletClient {
10056
10211
  }
10057
10212
  }
10058
10213
  /**
10214
+ * Re-locks every unlocked password-encrypted wallet: clears its decrypted
10215
+ * client key shares from storage and flips its ready state back to
10216
+ * ENCRYPTED, so the next operation prompts for the wallet password again.
10217
+ * The cached encrypted-shares blobs are kept, so a later unlockWallet does
10218
+ * not need a server round-trip. Wallets that are not password-encrypted have
10219
+ * no password to re-prompt for and are left untouched.
10220
+ *
10221
+ * When `accountAddress` is given, only that wallet is re-locked.
10222
+ */ async lockWallets(accountAddress) {
10223
+ const normalizedTarget = accountAddress ? normalizeAddress(accountAddress) : undefined;
10224
+ if (normalizedTarget && !this.walletMap[normalizedTarget]) {
10225
+ throw new Error(`Wallet not found for address: ${accountAddress}`);
10226
+ }
10227
+ const walletsToLock = Object.entries(this.walletMap).filter(([address, wallet])=>wallet.walletReadyState === WalletReadyState.READY && isWalletPasswordEncrypted(wallet) && (!normalizedTarget || address === normalizedTarget));
10228
+ const results = await Promise.allSettled(walletsToLock.map(async ([accountAddress])=>{
10229
+ await this.setClientKeySharesToStorage({
10230
+ accountAddress,
10231
+ clientKeyShares: []
10232
+ });
10233
+ this.updateWalletMap(accountAddress, {
10234
+ walletReadyState: WalletReadyState.ENCRYPTED
10235
+ });
10236
+ }));
10237
+ // Persist even on partial failure so the stored map matches the in-memory
10238
+ // one: a failed wallet stays READY with its shares still in storage.
10239
+ await this.storage.setItem(this.storageKey, JSON.stringify(this.walletMap));
10240
+ this.logger.info('[DynamicWaasWalletClient] lockWallets: re-locked wallets', {
10241
+ lockedCount: walletsToLock.length
10242
+ });
10243
+ const failure = results.find((result)=>result.status === 'rejected');
10244
+ if (failure) {
10245
+ throw failure.reason;
10246
+ }
10247
+ }
10248
+ /**
10059
10249
  * Decrypts cached encrypted key shares for a wallet and stores them locally.
10060
10250
  */ async decryptAndStoreWalletShares({ accountAddress, encryptedData, password }) {
10061
10251
  const data = JSON.parse(encryptedData);
@@ -10563,12 +10753,6 @@ class DynamicWalletClient {
10563
10753
  logger: this.logger,
10564
10754
  onUnauthorized: internalOptions == null ? void 0 : internalOptions.onUnauthorized
10565
10755
  });
10566
- // `reshare` left unbound until the signer-reshare flavor (reshare into a new
10567
- // signer's pending share set) exists; addSigner is stubbed until then.
10568
- this.businessAccounts = new BusinessAccountClient({
10569
- apiClient: this.apiClient,
10570
- logger: this.logger
10571
- });
10572
10756
  this.debug = Boolean(debug);
10573
10757
  this.featureFlags = featureFlags || {};
10574
10758
  this.argon2idRebackupTargets = argon2idRebackupTargets || [];
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@dynamic-labs-wallet/browser",
3
- "version": "1.1.28",
3
+ "version": "1.1.30",
4
4
  "license": "Licensed under the Dynamic Labs, Inc. Terms Of Service (https://www.dynamic.xyz/terms-conditions)",
5
5
  "type": "module",
6
6
  "dependencies": {
7
- "@dynamic-labs-wallet/core": "1.1.28",
7
+ "@dynamic-labs-wallet/core": "1.1.30",
8
8
  "@dynamic-labs-wallet/forward-mpc-client": "2.0.2",
9
- "@dynamic-labs-wallet/primitives": "1.1.28",
9
+ "@dynamic-labs-wallet/primitives": "1.1.30",
10
+ "@dynamic-labs/redaction": "^1.1.0",
10
11
  "@dynamic-labs/sdk-api-core": "0.50.0",
11
12
  "@noble/curves": "1.8.0",
12
13
  "argon2id": "1.0.1",
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Lets a client-side ceremony fail as soon as the server's create stream
3
+ * reports an error. The stream resolves early on `keygen_complete`, so a later
4
+ * `error` event (e.g. a lock-contention 429) only reaches `onError`; without
5
+ * this the client keeps waiting in a relay room the server never joined, or
6
+ * (after the relay keygen) for a `ceremony_complete` that will never arrive.
7
+ *
8
+ * Usage:
9
+ * 1. `watchStream()` for every create stream -> pass the result as its `onError`
10
+ * 2. `watchCompletion(cb)` once -> pass the result as `onCeremonyComplete`
11
+ * 3. `raceAgainstStreamError(work)` -> rejects early if a stream errors mid-work
12
+ * 4. `waitForCompletion()` -> resolves on `ceremony_complete`
13
+ */
14
+ export declare const createCeremonyErrorWatch: () => {
15
+ /**
16
+ * Registers a new create stream and returns its `onError` handler.
17
+ * A fallback re-creates the stream, so only the newest stream's errors count.
18
+ */
19
+ watchStream: () => ((error: Error) => void);
20
+ /** Wraps the caller's `onCeremonyComplete` so `waitForCompletion` can observe it. */
21
+ watchCompletion: <A extends unknown[]>(onCeremonyComplete?: (...args: A) => void) => ((...args: A) => void) | undefined;
22
+ /** Settles like `work`, but rejects early if the current stream errors first. */
23
+ raceAgainstStreamError: <T>(work: Promise<T>) => Promise<T>;
24
+ /** Resolves on `ceremony_complete`; rejects if the current stream errors first. */
25
+ waitForCompletion: () => Promise<void>;
26
+ };
27
+ //# sourceMappingURL=ceremonyErrorWatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ceremonyErrorWatch.d.ts","sourceRoot":"","sources":["../../packages/src/ceremonyErrorWatch.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB;IAMjC;;;OAGG;uBACc,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAUzC,qFAAqF;sBACnE,CAAC,SAAS,OAAO,EAAE,uBAAuB,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,IAAI,gBAEnE,CAAC,KAAG,IAAI;IAKrB,iFAAiF;6BACxD,CAAC,QAAQ,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAAC;IAEzD,mFAAmF;6BAC5D,OAAO,CAAC,IAAI,CAAC;CAEvC,CAAC"}