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