@dynamic-labs-wallet/browser 1.1.34 → 1.1.36
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 +707 -86
- package/index.esm.js +708 -88
- package/package.json +3 -3
- package/src/client.d.ts +179 -4
- package/src/client.d.ts.map +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.d.ts.map +1 -1
- package/src/queue.d.ts +8 -1
- package/src/queue.d.ts.map +1 -1
- package/src/services/forwardMpcEnclaveState.d.ts +22 -0
- package/src/services/forwardMpcEnclaveState.d.ts.map +1 -0
- package/src/services/logger.d.ts +11 -0
- package/src/services/logger.d.ts.map +1 -1
package/index.cjs
CHANGED
|
@@ -2600,6 +2600,45 @@ const classifyOne = (error)=>{
|
|
|
2600
2600
|
}
|
|
2601
2601
|
return fallback;
|
|
2602
2602
|
};
|
|
2603
|
+
/** The HTTP status and backend code of one link in a `cause` chain, or `undefined` when it is not a rejected request. */ const httpFieldsOf = (error)=>{
|
|
2604
|
+
if (error instanceof axios.AxiosError && error.response) {
|
|
2605
|
+
const errorCode = core.extractServerErrorCode(error);
|
|
2606
|
+
return _extends({
|
|
2607
|
+
httpStatus: error.response.status
|
|
2608
|
+
}, errorCode ? {
|
|
2609
|
+
errorCode
|
|
2610
|
+
} : {});
|
|
2611
|
+
}
|
|
2612
|
+
const status = getWalletApiErrorStatus(error);
|
|
2613
|
+
if (status === undefined) return undefined;
|
|
2614
|
+
const { code } = error;
|
|
2615
|
+
return _extends({
|
|
2616
|
+
httpStatus: status
|
|
2617
|
+
}, typeof code === 'string' ? {
|
|
2618
|
+
errorCode: code
|
|
2619
|
+
} : {});
|
|
2620
|
+
};
|
|
2621
|
+
/**
|
|
2622
|
+
* The fields a failure is filtered by: its class, the HTTP status and the
|
|
2623
|
+
* backend's own code when a request was rejected. Walks the `cause` chain so a
|
|
2624
|
+
* chain client's wrapper does not hide the API error beneath it. Scalars only,
|
|
2625
|
+
* so nothing from a response body can carry key material into a log.
|
|
2626
|
+
*/ const describeErrorForLog = (error)=>{
|
|
2627
|
+
const errorName = error instanceof Error ? error.name : typeof error;
|
|
2628
|
+
let current = error;
|
|
2629
|
+
for(let depth = 0; current !== undefined && current !== null && depth < MAX_CAUSE_DEPTH; depth += 1){
|
|
2630
|
+
const httpFields = httpFieldsOf(current);
|
|
2631
|
+
if (httpFields) return _extends({
|
|
2632
|
+
errorName
|
|
2633
|
+
}, httpFields);
|
|
2634
|
+
const next = current instanceof Error ? current.cause : undefined;
|
|
2635
|
+
if (next === current) break;
|
|
2636
|
+
current = next;
|
|
2637
|
+
}
|
|
2638
|
+
return {
|
|
2639
|
+
errorName
|
|
2640
|
+
};
|
|
2641
|
+
};
|
|
2603
2642
|
/**
|
|
2604
2643
|
* Decides how an operation failure reaches the Datadog logger, whose signature
|
|
2605
2644
|
* is `(message, context, error)`.
|
|
@@ -3035,9 +3074,10 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3035
3074
|
* @param accountAddress - The wallet address
|
|
3036
3075
|
* @param operation - The operation type (must be a valid SignQueueOperation)
|
|
3037
3076
|
* @param callback - The sign operation to execute
|
|
3077
|
+
* @param options - Optional diagnostics attached to timeout logs
|
|
3038
3078
|
* @throws Error if operation is not a valid sign queue operation
|
|
3039
3079
|
* @returns Promise resolving to the sign result
|
|
3040
|
-
*/ static async queueSignOperation(accountAddress, operation, callback) {
|
|
3080
|
+
*/ static async queueSignOperation(accountAddress, operation, callback, options) {
|
|
3041
3081
|
accountAddress = normalizeAddress(accountAddress);
|
|
3042
3082
|
// Runtime validation to catch any type assertion bypasses
|
|
3043
3083
|
if (!isSignQueueOperation(operation)) {
|
|
@@ -3060,6 +3100,13 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3060
3100
|
let currentPhase = 'queued';
|
|
3061
3101
|
let phaseStartedAt = Date.now();
|
|
3062
3102
|
const signDetails = {};
|
|
3103
|
+
const enclaveContext = ()=>{
|
|
3104
|
+
var _options_getEnclaveSnapshot;
|
|
3105
|
+
const forwardMpcEnclave = options == null ? void 0 : (_options_getEnclaveSnapshot = options.getEnclaveSnapshot) == null ? void 0 : _options_getEnclaveSnapshot.call(options);
|
|
3106
|
+
return forwardMpcEnclave ? {
|
|
3107
|
+
forwardMpcEnclave
|
|
3108
|
+
} : {};
|
|
3109
|
+
};
|
|
3063
3110
|
// The task keeps running after the queue timeout rejects `add()`, so keep
|
|
3064
3111
|
// a reference to observe how it eventually settles.
|
|
3065
3112
|
let taskPromise;
|
|
@@ -3115,7 +3162,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3115
3162
|
signQueuePending: signQueue.pending,
|
|
3116
3163
|
signQueueSize: signQueue.size,
|
|
3117
3164
|
recoveryInProgress: this.isRecoveryInProgress(accountAddress)
|
|
3118
|
-
}, signDetails),
|
|
3165
|
+
}, enclaveContext(), signDetails),
|
|
3119
3166
|
level: 'warn'
|
|
3120
3167
|
});
|
|
3121
3168
|
// The caller already saw an error; record how the orphaned ceremony
|
|
@@ -3133,7 +3180,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3133
3180
|
operation,
|
|
3134
3181
|
outcome: 'resolved',
|
|
3135
3182
|
settledMs: Date.now() - timedOutAt
|
|
3136
|
-
}, signDetails),
|
|
3183
|
+
}, enclaveContext(), signDetails),
|
|
3137
3184
|
level: 'warn'
|
|
3138
3185
|
});
|
|
3139
3186
|
}, (lateError)=>{
|
|
@@ -3146,7 +3193,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3146
3193
|
outcome: 'rejected',
|
|
3147
3194
|
settledMs: Date.now() - timedOutAt,
|
|
3148
3195
|
causes: describeCauseChain(lateError)
|
|
3149
|
-
}, signDetails),
|
|
3196
|
+
}, enclaveContext(), signDetails),
|
|
3150
3197
|
level: 'warn'
|
|
3151
3198
|
});
|
|
3152
3199
|
});
|
|
@@ -3238,6 +3285,46 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3238
3285
|
/** Track which wallets are currently executing inside a sign operation.
|
|
3239
3286
|
* Used to prevent deadlocks when recovery is called from within a sign op. */ WalletQueueManager.walletsWithActiveSignOp = new Map();
|
|
3240
3287
|
|
|
3288
|
+
const FLAP_WINDOW_MS = 60000;
|
|
3289
|
+
// Bounds memory on a socket that flaps indefinitely; the count saturates here.
|
|
3290
|
+
const MAX_TRACKED_DISCONNECTS = 50;
|
|
3291
|
+
// The iframe shares one socket across every chain client; one history per socket.
|
|
3292
|
+
const readers = new WeakMap();
|
|
3293
|
+
/**
|
|
3294
|
+
* Starts recording enclave disconnects and returns a reader for the current
|
|
3295
|
+
* state. Idempotent per client: call it right after creating the socket (before
|
|
3296
|
+
* `connect()`) so startup disconnects are kept, and later calls reuse the same
|
|
3297
|
+
* history instead of stacking listeners.
|
|
3298
|
+
*/ const trackForwardMpcEnclave = (client)=>{
|
|
3299
|
+
const existing = readers.get(client);
|
|
3300
|
+
if (existing) {
|
|
3301
|
+
return existing;
|
|
3302
|
+
}
|
|
3303
|
+
const disconnects = [];
|
|
3304
|
+
client.on('disconnected', (closeCode)=>{
|
|
3305
|
+
disconnects.push({
|
|
3306
|
+
at: Date.now(),
|
|
3307
|
+
closeCode
|
|
3308
|
+
});
|
|
3309
|
+
if (disconnects.length > MAX_TRACKED_DISCONNECTS) {
|
|
3310
|
+
disconnects.shift();
|
|
3311
|
+
}
|
|
3312
|
+
});
|
|
3313
|
+
const reader = ()=>{
|
|
3314
|
+
const now = Date.now();
|
|
3315
|
+
const last = disconnects.at(-1);
|
|
3316
|
+
return _extends({
|
|
3317
|
+
connected: client.connected,
|
|
3318
|
+
disconnectsLastMinute: disconnects.filter(({ at })=>now - at <= FLAP_WINDOW_MS).length
|
|
3319
|
+
}, last && {
|
|
3320
|
+
lastCloseCode: last.closeCode,
|
|
3321
|
+
msSinceLastDisconnect: now - last.at
|
|
3322
|
+
});
|
|
3323
|
+
};
|
|
3324
|
+
readers.set(client, reader);
|
|
3325
|
+
return reader;
|
|
3326
|
+
};
|
|
3327
|
+
|
|
3241
3328
|
// A DYNAMIC backup can persist multiple shares (e.g. 2-of-3); the singular
|
|
3242
3329
|
// externalKeyShareId is a fallback for older single-id shapes.
|
|
3243
3330
|
const collectDynamicKeyShareIds = (locations)=>{
|
|
@@ -3766,6 +3853,11 @@ class DynamicWalletClient {
|
|
|
3766
3853
|
});
|
|
3767
3854
|
});
|
|
3768
3855
|
}
|
|
3856
|
+
queueSign(accountAddress, task) {
|
|
3857
|
+
return WalletQueueManager.queueSignOperation(accountAddress, core.WalletOperation.SIGN_MESSAGE, task, {
|
|
3858
|
+
getEnclaveSnapshot: this.enclaveSnapshot
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3769
3861
|
connectForwardMPC() {
|
|
3770
3862
|
const forwardMPCConnectStartedAt = Date.now();
|
|
3771
3863
|
this.apiClient.forwardMPCClient.connect().then(()=>{
|
|
@@ -4667,7 +4759,7 @@ class DynamicWalletClient {
|
|
|
4667
4759
|
}
|
|
4668
4760
|
// Note: the server signing party (wallet-service) selects the ed25519
|
|
4669
4761
|
// protocol variant from its own persisted EAC, so it is NOT threaded here.
|
|
4670
|
-
const serializedContext = context ?
|
|
4762
|
+
const serializedContext = context ? core.toJsonSafeContext(context) : undefined;
|
|
4671
4763
|
const useHttpTransport = this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT] === true && this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT_SIGNMESSAGE] === true;
|
|
4672
4764
|
const params = {
|
|
4673
4765
|
walletId,
|
|
@@ -4690,6 +4782,45 @@ class DynamicWalletClient {
|
|
|
4690
4782
|
})) : await this.apiClient.signMessage(params);
|
|
4691
4783
|
return data;
|
|
4692
4784
|
}
|
|
4785
|
+
/**
|
|
4786
|
+
* The server half of `signAndBroadcast`: asks Dynamic to open the signing rooms and returns the transaction record.
|
|
4787
|
+
*
|
|
4788
|
+
* @param params.walletId - The wallet that signs.
|
|
4789
|
+
* @param params.shareSetId - Which of the wallet's key sets to use. Defaults to its active one.
|
|
4790
|
+
* @param params.messages - The transaction once per fee attempt, in fee order, as the bytes to sign (RLP hex on EVM, message bytes on Solana). Not hashed.
|
|
4791
|
+
* @param params.contexts - What each message does in readable form (chain, recipient, amount), one per message in the same order. Dynamic checks each against the wallet and its policies.
|
|
4792
|
+
* @param params.broadcastOptions - Solana send settings: other required signers' signatures, and skipping the pre-send dry run.
|
|
4793
|
+
* @param params.roomIds - Signing rooms you already opened, one per message. Send all or none; Dynamic opens them when omitted.
|
|
4794
|
+
* @param params.requestedCertainty - How sure to be before calling it confirmed: `fast`, `safe` or `final`, or `chain:` plus a chain-specific value. Defaults to `safe`.
|
|
4795
|
+
* @param params.externalReference - Your own label, returned on every event. Not unique.
|
|
4796
|
+
* @param params.idempotencyKey - Makes a retry safe: the same key replays the stored answer instead of sending twice.
|
|
4797
|
+
* @param params.mfaToken - Proof of the user's multi-factor check, when the wallet needs one.
|
|
4798
|
+
* @param params.elevatedAccessToken - Proof of a recent re-login, when the wallet needs one.
|
|
4799
|
+
* @param params.approvalQuorum - Approvers' consent for a governed wallet, sent on the retry after `intent_required`.
|
|
4800
|
+
* @param params.onError - Called with the failure's message, never the raw error.
|
|
4801
|
+
* @param params.dynamicRequestId - Id of this one request, to match client and server logs.
|
|
4802
|
+
* @param params.traceContext - Ties this call's logs to the server's, across retries. Created when omitted.
|
|
4803
|
+
*/ async serverSignAndBroadcast({ walletId, shareSetId, contexts, broadcastOptions, messages, roomIds, requestedCertainty, externalReference, idempotencyKey, mfaToken, elevatedAccessToken, approvalQuorum, onError, dynamicRequestId, traceContext }) {
|
|
4804
|
+
// Opens one signing room per message, unless `roomIds` names them, and returns the transaction record.
|
|
4805
|
+
return await this.apiClient.signAndBroadcast({
|
|
4806
|
+
walletId,
|
|
4807
|
+
shareSetId,
|
|
4808
|
+
contexts: contexts.map((context)=>core.toJsonSafeContext(context)),
|
|
4809
|
+
broadcastOptions,
|
|
4810
|
+
messages,
|
|
4811
|
+
roomIds,
|
|
4812
|
+
isFormatted: false,
|
|
4813
|
+
requestedCertainty,
|
|
4814
|
+
externalReference,
|
|
4815
|
+
idempotencyKey,
|
|
4816
|
+
mfaToken,
|
|
4817
|
+
elevatedAccessToken,
|
|
4818
|
+
approvalQuorum,
|
|
4819
|
+
onError,
|
|
4820
|
+
dynamicRequestId,
|
|
4821
|
+
traceContext
|
|
4822
|
+
});
|
|
4823
|
+
}
|
|
4693
4824
|
prepareMessageForForwardMPC({ chainName, message, formattedMessage, signingAlgo, isFormatted }) {
|
|
4694
4825
|
// BIP340 hash unwrap MUST come first. When `isFormatted=true`, callers wrap
|
|
4695
4826
|
// the payload in a `MessageHash`; the direct-MPC path then reads
|
|
@@ -4778,7 +4909,7 @@ class DynamicWalletClient {
|
|
|
4778
4909
|
throw error;
|
|
4779
4910
|
}
|
|
4780
4911
|
}
|
|
4781
|
-
async clientSign({ chainName, message, roomId: initialRoomId, keyShare, derivationPath, isFormatted, dynamicRequestId, traceContext, bitcoinConfig, variant, refreshRoom, onForwardMpcFailure }) {
|
|
4912
|
+
async clientSign({ chainName, message, roomId: initialRoomId, keyShare, derivationPath, isFormatted, dynamicRequestId, traceContext, bitcoinConfig, variant, refreshRoom, skipForwardMPC, onForwardMpcFailure }) {
|
|
4782
4913
|
// Reassigned when forward MPC fails and we mint a fresh room for the
|
|
4783
4914
|
// relay-based fallback below.
|
|
4784
4915
|
let roomId = initialRoomId;
|
|
@@ -4806,7 +4937,7 @@ class DynamicWalletClient {
|
|
|
4806
4937
|
isFormatted
|
|
4807
4938
|
}, this.getTraceContext(traceContext)));
|
|
4808
4939
|
// Try forward MPC first if enabled, with fallback to relay-based signing on attestation failure
|
|
4809
|
-
if (this.forwardMPCEnabled && !this.shouldSkipForwardMPC(chainName)) {
|
|
4940
|
+
if (this.forwardMPCEnabled && !skipForwardMPC && !this.shouldSkipForwardMPC(chainName)) {
|
|
4810
4941
|
attemptedForwardMPCSign = true;
|
|
4811
4942
|
const forwardMPCStartedAt = Date.now();
|
|
4812
4943
|
try {
|
|
@@ -5115,7 +5246,7 @@ class DynamicWalletClient {
|
|
|
5115
5246
|
}
|
|
5116
5247
|
//todo: need to modify with imported flag
|
|
5117
5248
|
async sign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, approvalQuorum, onError, traceContext, bitcoinConfig }) {
|
|
5118
|
-
return
|
|
5249
|
+
return this.queueSign(accountAddress, ({ reportPhase, reportDetails })=>this.internalSign({
|
|
5119
5250
|
accountAddress,
|
|
5120
5251
|
message,
|
|
5121
5252
|
chainName,
|
|
@@ -5134,6 +5265,278 @@ class DynamicWalletClient {
|
|
|
5134
5265
|
}));
|
|
5135
5266
|
}
|
|
5136
5267
|
/**
|
|
5268
|
+
* Signs a transaction for Dynamic to broadcast, one signing ceremony per message. The signature stays on the server.
|
|
5269
|
+
*
|
|
5270
|
+
* @param params.accountAddress - The wallet's address.
|
|
5271
|
+
* @param params.chainName - The wallet's chain, e.g. `EVM`.
|
|
5272
|
+
* @param params.messages - The transaction once per fee attempt, in fee order, as the bytes to sign (RLP hex on EVM, message bytes on Solana). Not hashed.
|
|
5273
|
+
* @param params.contexts - What each message does in readable form (chain, recipient, amount), one per message in the same order. Dynamic checks each against the wallet and its policies.
|
|
5274
|
+
* @param params.broadcastOptions - Solana send settings: other required signers' signatures, and skipping the pre-send dry run.
|
|
5275
|
+
* @param params.password - The wallet's backup password, if it has one.
|
|
5276
|
+
* @param params.signedSessionId - The signed-in session, used to unlock the wallet's key share.
|
|
5277
|
+
* @param params.mfaToken - Proof of the user's multi-factor check, when the wallet needs one.
|
|
5278
|
+
* @param params.elevatedAccessToken - Proof of a recent re-login, when the wallet needs one.
|
|
5279
|
+
* @param params.approvalQuorum - Approvers' consent for a governed wallet, sent on the retry after `intent_required`.
|
|
5280
|
+
* @param params.requestedCertainty - How sure to be before calling it confirmed: `fast`, `safe` or `final`, or `chain:` plus a chain-specific value. Defaults to `safe`.
|
|
5281
|
+
* @param params.externalReference - Your own label, returned on every event. Not unique.
|
|
5282
|
+
* @param params.idempotencyKey - Makes a retry safe: the same key replays the stored answer instead of sending twice. A new one is made when omitted.
|
|
5283
|
+
* @param params.onError - Called with the failure's message, never the raw error.
|
|
5284
|
+
* @param params.traceContext - Ties this call's logs to the server's, across retries. Created when omitted.
|
|
5285
|
+
* @returns The transaction record. A finished one comes back without signing again.
|
|
5286
|
+
* @throws Error when `contexts` and `messages` differ in length.
|
|
5287
|
+
*/ async signAndBroadcast({ accountAddress, chainName, contexts, broadcastOptions, messages, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, requestedCertainty, externalReference, idempotencyKey = uuid.v4(), onError, traceContext }) {
|
|
5288
|
+
// Each fee attempt is one message and one context; a mismatch is the caller's, so it is named here, before any ceremony.
|
|
5289
|
+
if (messages.length === 0 || contexts.length !== messages.length) {
|
|
5290
|
+
throw new Error(`Sign and broadcast needs one signing context per message, got ${contexts.length} contexts for ${messages.length} messages`);
|
|
5291
|
+
}
|
|
5292
|
+
return this.queueSign(accountAddress, ({ reportPhase })=>this.internalSignAndBroadcast({
|
|
5293
|
+
accountAddress,
|
|
5294
|
+
chainName,
|
|
5295
|
+
contexts,
|
|
5296
|
+
broadcastOptions,
|
|
5297
|
+
messages,
|
|
5298
|
+
password,
|
|
5299
|
+
signedSessionId,
|
|
5300
|
+
mfaToken,
|
|
5301
|
+
elevatedAccessToken,
|
|
5302
|
+
approvalQuorum,
|
|
5303
|
+
requestedCertainty,
|
|
5304
|
+
externalReference,
|
|
5305
|
+
idempotencyKey,
|
|
5306
|
+
onError,
|
|
5307
|
+
traceContext,
|
|
5308
|
+
onSignPhase: reportPhase
|
|
5309
|
+
}));
|
|
5310
|
+
}
|
|
5311
|
+
/**
|
|
5312
|
+
* Does the work of `signAndBroadcast` once it has its turn in the wallet queue: loads the wallet, asks Dynamic to open the rooms, then signs in each.
|
|
5313
|
+
*
|
|
5314
|
+
* Takes the same fields as `signAndBroadcast`.
|
|
5315
|
+
*
|
|
5316
|
+
* @param params.onSignPhase - Reports which step it is on, so the queue can tell which step used up the time.
|
|
5317
|
+
*/ async internalSignAndBroadcast({ accountAddress, chainName, contexts, broadcastOptions, messages, password, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, requestedCertainty, externalReference, idempotencyKey, onError, traceContext, onSignPhase, staleShareRetry = false, roomIdRetry = false }) {
|
|
5318
|
+
const dynamicRequestId = uuid.v4();
|
|
5319
|
+
traceContext = core.withTraceId(traceContext);
|
|
5320
|
+
let stage = 'prepareWallet';
|
|
5321
|
+
let roomIdSource;
|
|
5322
|
+
this.logger.info('[DynamicWaasWalletClient] Sign and broadcast started', _extends({
|
|
5323
|
+
key: 'signAndBroadcast',
|
|
5324
|
+
operation: 'start',
|
|
5325
|
+
accountAddress,
|
|
5326
|
+
chainName,
|
|
5327
|
+
dynamicRequestId,
|
|
5328
|
+
attemptCount: messages.length,
|
|
5329
|
+
staleShareRetry,
|
|
5330
|
+
roomIdRetry
|
|
5331
|
+
}, this.getTraceContext(traceContext)));
|
|
5332
|
+
try {
|
|
5333
|
+
const { wallet, derivationPath, clientKeyShares, timings: { startedAt, passwordVerifiedAt, walletLoadedAt, sharesReadyAt } } = await this.prepareWalletForSign({
|
|
5334
|
+
accountAddress,
|
|
5335
|
+
password,
|
|
5336
|
+
signedSessionId,
|
|
5337
|
+
onSignPhase
|
|
5338
|
+
});
|
|
5339
|
+
stage = 'resolveCachedRoom';
|
|
5340
|
+
onSignPhase == null ? void 0 : onSignPhase('resolveCachedRoom');
|
|
5341
|
+
const roomIds = roomIdRetry ? undefined : await this.getCachedRoomsForSignAndBroadcast(wallet, accountAddress, chainName, messages.length, dynamicRequestId, traceContext);
|
|
5342
|
+
roomIdSource = roomIds ? 'cache' : 'server';
|
|
5343
|
+
const roomsResolvedAt = Date.now();
|
|
5344
|
+
this.logSlowSignPreflight({
|
|
5345
|
+
accountAddress,
|
|
5346
|
+
chainName,
|
|
5347
|
+
dynamicRequestId,
|
|
5348
|
+
traceContext,
|
|
5349
|
+
verifyPasswordMs: passwordVerifiedAt - startedAt,
|
|
5350
|
+
getWalletMs: walletLoadedAt - passwordVerifiedAt,
|
|
5351
|
+
ensureClientShareMs: sharesReadyAt - walletLoadedAt,
|
|
5352
|
+
resolveCachedRoomMs: roomsResolvedAt - sharesReadyAt,
|
|
5353
|
+
totalMs: roomsResolvedAt - startedAt
|
|
5354
|
+
});
|
|
5355
|
+
stage = 'serverSign';
|
|
5356
|
+
onSignPhase == null ? void 0 : onSignPhase('serverSign');
|
|
5357
|
+
const record = await this.serverSignAndBroadcast({
|
|
5358
|
+
walletId: wallet.walletId,
|
|
5359
|
+
shareSetId: wallet.shareSetId,
|
|
5360
|
+
contexts,
|
|
5361
|
+
broadcastOptions,
|
|
5362
|
+
messages,
|
|
5363
|
+
roomIds,
|
|
5364
|
+
requestedCertainty,
|
|
5365
|
+
externalReference,
|
|
5366
|
+
idempotencyKey,
|
|
5367
|
+
mfaToken,
|
|
5368
|
+
elevatedAccessToken,
|
|
5369
|
+
approvalQuorum,
|
|
5370
|
+
onError,
|
|
5371
|
+
dynamicRequestId,
|
|
5372
|
+
traceContext
|
|
5373
|
+
});
|
|
5374
|
+
var _record_rooms;
|
|
5375
|
+
const rooms = (_record_rooms = record.rooms) != null ? _record_rooms : [];
|
|
5376
|
+
var _roomIds_length;
|
|
5377
|
+
this.logger.info('[DynamicWaasWalletClient] Sign and broadcast accepted', _extends({
|
|
5378
|
+
key: 'signAndBroadcast',
|
|
5379
|
+
operation: 'accepted',
|
|
5380
|
+
accountAddress,
|
|
5381
|
+
chainName,
|
|
5382
|
+
dynamicRequestId,
|
|
5383
|
+
transactionId: record.transactionId,
|
|
5384
|
+
transactionStatus: record.status,
|
|
5385
|
+
attemptCount: messages.length,
|
|
5386
|
+
roomIdSource,
|
|
5387
|
+
roomsSent: (_roomIds_length = roomIds == null ? void 0 : roomIds.length) != null ? _roomIds_length : 0,
|
|
5388
|
+
roomsReturned: rooms.length,
|
|
5389
|
+
// A retry of an existing row names that row's rooms, not the ones just sent.
|
|
5390
|
+
returnedRoomsMatchSent: roomIds ? roomIds.every((roomId, index)=>{
|
|
5391
|
+
var _rooms_index;
|
|
5392
|
+
return ((_rooms_index = rooms[index]) == null ? void 0 : _rooms_index.roomId) === roomId;
|
|
5393
|
+
}) : undefined
|
|
5394
|
+
}, this.getTraceContext(traceContext)));
|
|
5395
|
+
// A retry of a finished request returns the record without rooms.
|
|
5396
|
+
if (record.status !== 'pending_signature') {
|
|
5397
|
+
return record;
|
|
5398
|
+
}
|
|
5399
|
+
if (rooms.length !== messages.length) {
|
|
5400
|
+
throw new Error(`Expected ${messages.length} signing rooms, got ${rooms.length} for transaction ${record.transactionId}`);
|
|
5401
|
+
}
|
|
5402
|
+
stage = 'ceremony';
|
|
5403
|
+
onSignPhase == null ? void 0 : onSignPhase('ceremony');
|
|
5404
|
+
const ceremonies = Promise.all(rooms.map((room, index)=>this.clientSign({
|
|
5405
|
+
chainName,
|
|
5406
|
+
message: messages[index],
|
|
5407
|
+
roomId: room.roomId,
|
|
5408
|
+
keyShare: clientKeyShares[0],
|
|
5409
|
+
derivationPath,
|
|
5410
|
+
isFormatted: false,
|
|
5411
|
+
dynamicRequestId,
|
|
5412
|
+
traceContext,
|
|
5413
|
+
variant: wallet.variant,
|
|
5414
|
+
// Nothing mints a replacement room for a broadcast attempt.
|
|
5415
|
+
skipForwardMPC: true
|
|
5416
|
+
})));
|
|
5417
|
+
const getClientSignMismatch = this.trackClientSignMismatch(ceremonies, {
|
|
5418
|
+
accountAddress,
|
|
5419
|
+
chainName,
|
|
5420
|
+
password,
|
|
5421
|
+
signedSessionId,
|
|
5422
|
+
mfaToken,
|
|
5423
|
+
dynamicRequestId,
|
|
5424
|
+
traceContext
|
|
5425
|
+
});
|
|
5426
|
+
try {
|
|
5427
|
+
await ceremonies;
|
|
5428
|
+
} catch (signError) {
|
|
5429
|
+
stage = 'healAfterFailure';
|
|
5430
|
+
await this.healAfterSignFailure({
|
|
5431
|
+
signError,
|
|
5432
|
+
clientSignMismatch: getClientSignMismatch(),
|
|
5433
|
+
accountAddress,
|
|
5434
|
+
chainName,
|
|
5435
|
+
password,
|
|
5436
|
+
signedSessionId,
|
|
5437
|
+
mfaToken,
|
|
5438
|
+
dynamicRequestId,
|
|
5439
|
+
traceContext
|
|
5440
|
+
});
|
|
5441
|
+
stage = 'ceremony';
|
|
5442
|
+
throw signError;
|
|
5443
|
+
}
|
|
5444
|
+
this.logger.info('[DynamicWaasWalletClient] Client sign completed', _extends({
|
|
5445
|
+
accountAddress,
|
|
5446
|
+
chainName,
|
|
5447
|
+
roomIds: rooms.map((room)=>room.roomId),
|
|
5448
|
+
dynamicRequestId,
|
|
5449
|
+
key: 'signAndBroadcast',
|
|
5450
|
+
operation: 'clientSign'
|
|
5451
|
+
}, this.getTraceContext(traceContext)));
|
|
5452
|
+
return record;
|
|
5453
|
+
} catch (error) {
|
|
5454
|
+
const retryParams = {
|
|
5455
|
+
accountAddress,
|
|
5456
|
+
chainName,
|
|
5457
|
+
contexts,
|
|
5458
|
+
broadcastOptions,
|
|
5459
|
+
messages,
|
|
5460
|
+
password,
|
|
5461
|
+
signedSessionId,
|
|
5462
|
+
mfaToken,
|
|
5463
|
+
elevatedAccessToken,
|
|
5464
|
+
approvalQuorum,
|
|
5465
|
+
requestedCertainty,
|
|
5466
|
+
externalReference,
|
|
5467
|
+
idempotencyKey,
|
|
5468
|
+
onError,
|
|
5469
|
+
traceContext,
|
|
5470
|
+
onSignPhase
|
|
5471
|
+
};
|
|
5472
|
+
// The server rejected the local shares as stale before opening any room. Heal them
|
|
5473
|
+
// from backup and retry once with the same idempotency key, as plain signing does.
|
|
5474
|
+
if (!staleShareRetry && isStaleClientSharesError(error)) {
|
|
5475
|
+
await this.healStaleShares({
|
|
5476
|
+
accountAddress,
|
|
5477
|
+
chainName,
|
|
5478
|
+
password,
|
|
5479
|
+
signedSessionId,
|
|
5480
|
+
walletOperation: core.WalletOperation.SIGN_MESSAGE,
|
|
5481
|
+
mfaToken,
|
|
5482
|
+
dynamicRequestId,
|
|
5483
|
+
traceContext,
|
|
5484
|
+
trigger: 'server 409 stale-shares preflight (sign and broadcast)',
|
|
5485
|
+
confirmedMismatch: true
|
|
5486
|
+
});
|
|
5487
|
+
this.logger.info('[StaleShareCheck] retrying sign and broadcast after 409 stale-shares preflight', _extends({
|
|
5488
|
+
key: 'signAndBroadcast',
|
|
5489
|
+
operation: 'staleShareRetry',
|
|
5490
|
+
accountAddress,
|
|
5491
|
+
chainName,
|
|
5492
|
+
dynamicRequestId,
|
|
5493
|
+
stage
|
|
5494
|
+
}, this.getTraceContext(traceContext)));
|
|
5495
|
+
return await this.internalSignAndBroadcast(_extends({}, retryParams, {
|
|
5496
|
+
staleShareRetry: true,
|
|
5497
|
+
roomIdRetry
|
|
5498
|
+
}));
|
|
5499
|
+
}
|
|
5500
|
+
// A cached room was already claimed by an earlier ceremony. Retry once with the same
|
|
5501
|
+
// idempotency key and no cached rooms, so the server opens fresh ones.
|
|
5502
|
+
if (!roomIdRetry && isRoomIdAlreadyUsedError(error)) {
|
|
5503
|
+
this.logger.info('[RoomCache] retrying sign and broadcast without cached rooms after 409 ROOM_ID_ALREADY_USED', _extends({
|
|
5504
|
+
key: 'signAndBroadcast',
|
|
5505
|
+
operation: 'roomIdRetry',
|
|
5506
|
+
accountAddress,
|
|
5507
|
+
chainName,
|
|
5508
|
+
dynamicRequestId,
|
|
5509
|
+
stage
|
|
5510
|
+
}, this.getTraceContext(traceContext)));
|
|
5511
|
+
return await this.internalSignAndBroadcast(_extends({}, retryParams, {
|
|
5512
|
+
staleShareRetry,
|
|
5513
|
+
roomIdRetry: true
|
|
5514
|
+
}));
|
|
5515
|
+
}
|
|
5516
|
+
this.reportSignError({
|
|
5517
|
+
error,
|
|
5518
|
+
accountAddress,
|
|
5519
|
+
chainName,
|
|
5520
|
+
isFormatted: false,
|
|
5521
|
+
dynamicRequestId,
|
|
5522
|
+
traceContext,
|
|
5523
|
+
operation: 'signAndBroadcast',
|
|
5524
|
+
details: {
|
|
5525
|
+
stage,
|
|
5526
|
+
attemptCount: messages.length,
|
|
5527
|
+
roomIdSource,
|
|
5528
|
+
staleShareRetry,
|
|
5529
|
+
roomIdRetry
|
|
5530
|
+
}
|
|
5531
|
+
});
|
|
5532
|
+
core.attachRequestIds(error, {
|
|
5533
|
+
traceId: traceContext.traceId,
|
|
5534
|
+
requestId: dynamicRequestId
|
|
5535
|
+
});
|
|
5536
|
+
throw error;
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
/**
|
|
5137
5540
|
* Reports a sign that spent a long time before reaching the network. Emitted
|
|
5138
5541
|
* once per slow sign with the phase breakdown, so a stall can be attributed
|
|
5139
5542
|
* without guessing: a shared dependency shows up as many signs reporting a
|
|
@@ -5157,11 +5560,83 @@ class DynamicWalletClient {
|
|
|
5157
5560
|
dynamicRequestId
|
|
5158
5561
|
}, this.getTraceContext(traceContext)));
|
|
5159
5562
|
}
|
|
5160
|
-
|
|
5563
|
+
/**
|
|
5564
|
+
* Takes one already-opened room per message from the local pool, or none: a partial set saves nothing, so the server opens them all instead.
|
|
5565
|
+
*
|
|
5566
|
+
* @param wallet - The wallet signing; its key scheme decides which rooms fit.
|
|
5567
|
+
* @param accountAddress - The wallet's address, for logs.
|
|
5568
|
+
* @param chainName - The wallet's chain, for logs.
|
|
5569
|
+
* @param count - How many rooms are needed: one per message.
|
|
5570
|
+
* @param dynamicRequestId - Id of this request, so the pool's logs join the sign's.
|
|
5571
|
+
* @param traceContext - Trace ids for the same reason.
|
|
5572
|
+
* @returns The room ids, or `undefined` when the pool cannot cover every message.
|
|
5573
|
+
*/ async getCachedRoomsForSignAndBroadcast(wallet, accountAddress, chainName, count, dynamicRequestId, traceContext) {
|
|
5574
|
+
const logContext = _extends({
|
|
5575
|
+
dynamicRequestId
|
|
5576
|
+
}, this.getTraceContext(traceContext));
|
|
5161
5577
|
if (this.featureFlags[core.FEATURE_FLAGS.ENABLE_ROOM_CACHE_FLAG] !== true) {
|
|
5578
|
+
this.logCachedRoomsUnavailable({
|
|
5579
|
+
reason: 'cache_disabled',
|
|
5580
|
+
accountAddress,
|
|
5581
|
+
chainName,
|
|
5582
|
+
count,
|
|
5583
|
+
logContext
|
|
5584
|
+
});
|
|
5162
5585
|
return undefined;
|
|
5163
5586
|
}
|
|
5164
|
-
const
|
|
5587
|
+
const rooms = await this.takeRooms({
|
|
5588
|
+
roomType: RoomTypeEnum.RoomTypeEnum.Threshold,
|
|
5589
|
+
thresholdSignatureScheme: wallet.thresholdSignatureScheme,
|
|
5590
|
+
count,
|
|
5591
|
+
logContext
|
|
5592
|
+
});
|
|
5593
|
+
if (rooms.length === 0) {
|
|
5594
|
+
this.logCachedRoomsUnavailable({
|
|
5595
|
+
reason: 'pool_too_small',
|
|
5596
|
+
accountAddress,
|
|
5597
|
+
chainName,
|
|
5598
|
+
count,
|
|
5599
|
+
logContext
|
|
5600
|
+
});
|
|
5601
|
+
return undefined;
|
|
5602
|
+
}
|
|
5603
|
+
const roomIds = rooms.map((room)=>room.roomId);
|
|
5604
|
+
this.logger.info('[DynamicWaasWalletClient] Using cached rooms', _extends({
|
|
5605
|
+
key: 'signAndBroadcast',
|
|
5606
|
+
operation: 'cachedRoomsFound',
|
|
5607
|
+
accountAddress,
|
|
5608
|
+
chainName,
|
|
5609
|
+
walletId: wallet.walletId,
|
|
5610
|
+
roomIds
|
|
5611
|
+
}, logContext));
|
|
5612
|
+
return roomIds;
|
|
5613
|
+
}
|
|
5614
|
+
/**
|
|
5615
|
+
* Logs why no cached rooms were sent, so how often the cache covers a request can be measured.
|
|
5616
|
+
*
|
|
5617
|
+
* @param params.reason - `cache_disabled` or `pool_too_small`.
|
|
5618
|
+
* @param params.accountAddress - The wallet's address.
|
|
5619
|
+
* @param params.chainName - The wallet's chain.
|
|
5620
|
+
* @param params.count - How many rooms were needed.
|
|
5621
|
+
* @param params.logContext - Request and trace ids to attach.
|
|
5622
|
+
*/ logCachedRoomsUnavailable({ reason, accountAddress, chainName, count, logContext }) {
|
|
5623
|
+
this.logger.info('[DynamicWaasWalletClient] Cached rooms unavailable; the server opens them', _extends({
|
|
5624
|
+
key: 'signAndBroadcast',
|
|
5625
|
+
operation: 'cachedRoomsUnavailable',
|
|
5626
|
+
reason,
|
|
5627
|
+
accountAddress,
|
|
5628
|
+
chainName,
|
|
5629
|
+
requestedCount: count
|
|
5630
|
+
}, logContext));
|
|
5631
|
+
}
|
|
5632
|
+
async getCachedRoomForSign(wallet, accountAddress, chainName, dynamicRequestId, traceContext) {
|
|
5633
|
+
if (this.featureFlags[core.FEATURE_FLAGS.ENABLE_ROOM_CACHE_FLAG] !== true) {
|
|
5634
|
+
return undefined;
|
|
5635
|
+
}
|
|
5636
|
+
const logContext = _extends({
|
|
5637
|
+
dynamicRequestId
|
|
5638
|
+
}, this.getTraceContext(traceContext));
|
|
5639
|
+
const room = await this.getRoom(RoomTypeEnum.RoomTypeEnum.Threshold, wallet.thresholdSignatureScheme, logContext);
|
|
5165
5640
|
const roomId = room == null ? void 0 : room.roomId;
|
|
5166
5641
|
if (roomId) {
|
|
5167
5642
|
this.logger.info('[DynamicWaasWalletClient] Using cached room', _extends({
|
|
@@ -5171,7 +5646,7 @@ class DynamicWalletClient {
|
|
|
5171
5646
|
chainName,
|
|
5172
5647
|
walletId: wallet.walletId,
|
|
5173
5648
|
roomId
|
|
5174
|
-
},
|
|
5649
|
+
}, logContext));
|
|
5175
5650
|
}
|
|
5176
5651
|
return roomId;
|
|
5177
5652
|
}
|
|
@@ -5238,38 +5713,130 @@ class DynamicWalletClient {
|
|
|
5238
5713
|
clearTimeout(warningTimer);
|
|
5239
5714
|
}
|
|
5240
5715
|
}
|
|
5716
|
+
/** Verifies the password, loads the wallet and makes sure its client share exists: the start of every sign. */ async prepareWalletForSign({ accountAddress, password, signedSessionId, onSignPhase }) {
|
|
5717
|
+
const startedAt = Date.now();
|
|
5718
|
+
onSignPhase == null ? void 0 : onSignPhase('verifyPassword');
|
|
5719
|
+
await this.verifyPassword({
|
|
5720
|
+
accountAddress,
|
|
5721
|
+
password,
|
|
5722
|
+
signedSessionId
|
|
5723
|
+
});
|
|
5724
|
+
const passwordVerifiedAt = Date.now();
|
|
5725
|
+
onSignPhase == null ? void 0 : onSignPhase('getWallet');
|
|
5726
|
+
const wallet = await this.getWallet({
|
|
5727
|
+
accountAddress,
|
|
5728
|
+
password,
|
|
5729
|
+
walletOperation: core.WalletOperation.SIGN_MESSAGE,
|
|
5730
|
+
signedSessionId
|
|
5731
|
+
});
|
|
5732
|
+
const walletLoadedAt = Date.now();
|
|
5733
|
+
const derivationPath = wallet.derivationPath && wallet.derivationPath != '' ? new Uint32Array(Object.values(JSON.parse(wallet.derivationPath))) : undefined;
|
|
5734
|
+
onSignPhase == null ? void 0 : onSignPhase('ensureClientShare');
|
|
5735
|
+
const clientKeyShares = await this.ensureClientShare(accountAddress);
|
|
5736
|
+
const sharesReadyAt = Date.now();
|
|
5737
|
+
return {
|
|
5738
|
+
wallet,
|
|
5739
|
+
derivationPath,
|
|
5740
|
+
clientKeyShares,
|
|
5741
|
+
timings: {
|
|
5742
|
+
startedAt,
|
|
5743
|
+
passwordVerifiedAt,
|
|
5744
|
+
walletLoadedAt,
|
|
5745
|
+
sharesReadyAt
|
|
5746
|
+
}
|
|
5747
|
+
};
|
|
5748
|
+
}
|
|
5749
|
+
/**
|
|
5750
|
+
* Watches a ceremony for a client-side ERROR_PUBLIC_KEY_MISMATCH, the only place a stale
|
|
5751
|
+
* local share surfaces when a generic server error wins the race and masks it. Schedules a
|
|
5752
|
+
* heal as soon as it settles and returns a reader for the mismatch.
|
|
5753
|
+
*/ trackClientSignMismatch(clientSignPromise, healContext) {
|
|
5754
|
+
let mismatch;
|
|
5755
|
+
clientSignPromise.catch((error)=>{
|
|
5756
|
+
if (isPublicKeyMismatchError(error)) {
|
|
5757
|
+
mismatch = error;
|
|
5758
|
+
this.logger.warn('[StaleShareCheck] clientSign rejected with public-key mismatch — scheduling share recovery', _extends({
|
|
5759
|
+
accountAddress: healContext.accountAddress,
|
|
5760
|
+
chainName: healContext.chainName,
|
|
5761
|
+
dynamicRequestId: healContext.dynamicRequestId
|
|
5762
|
+
}, this.getTraceContext(healContext.traceContext)));
|
|
5763
|
+
void this.healStaleShares(_extends({}, healContext, {
|
|
5764
|
+
walletOperation: core.WalletOperation.SIGN_MESSAGE,
|
|
5765
|
+
trigger: 'clientSign rejection',
|
|
5766
|
+
confirmedMismatch: true
|
|
5767
|
+
}));
|
|
5768
|
+
}
|
|
5769
|
+
});
|
|
5770
|
+
return ()=>mismatch;
|
|
5771
|
+
}
|
|
5772
|
+
/**
|
|
5773
|
+
* Heals stale shares after a failed ceremony so the caller's retry succeeds. The ground-truth
|
|
5774
|
+
* check runs whatever the raced error was, because a generic server error often wins the race
|
|
5775
|
+
* while the client-side mismatch settles later. It no-ops when shares are fresh.
|
|
5776
|
+
*/ async healAfterSignFailure(_param) {
|
|
5777
|
+
var { signError, clientSignMismatch } = _param, healContext = _object_without_properties_loose(_param, [
|
|
5778
|
+
"signError",
|
|
5779
|
+
"clientSignMismatch"
|
|
5780
|
+
]);
|
|
5781
|
+
await this.healStaleShares(_extends({}, healContext, {
|
|
5782
|
+
walletOperation: core.WalletOperation.SIGN_MESSAGE,
|
|
5783
|
+
trigger: 'sign failure',
|
|
5784
|
+
confirmedMismatch: isPublicKeyMismatchError(signError) || clientSignMismatch !== undefined
|
|
5785
|
+
}));
|
|
5786
|
+
}
|
|
5787
|
+
/**
|
|
5788
|
+
* Logs a failed sign or sign-and-broadcast with the same fields on both, so failures can be filtered by operation, HTTP status and backend code.
|
|
5789
|
+
*
|
|
5790
|
+
* @param params.error - What failed.
|
|
5791
|
+
* @param params.accountAddress - The wallet's address.
|
|
5792
|
+
* @param params.chainName - The wallet's chain.
|
|
5793
|
+
* @param params.isFormatted - Whether the message was a hash.
|
|
5794
|
+
* @param params.dynamicRequestId - Id of the failed attempt.
|
|
5795
|
+
* @param params.traceContext - Ids shared by every attempt of this operation.
|
|
5796
|
+
* @param params.operation - Which call failed. Defaults to `sign`.
|
|
5797
|
+
* @param params.details - Where and how it failed, as short values (never request or response bodies).
|
|
5798
|
+
*/ reportSignError({ error, accountAddress, chainName, isFormatted, dynamicRequestId, traceContext, operation = 'sign', details }) {
|
|
5799
|
+
logError({
|
|
5800
|
+
message: `Error in ${operation}`,
|
|
5801
|
+
error: error,
|
|
5802
|
+
context: _extends({
|
|
5803
|
+
accountAddress,
|
|
5804
|
+
chainName,
|
|
5805
|
+
isFormatted: isFormatted ? 'true' : 'false',
|
|
5806
|
+
dynamicRequestId,
|
|
5807
|
+
operation
|
|
5808
|
+
}, describeErrorForLog(error), details, this.getTraceContext(traceContext))
|
|
5809
|
+
});
|
|
5810
|
+
}
|
|
5241
5811
|
async internalSign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, approvalQuorum, onError, traceContext, bitcoinConfig, onSignPhase, onSignDetails, staleShareRetry = false, roomIdRetry = false }) {
|
|
5242
5812
|
const dynamicRequestId = uuid.v4();
|
|
5243
5813
|
onSignDetails == null ? void 0 : onSignDetails({
|
|
5244
5814
|
dynamicRequestId
|
|
5245
5815
|
});
|
|
5816
|
+
// Retries below reuse this, so every attempt of one sign shares a trace id.
|
|
5817
|
+
traceContext = core.withTraceId(traceContext);
|
|
5818
|
+
this.logger.info('[DynamicWaasWalletClient] Sign started', _extends({
|
|
5819
|
+
key: 'sign',
|
|
5820
|
+
operation: 'start',
|
|
5821
|
+
accountAddress,
|
|
5822
|
+
chainName,
|
|
5823
|
+
isFormatted,
|
|
5824
|
+
dynamicRequestId,
|
|
5825
|
+
staleShareRetry,
|
|
5826
|
+
roomIdRetry
|
|
5827
|
+
}, this.getTraceContext(traceContext)));
|
|
5246
5828
|
try {
|
|
5247
5829
|
// Pre-flight phase boundaries. Nothing reaches the network until
|
|
5248
5830
|
// serverSign below, so a stall here is invisible in server logs — these
|
|
5249
5831
|
// timestamps are what attribute it to a phase.
|
|
5250
|
-
const preflightStartedAt =
|
|
5251
|
-
onSignPhase == null ? void 0 : onSignPhase('verifyPassword');
|
|
5252
|
-
await this.verifyPassword({
|
|
5253
|
-
accountAddress,
|
|
5254
|
-
password,
|
|
5255
|
-
signedSessionId
|
|
5256
|
-
});
|
|
5257
|
-
const passwordVerifiedAt = Date.now();
|
|
5258
|
-
onSignPhase == null ? void 0 : onSignPhase('getWallet');
|
|
5259
|
-
const wallet = await this.getWallet({
|
|
5832
|
+
const { wallet, derivationPath, clientKeyShares, timings: { startedAt: preflightStartedAt, passwordVerifiedAt, walletLoadedAt, sharesReadyAt } } = await this.prepareWalletForSign({
|
|
5260
5833
|
accountAddress,
|
|
5261
5834
|
password,
|
|
5262
|
-
|
|
5263
|
-
|
|
5835
|
+
signedSessionId,
|
|
5836
|
+
onSignPhase
|
|
5264
5837
|
});
|
|
5265
|
-
const walletLoadedAt = Date.now();
|
|
5266
|
-
const derivationPath = wallet.derivationPath && wallet.derivationPath != '' ? new Uint32Array(Object.values(JSON.parse(wallet.derivationPath))) : undefined;
|
|
5267
|
-
// Ensure client key shares exist before hitting the API
|
|
5268
|
-
onSignPhase == null ? void 0 : onSignPhase('ensureClientShare');
|
|
5269
|
-
const clientKeyShares = await this.ensureClientShare(accountAddress);
|
|
5270
|
-
const sharesReadyAt = Date.now();
|
|
5271
5838
|
onSignPhase == null ? void 0 : onSignPhase('resolveCachedRoom');
|
|
5272
|
-
const cachedRoomId = roomIdRetry ? undefined : await this.getCachedRoomForSign(wallet, accountAddress, chainName, traceContext);
|
|
5839
|
+
const cachedRoomId = roomIdRetry ? undefined : await this.getCachedRoomForSign(wallet, accountAddress, chainName, dynamicRequestId, traceContext);
|
|
5273
5840
|
const roomResolvedAt = Date.now();
|
|
5274
5841
|
this.logSlowSignPreflight({
|
|
5275
5842
|
accountAddress,
|
|
@@ -5355,9 +5922,7 @@ class DynamicWalletClient {
|
|
|
5355
5922
|
operation: 'serverSign'
|
|
5356
5923
|
}, this.getTraceContext(traceContext)));
|
|
5357
5924
|
let signature;
|
|
5358
|
-
|
|
5359
|
-
// detect a client-side mismatch masked by a generic SSE race winner.
|
|
5360
|
-
let clientSignMismatchError;
|
|
5925
|
+
let getClientSignMismatch = ()=>undefined;
|
|
5361
5926
|
try {
|
|
5362
5927
|
// Race clientSign against SSE errors so that server-side failures
|
|
5363
5928
|
// (e.g. policy violations) immediately reject instead of hanging
|
|
@@ -5420,28 +5985,15 @@ class DynamicWalletClient {
|
|
|
5420
5985
|
// a stale-local-share failure surfaces, since the server's generic SSE
|
|
5421
5986
|
// error wins the race and masks it. Schedule recovery regardless of
|
|
5422
5987
|
// which promise wins or when this rejection settles.
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
accountAddress,
|
|
5433
|
-
chainName,
|
|
5434
|
-
password,
|
|
5435
|
-
signedSessionId,
|
|
5436
|
-
walletOperation: core.WalletOperation.SIGN_MESSAGE,
|
|
5437
|
-
mfaToken,
|
|
5438
|
-
dynamicRequestId,
|
|
5439
|
-
traceContext,
|
|
5440
|
-
bitcoinConfig,
|
|
5441
|
-
trigger: 'clientSign rejection',
|
|
5442
|
-
confirmedMismatch: true
|
|
5443
|
-
});
|
|
5444
|
-
}
|
|
5988
|
+
getClientSignMismatch = this.trackClientSignMismatch(clientSignPromise, {
|
|
5989
|
+
accountAddress,
|
|
5990
|
+
chainName,
|
|
5991
|
+
password,
|
|
5992
|
+
signedSessionId,
|
|
5993
|
+
mfaToken,
|
|
5994
|
+
dynamicRequestId,
|
|
5995
|
+
traceContext,
|
|
5996
|
+
bitcoinConfig
|
|
5445
5997
|
});
|
|
5446
5998
|
signature = await Promise.race([
|
|
5447
5999
|
clientSignPromise,
|
|
@@ -5455,23 +6007,17 @@ class DynamicWalletClient {
|
|
|
5455
6007
|
if (isStaleClientSharesError(signError) || isRoomIdAlreadyUsedError(signError)) {
|
|
5456
6008
|
throw signError;
|
|
5457
6009
|
}
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
// settles later, so error-string inspection alone is timing-dependent.
|
|
5462
|
-
// The check no-ops when shares are fresh.
|
|
5463
|
-
await this.healStaleShares({
|
|
6010
|
+
await this.healAfterSignFailure({
|
|
6011
|
+
signError,
|
|
6012
|
+
clientSignMismatch: getClientSignMismatch(),
|
|
5464
6013
|
accountAddress,
|
|
5465
6014
|
chainName,
|
|
5466
6015
|
password,
|
|
5467
6016
|
signedSessionId,
|
|
5468
|
-
walletOperation: core.WalletOperation.SIGN_MESSAGE,
|
|
5469
6017
|
mfaToken,
|
|
5470
6018
|
dynamicRequestId,
|
|
5471
6019
|
traceContext,
|
|
5472
|
-
bitcoinConfig
|
|
5473
|
-
trigger: 'sign failure',
|
|
5474
|
-
confirmedMismatch: isPublicKeyMismatchError(signError) || clientSignMismatchError !== undefined
|
|
6020
|
+
bitcoinConfig
|
|
5475
6021
|
});
|
|
5476
6022
|
throw signError;
|
|
5477
6023
|
}
|
|
@@ -5482,6 +6028,7 @@ class DynamicWalletClient {
|
|
|
5482
6028
|
roomId,
|
|
5483
6029
|
derivationPath,
|
|
5484
6030
|
isFormatted,
|
|
6031
|
+
dynamicRequestId,
|
|
5485
6032
|
key: 'sign',
|
|
5486
6033
|
operation: 'clientSign'
|
|
5487
6034
|
}, this.getTraceContext(traceContext)));
|
|
@@ -5565,15 +6112,21 @@ class DynamicWalletClient {
|
|
|
5565
6112
|
roomIdRetry: true
|
|
5566
6113
|
});
|
|
5567
6114
|
}
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
6115
|
+
this.reportSignError({
|
|
6116
|
+
error,
|
|
6117
|
+
accountAddress,
|
|
6118
|
+
chainName,
|
|
6119
|
+
isFormatted,
|
|
6120
|
+
dynamicRequestId,
|
|
6121
|
+
traceContext,
|
|
6122
|
+
details: {
|
|
6123
|
+
staleShareRetry,
|
|
6124
|
+
roomIdRetry
|
|
6125
|
+
}
|
|
6126
|
+
});
|
|
6127
|
+
core.attachRequestIds(error, {
|
|
6128
|
+
traceId: traceContext == null ? void 0 : traceContext.traceId,
|
|
6129
|
+
requestId: dynamicRequestId
|
|
5577
6130
|
});
|
|
5578
6131
|
throw error;
|
|
5579
6132
|
}
|
|
@@ -7138,7 +7691,42 @@ class DynamicWalletClient {
|
|
|
7138
7691
|
}
|
|
7139
7692
|
return new web.BIP340KeygenResult(extractedPubkey, secretShare);
|
|
7140
7693
|
}
|
|
7141
|
-
|
|
7694
|
+
/**
|
|
7695
|
+
* Builds the export Intent for a business-account wallet, to be signed with
|
|
7696
|
+
* the session key and sent as `approvalQuorum` on the export call.
|
|
7697
|
+
*
|
|
7698
|
+
* The `exportId` is the key share's keygen id, so it comes out the same here
|
|
7699
|
+
* and on the export call; the Intent built here binds it. Makes no API call
|
|
7700
|
+
* and starts no ceremony.
|
|
7701
|
+
*/ async prepareExportIntent({ accountAddress, chainName, bitcoinConfig, password = undefined, signedSessionId, businessAccountId, initiatorUserId, completeBy }) {
|
|
7702
|
+
const wallet = await this.getWallet({
|
|
7703
|
+
accountAddress,
|
|
7704
|
+
password,
|
|
7705
|
+
walletOperation: core.WalletOperation.EXPORT_PRIVATE_KEY,
|
|
7706
|
+
signedSessionId
|
|
7707
|
+
});
|
|
7708
|
+
const mpcSigner = getMPCSigner({
|
|
7709
|
+
chainName,
|
|
7710
|
+
baseRelayUrl: this.baseMPCRelayApiUrl,
|
|
7711
|
+
bitcoinConfig,
|
|
7712
|
+
variant: wallet.variant
|
|
7713
|
+
});
|
|
7714
|
+
const reconstructedKeyShare = await this.getReconstructedKeyShare(accountAddress, mpcSigner);
|
|
7715
|
+
const exportId = await this.getExportId({
|
|
7716
|
+
chainName,
|
|
7717
|
+
clientKeyShare: reconstructedKeyShare,
|
|
7718
|
+
bitcoinConfig,
|
|
7719
|
+
accountAddress: wallet.accountAddress
|
|
7720
|
+
});
|
|
7721
|
+
return core.createExportIntent({
|
|
7722
|
+
accountAddress,
|
|
7723
|
+
businessAccountId,
|
|
7724
|
+
completeBy,
|
|
7725
|
+
exportId,
|
|
7726
|
+
initiatorUserId
|
|
7727
|
+
});
|
|
7728
|
+
}
|
|
7729
|
+
async exportKey({ accountAddress, chainName, bitcoinConfig, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, proposalId, traceContext }) {
|
|
7142
7730
|
// Public entry point delegates to the private implementation. Keeping the
|
|
7143
7731
|
// `staleShareRetry` flag off the public signature mirrors sign/refresh/
|
|
7144
7732
|
// reshare and ensures the transparent 409 retry can't be suppressed by
|
|
@@ -7151,10 +7739,12 @@ class DynamicWalletClient {
|
|
|
7151
7739
|
signedSessionId,
|
|
7152
7740
|
mfaToken,
|
|
7153
7741
|
elevatedAccessToken,
|
|
7742
|
+
approvalQuorum,
|
|
7743
|
+
proposalId,
|
|
7154
7744
|
traceContext
|
|
7155
7745
|
});
|
|
7156
7746
|
}
|
|
7157
|
-
async internalExportKey({ accountAddress, chainName, bitcoinConfig, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, traceContext, staleShareRetry = false }) {
|
|
7747
|
+
async internalExportKey({ accountAddress, chainName, bitcoinConfig, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, proposalId, traceContext, staleShareRetry = false }) {
|
|
7158
7748
|
const dynamicRequestId = uuid.v4();
|
|
7159
7749
|
try {
|
|
7160
7750
|
const wallet = await this.getWallet({
|
|
@@ -7181,7 +7771,10 @@ class DynamicWalletClient {
|
|
|
7181
7771
|
// transport has no such stream, so a post-init denial surfaces as an
|
|
7182
7772
|
// MPC relay timeout there.
|
|
7183
7773
|
const sseErrorGate = core.createRejectablePromise();
|
|
7184
|
-
|
|
7774
|
+
// An export that carries approvals stays on SSE: the server answers the
|
|
7775
|
+
// approval round (`consent_required`) on that stream, and the HTTP route
|
|
7776
|
+
// refuses `approvalQuorum` / `proposalId`.
|
|
7777
|
+
const useHttpTransport = this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT] === true && this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT_EXPORT] === true && !approvalQuorum && !proposalId;
|
|
7185
7778
|
// Fresh key per invocation: the transparent stale-share retry below is a
|
|
7186
7779
|
// NEW ceremony (new shareSetId), so it must not reuse the failed
|
|
7187
7780
|
// attempt's key — the dedup would replay that row's terminal error.
|
|
@@ -7199,6 +7792,8 @@ class DynamicWalletClient {
|
|
|
7199
7792
|
const data = useHttpTransport ? await this.apiClient.requestExportKeyJson(_extends({}, exportParams, {
|
|
7200
7793
|
idempotencyKey: idempotencyKey
|
|
7201
7794
|
})) : await this.apiClient.exportKey(_extends({}, exportParams, {
|
|
7795
|
+
approvalQuorum,
|
|
7796
|
+
proposalId,
|
|
7202
7797
|
onError: (error)=>sseErrorGate.reject(error)
|
|
7203
7798
|
}));
|
|
7204
7799
|
this.logger.debug('[DynamicWaasWalletClient] Starting export of private key', _extends({
|
|
@@ -7267,6 +7862,8 @@ class DynamicWalletClient {
|
|
|
7267
7862
|
signedSessionId,
|
|
7268
7863
|
mfaToken,
|
|
7269
7864
|
elevatedAccessToken,
|
|
7865
|
+
approvalQuorum,
|
|
7866
|
+
proposalId,
|
|
7270
7867
|
traceContext,
|
|
7271
7868
|
staleShareRetry: true
|
|
7272
7869
|
});
|
|
@@ -10677,8 +11274,25 @@ class DynamicWalletClient {
|
|
|
10677
11274
|
const MPC_SCHEME_CONFIG = core.MPC_CONFIG[thresholdSignatureScheme];
|
|
10678
11275
|
return roomType === RoomTypeEnum.RoomTypeEnum.Threshold ? MPC_SCHEME_CONFIG.threshold : MPC_SCHEME_CONFIG.numberOfParties;
|
|
10679
11276
|
}
|
|
10680
|
-
async getRoom(roomType, thresholdSignatureScheme) {
|
|
10681
|
-
|
|
11277
|
+
async getRoom(roomType, thresholdSignatureScheme, logContext) {
|
|
11278
|
+
const [room] = await this.takeRooms({
|
|
11279
|
+
roomType,
|
|
11280
|
+
thresholdSignatureScheme,
|
|
11281
|
+
count: 1,
|
|
11282
|
+
logContext
|
|
11283
|
+
});
|
|
11284
|
+
return room;
|
|
11285
|
+
}
|
|
11286
|
+
/**
|
|
11287
|
+
* Takes `count` cached rooms, or none when the pool holds fewer: a caller that needs every room gains nothing from some of them, and would spend them.
|
|
11288
|
+
*
|
|
11289
|
+
* @param params.roomType - Which kind of room.
|
|
11290
|
+
* @param params.thresholdSignatureScheme - The key scheme; it decides how many parties the room needs.
|
|
11291
|
+
* @param params.count - How many rooms to take.
|
|
11292
|
+
* @param params.logContext - Request and trace ids, so the pool's log joins the operation's.
|
|
11293
|
+
* @returns The rooms taken, or an empty list.
|
|
11294
|
+
*/ async takeRooms({ roomType, thresholdSignatureScheme, count, logContext }) {
|
|
11295
|
+
var _rooms_, _DynamicWalletClient_rooms_numberOfParties;
|
|
10682
11296
|
const numberOfParties = this.getNumberOfParties(roomType, thresholdSignatureScheme);
|
|
10683
11297
|
const consume = async ()=>{
|
|
10684
11298
|
// Re-read from localStorage rather than the in-memory static so we see rooms
|
|
@@ -10691,29 +11305,34 @@ class DynamicWalletClient {
|
|
|
10691
11305
|
const schemeRooms = storedRooms[numberOfParties] || [];
|
|
10692
11306
|
// Filter out expired rooms and remove the next available room.
|
|
10693
11307
|
const filteredRooms = schemeRooms.filter((room)=>room.expiresAt > now);
|
|
10694
|
-
const
|
|
11308
|
+
const taken = filteredRooms.length >= count ? filteredRooms.splice(0, count) : [];
|
|
10695
11309
|
// Use setRooms() rather than replacing DynamicWalletClient.rooms wholesale.
|
|
10696
11310
|
// setRooms() only mutates the specific numberOfParties key, so rooms for other
|
|
10697
11311
|
// party counts and any rooms written in-memory by a concurrent createRooms()
|
|
10698
11312
|
// call that hasn't flushed to storage yet are preserved. Persistence is also
|
|
10699
11313
|
// routed through the existing persistRooms() chain to keep writes serialized.
|
|
10700
11314
|
await this.setRooms(numberOfParties, filteredRooms);
|
|
10701
|
-
return
|
|
11315
|
+
return taken;
|
|
10702
11316
|
};
|
|
10703
11317
|
// Use the Web Locks API for cross-tab mutual exclusion so that two tabs
|
|
10704
11318
|
// cannot both consume the same cached room. Falls back to a direct call in
|
|
10705
11319
|
// environments where navigator.locks is unavailable (e.g. Node.js tests).
|
|
10706
11320
|
const webLocksAvailable = typeof navigator !== 'undefined' && !!navigator.locks;
|
|
10707
|
-
const
|
|
11321
|
+
const rooms = webLocksAvailable ? await navigator.locks.request(`${this.storageKey}-room-cache`, consume) : await consume();
|
|
10708
11322
|
var _DynamicWalletClient_rooms_numberOfParties_length;
|
|
10709
|
-
this.logger.info('[DynamicWaasWalletClient] getRoom result', {
|
|
11323
|
+
this.logger.info('[DynamicWaasWalletClient] getRoom result', _extends({
|
|
10710
11324
|
key: 'roomCache',
|
|
10711
11325
|
operation: 'getRoom',
|
|
10712
11326
|
numberOfParties,
|
|
10713
|
-
roomId:
|
|
11327
|
+
roomId: (_rooms_ = rooms[0]) == null ? void 0 : _rooms_.roomId,
|
|
11328
|
+
requestedCount: count,
|
|
11329
|
+
takenCount: rooms.length
|
|
11330
|
+
}, count > 1 ? {
|
|
11331
|
+
roomIds: rooms.map((room)=>room.roomId)
|
|
11332
|
+
} : {}, logContext, {
|
|
10714
11333
|
remainingInMemory: (_DynamicWalletClient_rooms_numberOfParties_length = (_DynamicWalletClient_rooms_numberOfParties = DynamicWalletClient.rooms[numberOfParties]) == null ? void 0 : _DynamicWalletClient_rooms_numberOfParties.length) != null ? _DynamicWalletClient_rooms_numberOfParties_length : 0,
|
|
10715
11334
|
webLocksAvailable
|
|
10716
|
-
});
|
|
11335
|
+
}));
|
|
10717
11336
|
// If less than or only 1 room left after removing one, trigger async creation for more rooms.
|
|
10718
11337
|
// The catch prevents an unhandled promise rejection when the API
|
|
10719
11338
|
// rate-limits this best-effort background call — createRooms already
|
|
@@ -10726,7 +11345,7 @@ class DynamicWalletClient {
|
|
|
10726
11345
|
roomCount: ROOM_CACHE_COUNT
|
|
10727
11346
|
}).catch(()=>{});
|
|
10728
11347
|
}
|
|
10729
|
-
return
|
|
11348
|
+
return rooms;
|
|
10730
11349
|
}
|
|
10731
11350
|
getTraceContext(traceContext) {
|
|
10732
11351
|
const now = Date.now();
|
|
@@ -10831,6 +11450,7 @@ class DynamicWalletClient {
|
|
|
10831
11450
|
this.startLoggerContext(authToken);
|
|
10832
11451
|
}
|
|
10833
11452
|
this.forwardMPCEnabled = this.featureFlags && this.featureFlags[core.FEATURE_FLAGS.ENABLE_FORWARD_MPC_CLIENT_FLAG] === true;
|
|
11453
|
+
this.enclaveSnapshot = this.forwardMPCEnabled ? trackForwardMpcEnclave(this.apiClient.forwardMPCClient) : undefined;
|
|
10834
11454
|
if (this.forwardMPCEnabled) {
|
|
10835
11455
|
this.connectForwardMPC();
|
|
10836
11456
|
}
|
|
@@ -11002,6 +11622,7 @@ exports.resolveLogLevel = resolveLogLevel;
|
|
|
11002
11622
|
exports.retryPromise = retryPromise;
|
|
11003
11623
|
exports.shouldReshareToSameBackups = shouldReshareToSameBackups;
|
|
11004
11624
|
exports.timeoutPromise = timeoutPromise;
|
|
11625
|
+
exports.trackForwardMpcEnclave = trackForwardMpcEnclave;
|
|
11005
11626
|
Object.keys(core).forEach(function (k) {
|
|
11006
11627
|
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
11007
11628
|
enumerable: true,
|