@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.cjs +256 -72
- package/index.esm.js +257 -73
- package/package.json +4 -3
- package/src/ceremonyErrorWatch.d.ts +27 -0
- package/src/ceremonyErrorWatch.d.ts.map +1 -0
- package/src/client.d.ts +17 -6
- package/src/client.d.ts.map +1 -1
- package/src/passwordValidation.d.ts +7 -0
- package/src/passwordValidation.d.ts.map +1 -1
- package/src/queue.d.ts +13 -1
- package/src/queue.d.ts.map +1 -1
- package/src/services/errorDescription.d.ts +28 -0
- package/src/services/errorDescription.d.ts.map +1 -0
- package/src/services/logger.d.ts +2 -14
- package/src/services/logger.d.ts.map +1 -1
- package/src/utils.d.ts.map +1 -1
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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"}
|