@dynamic-labs-wallet/browser 1.1.33 → 1.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs +686 -91
- package/index.esm.js +687 -93
- package/package.json +3 -3
- package/src/client.d.ts +155 -3
- 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/src/utils.d.ts +3 -1
- package/src/utils.d.ts.map +1 -1
package/index.cjs
CHANGED
|
@@ -1804,12 +1804,18 @@ const downloadStringAsFile = ({ filename, content, mimeType = 'application/json'
|
|
|
1804
1804
|
* The SDK heals (re-syncs local shares from backup) and retries internally.
|
|
1805
1805
|
*
|
|
1806
1806
|
* The SSE event-stream layer copies the event's data fields onto the Error
|
|
1807
|
-
* object (see createErrorFromEventData), so `code` is available
|
|
1807
|
+
* object (see createErrorFromEventData), so `code` is available there. The
|
|
1808
|
+
* HTTP (non-SSE) transport instead rejects with an AxiosError whose server
|
|
1809
|
+
* body carries `code` under `response.data`.
|
|
1808
1810
|
*/ const isStaleClientSharesError = (error)=>{
|
|
1809
1811
|
if (!error || typeof error !== 'object') {
|
|
1810
1812
|
return false;
|
|
1811
1813
|
}
|
|
1812
|
-
|
|
1814
|
+
if (error.code === 'STALE_CLIENT_SHARES') {
|
|
1815
|
+
return true;
|
|
1816
|
+
}
|
|
1817
|
+
const axiosResponse = error.response;
|
|
1818
|
+
return (axiosResponse == null ? void 0 : axiosResponse.data) != null && axiosResponse.data.code === 'STALE_CLIENT_SHARES';
|
|
1813
1819
|
};
|
|
1814
1820
|
/**
|
|
1815
1821
|
* Detects the server's 409 ROOM_ID_ALREADY_USED rejection. The client
|
|
@@ -2594,6 +2600,45 @@ const classifyOne = (error)=>{
|
|
|
2594
2600
|
}
|
|
2595
2601
|
return fallback;
|
|
2596
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
|
+
};
|
|
2597
2642
|
/**
|
|
2598
2643
|
* Decides how an operation failure reaches the Datadog logger, whose signature
|
|
2599
2644
|
* is `(message, context, error)`.
|
|
@@ -3029,9 +3074,10 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3029
3074
|
* @param accountAddress - The wallet address
|
|
3030
3075
|
* @param operation - The operation type (must be a valid SignQueueOperation)
|
|
3031
3076
|
* @param callback - The sign operation to execute
|
|
3077
|
+
* @param options - Optional diagnostics attached to timeout logs
|
|
3032
3078
|
* @throws Error if operation is not a valid sign queue operation
|
|
3033
3079
|
* @returns Promise resolving to the sign result
|
|
3034
|
-
*/ static async queueSignOperation(accountAddress, operation, callback) {
|
|
3080
|
+
*/ static async queueSignOperation(accountAddress, operation, callback, options) {
|
|
3035
3081
|
accountAddress = normalizeAddress(accountAddress);
|
|
3036
3082
|
// Runtime validation to catch any type assertion bypasses
|
|
3037
3083
|
if (!isSignQueueOperation(operation)) {
|
|
@@ -3054,6 +3100,13 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3054
3100
|
let currentPhase = 'queued';
|
|
3055
3101
|
let phaseStartedAt = Date.now();
|
|
3056
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
|
+
};
|
|
3057
3110
|
// The task keeps running after the queue timeout rejects `add()`, so keep
|
|
3058
3111
|
// a reference to observe how it eventually settles.
|
|
3059
3112
|
let taskPromise;
|
|
@@ -3109,7 +3162,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3109
3162
|
signQueuePending: signQueue.pending,
|
|
3110
3163
|
signQueueSize: signQueue.size,
|
|
3111
3164
|
recoveryInProgress: this.isRecoveryInProgress(accountAddress)
|
|
3112
|
-
}, signDetails),
|
|
3165
|
+
}, enclaveContext(), signDetails),
|
|
3113
3166
|
level: 'warn'
|
|
3114
3167
|
});
|
|
3115
3168
|
// The caller already saw an error; record how the orphaned ceremony
|
|
@@ -3127,7 +3180,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3127
3180
|
operation,
|
|
3128
3181
|
outcome: 'resolved',
|
|
3129
3182
|
settledMs: Date.now() - timedOutAt
|
|
3130
|
-
}, signDetails),
|
|
3183
|
+
}, enclaveContext(), signDetails),
|
|
3131
3184
|
level: 'warn'
|
|
3132
3185
|
});
|
|
3133
3186
|
}, (lateError)=>{
|
|
@@ -3140,7 +3193,7 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3140
3193
|
outcome: 'rejected',
|
|
3141
3194
|
settledMs: Date.now() - timedOutAt,
|
|
3142
3195
|
causes: describeCauseChain(lateError)
|
|
3143
|
-
}, signDetails),
|
|
3196
|
+
}, enclaveContext(), signDetails),
|
|
3144
3197
|
level: 'warn'
|
|
3145
3198
|
});
|
|
3146
3199
|
});
|
|
@@ -3232,6 +3285,46 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
3232
3285
|
/** Track which wallets are currently executing inside a sign operation.
|
|
3233
3286
|
* Used to prevent deadlocks when recovery is called from within a sign op. */ WalletQueueManager.walletsWithActiveSignOp = new Map();
|
|
3234
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
|
+
|
|
3235
3328
|
// A DYNAMIC backup can persist multiple shares (e.g. 2-of-3); the singular
|
|
3236
3329
|
// externalKeyShareId is a fallback for older single-id shapes.
|
|
3237
3330
|
const collectDynamicKeyShareIds = (locations)=>{
|
|
@@ -3760,6 +3853,11 @@ class DynamicWalletClient {
|
|
|
3760
3853
|
});
|
|
3761
3854
|
});
|
|
3762
3855
|
}
|
|
3856
|
+
queueSign(accountAddress, task) {
|
|
3857
|
+
return WalletQueueManager.queueSignOperation(accountAddress, core.WalletOperation.SIGN_MESSAGE, task, {
|
|
3858
|
+
getEnclaveSnapshot: this.enclaveSnapshot
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3763
3861
|
connectForwardMPC() {
|
|
3764
3862
|
const forwardMPCConnectStartedAt = Date.now();
|
|
3765
3863
|
this.apiClient.forwardMPCClient.connect().then(()=>{
|
|
@@ -4661,7 +4759,7 @@ class DynamicWalletClient {
|
|
|
4661
4759
|
}
|
|
4662
4760
|
// Note: the server signing party (wallet-service) selects the ed25519
|
|
4663
4761
|
// protocol variant from its own persisted EAC, so it is NOT threaded here.
|
|
4664
|
-
const serializedContext = context ?
|
|
4762
|
+
const serializedContext = context ? core.toJsonSafeContext(context) : undefined;
|
|
4665
4763
|
const useHttpTransport = this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT] === true && this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT_SIGNMESSAGE] === true;
|
|
4666
4764
|
const params = {
|
|
4667
4765
|
walletId,
|
|
@@ -4684,6 +4782,45 @@ class DynamicWalletClient {
|
|
|
4684
4782
|
})) : await this.apiClient.signMessage(params);
|
|
4685
4783
|
return data;
|
|
4686
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
|
+
}
|
|
4687
4824
|
prepareMessageForForwardMPC({ chainName, message, formattedMessage, signingAlgo, isFormatted }) {
|
|
4688
4825
|
// BIP340 hash unwrap MUST come first. When `isFormatted=true`, callers wrap
|
|
4689
4826
|
// the payload in a `MessageHash`; the direct-MPC path then reads
|
|
@@ -4772,7 +4909,7 @@ class DynamicWalletClient {
|
|
|
4772
4909
|
throw error;
|
|
4773
4910
|
}
|
|
4774
4911
|
}
|
|
4775
|
-
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 }) {
|
|
4776
4913
|
// Reassigned when forward MPC fails and we mint a fresh room for the
|
|
4777
4914
|
// relay-based fallback below.
|
|
4778
4915
|
let roomId = initialRoomId;
|
|
@@ -4800,7 +4937,7 @@ class DynamicWalletClient {
|
|
|
4800
4937
|
isFormatted
|
|
4801
4938
|
}, this.getTraceContext(traceContext)));
|
|
4802
4939
|
// Try forward MPC first if enabled, with fallback to relay-based signing on attestation failure
|
|
4803
|
-
if (this.forwardMPCEnabled && !this.shouldSkipForwardMPC(chainName)) {
|
|
4940
|
+
if (this.forwardMPCEnabled && !skipForwardMPC && !this.shouldSkipForwardMPC(chainName)) {
|
|
4804
4941
|
attemptedForwardMPCSign = true;
|
|
4805
4942
|
const forwardMPCStartedAt = Date.now();
|
|
4806
4943
|
try {
|
|
@@ -5109,7 +5246,7 @@ class DynamicWalletClient {
|
|
|
5109
5246
|
}
|
|
5110
5247
|
//todo: need to modify with imported flag
|
|
5111
5248
|
async sign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, approvalQuorum, onError, traceContext, bitcoinConfig }) {
|
|
5112
|
-
return
|
|
5249
|
+
return this.queueSign(accountAddress, ({ reportPhase, reportDetails })=>this.internalSign({
|
|
5113
5250
|
accountAddress,
|
|
5114
5251
|
message,
|
|
5115
5252
|
chainName,
|
|
@@ -5128,6 +5265,278 @@ class DynamicWalletClient {
|
|
|
5128
5265
|
}));
|
|
5129
5266
|
}
|
|
5130
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
|
+
/**
|
|
5131
5540
|
* Reports a sign that spent a long time before reaching the network. Emitted
|
|
5132
5541
|
* once per slow sign with the phase breakdown, so a stall can be attributed
|
|
5133
5542
|
* without guessing: a shared dependency shows up as many signs reporting a
|
|
@@ -5151,11 +5560,83 @@ class DynamicWalletClient {
|
|
|
5151
5560
|
dynamicRequestId
|
|
5152
5561
|
}, this.getTraceContext(traceContext)));
|
|
5153
5562
|
}
|
|
5154
|
-
|
|
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));
|
|
5155
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
|
+
});
|
|
5585
|
+
return undefined;
|
|
5586
|
+
}
|
|
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
|
+
});
|
|
5156
5601
|
return undefined;
|
|
5157
5602
|
}
|
|
5158
|
-
const
|
|
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);
|
|
5159
5640
|
const roomId = room == null ? void 0 : room.roomId;
|
|
5160
5641
|
if (roomId) {
|
|
5161
5642
|
this.logger.info('[DynamicWaasWalletClient] Using cached room', _extends({
|
|
@@ -5165,7 +5646,7 @@ class DynamicWalletClient {
|
|
|
5165
5646
|
chainName,
|
|
5166
5647
|
walletId: wallet.walletId,
|
|
5167
5648
|
roomId
|
|
5168
|
-
},
|
|
5649
|
+
}, logContext));
|
|
5169
5650
|
}
|
|
5170
5651
|
return roomId;
|
|
5171
5652
|
}
|
|
@@ -5232,38 +5713,130 @@ class DynamicWalletClient {
|
|
|
5232
5713
|
clearTimeout(warningTimer);
|
|
5233
5714
|
}
|
|
5234
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
|
+
}
|
|
5235
5811
|
async internalSign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, approvalQuorum, onError, traceContext, bitcoinConfig, onSignPhase, onSignDetails, staleShareRetry = false, roomIdRetry = false }) {
|
|
5236
5812
|
const dynamicRequestId = uuid.v4();
|
|
5237
5813
|
onSignDetails == null ? void 0 : onSignDetails({
|
|
5238
5814
|
dynamicRequestId
|
|
5239
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)));
|
|
5240
5828
|
try {
|
|
5241
5829
|
// Pre-flight phase boundaries. Nothing reaches the network until
|
|
5242
5830
|
// serverSign below, so a stall here is invisible in server logs — these
|
|
5243
5831
|
// timestamps are what attribute it to a phase.
|
|
5244
|
-
const preflightStartedAt =
|
|
5245
|
-
onSignPhase == null ? void 0 : onSignPhase('verifyPassword');
|
|
5246
|
-
await this.verifyPassword({
|
|
5832
|
+
const { wallet, derivationPath, clientKeyShares, timings: { startedAt: preflightStartedAt, passwordVerifiedAt, walletLoadedAt, sharesReadyAt } } = await this.prepareWalletForSign({
|
|
5247
5833
|
accountAddress,
|
|
5248
5834
|
password,
|
|
5249
|
-
signedSessionId
|
|
5250
|
-
|
|
5251
|
-
const passwordVerifiedAt = Date.now();
|
|
5252
|
-
onSignPhase == null ? void 0 : onSignPhase('getWallet');
|
|
5253
|
-
const wallet = await this.getWallet({
|
|
5254
|
-
accountAddress,
|
|
5255
|
-
password,
|
|
5256
|
-
walletOperation: core.WalletOperation.SIGN_MESSAGE,
|
|
5257
|
-
signedSessionId
|
|
5835
|
+
signedSessionId,
|
|
5836
|
+
onSignPhase
|
|
5258
5837
|
});
|
|
5259
|
-
const walletLoadedAt = Date.now();
|
|
5260
|
-
const derivationPath = wallet.derivationPath && wallet.derivationPath != '' ? new Uint32Array(Object.values(JSON.parse(wallet.derivationPath))) : undefined;
|
|
5261
|
-
// Ensure client key shares exist before hitting the API
|
|
5262
|
-
onSignPhase == null ? void 0 : onSignPhase('ensureClientShare');
|
|
5263
|
-
const clientKeyShares = await this.ensureClientShare(accountAddress);
|
|
5264
|
-
const sharesReadyAt = Date.now();
|
|
5265
5838
|
onSignPhase == null ? void 0 : onSignPhase('resolveCachedRoom');
|
|
5266
|
-
const cachedRoomId = roomIdRetry ? undefined : await this.getCachedRoomForSign(wallet, accountAddress, chainName, traceContext);
|
|
5839
|
+
const cachedRoomId = roomIdRetry ? undefined : await this.getCachedRoomForSign(wallet, accountAddress, chainName, dynamicRequestId, traceContext);
|
|
5267
5840
|
const roomResolvedAt = Date.now();
|
|
5268
5841
|
this.logSlowSignPreflight({
|
|
5269
5842
|
accountAddress,
|
|
@@ -5349,9 +5922,7 @@ class DynamicWalletClient {
|
|
|
5349
5922
|
operation: 'serverSign'
|
|
5350
5923
|
}, this.getTraceContext(traceContext)));
|
|
5351
5924
|
let signature;
|
|
5352
|
-
|
|
5353
|
-
// detect a client-side mismatch masked by a generic SSE race winner.
|
|
5354
|
-
let clientSignMismatchError;
|
|
5925
|
+
let getClientSignMismatch = ()=>undefined;
|
|
5355
5926
|
try {
|
|
5356
5927
|
// Race clientSign against SSE errors so that server-side failures
|
|
5357
5928
|
// (e.g. policy violations) immediately reject instead of hanging
|
|
@@ -5414,28 +5985,15 @@ class DynamicWalletClient {
|
|
|
5414
5985
|
// a stale-local-share failure surfaces, since the server's generic SSE
|
|
5415
5986
|
// error wins the race and masks it. Schedule recovery regardless of
|
|
5416
5987
|
// which promise wins or when this rejection settles.
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
accountAddress,
|
|
5427
|
-
chainName,
|
|
5428
|
-
password,
|
|
5429
|
-
signedSessionId,
|
|
5430
|
-
walletOperation: core.WalletOperation.SIGN_MESSAGE,
|
|
5431
|
-
mfaToken,
|
|
5432
|
-
dynamicRequestId,
|
|
5433
|
-
traceContext,
|
|
5434
|
-
bitcoinConfig,
|
|
5435
|
-
trigger: 'clientSign rejection',
|
|
5436
|
-
confirmedMismatch: true
|
|
5437
|
-
});
|
|
5438
|
-
}
|
|
5988
|
+
getClientSignMismatch = this.trackClientSignMismatch(clientSignPromise, {
|
|
5989
|
+
accountAddress,
|
|
5990
|
+
chainName,
|
|
5991
|
+
password,
|
|
5992
|
+
signedSessionId,
|
|
5993
|
+
mfaToken,
|
|
5994
|
+
dynamicRequestId,
|
|
5995
|
+
traceContext,
|
|
5996
|
+
bitcoinConfig
|
|
5439
5997
|
});
|
|
5440
5998
|
signature = await Promise.race([
|
|
5441
5999
|
clientSignPromise,
|
|
@@ -5449,23 +6007,17 @@ class DynamicWalletClient {
|
|
|
5449
6007
|
if (isStaleClientSharesError(signError) || isRoomIdAlreadyUsedError(signError)) {
|
|
5450
6008
|
throw signError;
|
|
5451
6009
|
}
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
// settles later, so error-string inspection alone is timing-dependent.
|
|
5456
|
-
// The check no-ops when shares are fresh.
|
|
5457
|
-
await this.healStaleShares({
|
|
6010
|
+
await this.healAfterSignFailure({
|
|
6011
|
+
signError,
|
|
6012
|
+
clientSignMismatch: getClientSignMismatch(),
|
|
5458
6013
|
accountAddress,
|
|
5459
6014
|
chainName,
|
|
5460
6015
|
password,
|
|
5461
6016
|
signedSessionId,
|
|
5462
|
-
walletOperation: core.WalletOperation.SIGN_MESSAGE,
|
|
5463
6017
|
mfaToken,
|
|
5464
6018
|
dynamicRequestId,
|
|
5465
6019
|
traceContext,
|
|
5466
|
-
bitcoinConfig
|
|
5467
|
-
trigger: 'sign failure',
|
|
5468
|
-
confirmedMismatch: isPublicKeyMismatchError(signError) || clientSignMismatchError !== undefined
|
|
6020
|
+
bitcoinConfig
|
|
5469
6021
|
});
|
|
5470
6022
|
throw signError;
|
|
5471
6023
|
}
|
|
@@ -5476,6 +6028,7 @@ class DynamicWalletClient {
|
|
|
5476
6028
|
roomId,
|
|
5477
6029
|
derivationPath,
|
|
5478
6030
|
isFormatted,
|
|
6031
|
+
dynamicRequestId,
|
|
5479
6032
|
key: 'sign',
|
|
5480
6033
|
operation: 'clientSign'
|
|
5481
6034
|
}, this.getTraceContext(traceContext)));
|
|
@@ -5559,15 +6112,21 @@ class DynamicWalletClient {
|
|
|
5559
6112
|
roomIdRetry: true
|
|
5560
6113
|
});
|
|
5561
6114
|
}
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
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
|
|
5571
6130
|
});
|
|
5572
6131
|
throw error;
|
|
5573
6132
|
}
|
|
@@ -7171,9 +7730,16 @@ class DynamicWalletClient {
|
|
|
7171
7730
|
accountAddress: wallet.accountAddress
|
|
7172
7731
|
});
|
|
7173
7732
|
// Denial arrives as a *later* SSE `error` event, after room_created
|
|
7174
|
-
// already resolved below (see internalSign's sseErrorGate).
|
|
7733
|
+
// already resolved below (see internalSign's sseErrorGate). HTTP
|
|
7734
|
+
// transport has no such stream, so a post-init denial surfaces as an
|
|
7735
|
+
// MPC relay timeout there.
|
|
7175
7736
|
const sseErrorGate = core.createRejectablePromise();
|
|
7176
|
-
const
|
|
7737
|
+
const useHttpTransport = this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT] === true && this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT_EXPORT] === true;
|
|
7738
|
+
// Fresh key per invocation: the transparent stale-share retry below is a
|
|
7739
|
+
// NEW ceremony (new shareSetId), so it must not reuse the failed
|
|
7740
|
+
// attempt's key — the dedup would replay that row's terminal error.
|
|
7741
|
+
const idempotencyKey = useHttpTransport ? uuid.v4() : undefined;
|
|
7742
|
+
const exportParams = {
|
|
7177
7743
|
walletId: wallet.walletId,
|
|
7178
7744
|
shareSetId: wallet.shareSetId,
|
|
7179
7745
|
exportId,
|
|
@@ -7181,8 +7747,13 @@ class DynamicWalletClient {
|
|
|
7181
7747
|
dynamicRequestId,
|
|
7182
7748
|
mfaToken,
|
|
7183
7749
|
elevatedAccessToken,
|
|
7750
|
+
traceContext
|
|
7751
|
+
};
|
|
7752
|
+
const data = useHttpTransport ? await this.apiClient.requestExportKeyJson(_extends({}, exportParams, {
|
|
7753
|
+
idempotencyKey: idempotencyKey
|
|
7754
|
+
})) : await this.apiClient.exportKey(_extends({}, exportParams, {
|
|
7184
7755
|
onError: (error)=>sseErrorGate.reject(error)
|
|
7185
|
-
});
|
|
7756
|
+
}));
|
|
7186
7757
|
this.logger.debug('[DynamicWaasWalletClient] Starting export of private key', _extends({
|
|
7187
7758
|
accountAddress,
|
|
7188
7759
|
chainName,
|
|
@@ -7190,9 +7761,9 @@ class DynamicWalletClient {
|
|
|
7190
7761
|
exportId,
|
|
7191
7762
|
roomId: data.roomId
|
|
7192
7763
|
}, this.getTraceContext(traceContext)));
|
|
7193
|
-
// Race the MPC ceremony against the
|
|
7194
|
-
//
|
|
7195
|
-
//
|
|
7764
|
+
// Race the MPC ceremony against the error gate opened above so a late
|
|
7765
|
+
// policy-violation SSE event aborts immediately rather than waiting out
|
|
7766
|
+
// the relay's own timeout.
|
|
7196
7767
|
const mpcExportPromise = this.performMPCExport(mpcSigner, reconstructedKeyShare, data.roomId, exportId, accountAddress, chainName);
|
|
7197
7768
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
7198
7769
|
mpcExportPromise.catch(()=>{});
|
|
@@ -10659,8 +11230,25 @@ class DynamicWalletClient {
|
|
|
10659
11230
|
const MPC_SCHEME_CONFIG = core.MPC_CONFIG[thresholdSignatureScheme];
|
|
10660
11231
|
return roomType === RoomTypeEnum.RoomTypeEnum.Threshold ? MPC_SCHEME_CONFIG.threshold : MPC_SCHEME_CONFIG.numberOfParties;
|
|
10661
11232
|
}
|
|
10662
|
-
async getRoom(roomType, thresholdSignatureScheme) {
|
|
10663
|
-
|
|
11233
|
+
async getRoom(roomType, thresholdSignatureScheme, logContext) {
|
|
11234
|
+
const [room] = await this.takeRooms({
|
|
11235
|
+
roomType,
|
|
11236
|
+
thresholdSignatureScheme,
|
|
11237
|
+
count: 1,
|
|
11238
|
+
logContext
|
|
11239
|
+
});
|
|
11240
|
+
return room;
|
|
11241
|
+
}
|
|
11242
|
+
/**
|
|
11243
|
+
* 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.
|
|
11244
|
+
*
|
|
11245
|
+
* @param params.roomType - Which kind of room.
|
|
11246
|
+
* @param params.thresholdSignatureScheme - The key scheme; it decides how many parties the room needs.
|
|
11247
|
+
* @param params.count - How many rooms to take.
|
|
11248
|
+
* @param params.logContext - Request and trace ids, so the pool's log joins the operation's.
|
|
11249
|
+
* @returns The rooms taken, or an empty list.
|
|
11250
|
+
*/ async takeRooms({ roomType, thresholdSignatureScheme, count, logContext }) {
|
|
11251
|
+
var _rooms_, _DynamicWalletClient_rooms_numberOfParties;
|
|
10664
11252
|
const numberOfParties = this.getNumberOfParties(roomType, thresholdSignatureScheme);
|
|
10665
11253
|
const consume = async ()=>{
|
|
10666
11254
|
// Re-read from localStorage rather than the in-memory static so we see rooms
|
|
@@ -10673,29 +11261,34 @@ class DynamicWalletClient {
|
|
|
10673
11261
|
const schemeRooms = storedRooms[numberOfParties] || [];
|
|
10674
11262
|
// Filter out expired rooms and remove the next available room.
|
|
10675
11263
|
const filteredRooms = schemeRooms.filter((room)=>room.expiresAt > now);
|
|
10676
|
-
const
|
|
11264
|
+
const taken = filteredRooms.length >= count ? filteredRooms.splice(0, count) : [];
|
|
10677
11265
|
// Use setRooms() rather than replacing DynamicWalletClient.rooms wholesale.
|
|
10678
11266
|
// setRooms() only mutates the specific numberOfParties key, so rooms for other
|
|
10679
11267
|
// party counts and any rooms written in-memory by a concurrent createRooms()
|
|
10680
11268
|
// call that hasn't flushed to storage yet are preserved. Persistence is also
|
|
10681
11269
|
// routed through the existing persistRooms() chain to keep writes serialized.
|
|
10682
11270
|
await this.setRooms(numberOfParties, filteredRooms);
|
|
10683
|
-
return
|
|
11271
|
+
return taken;
|
|
10684
11272
|
};
|
|
10685
11273
|
// Use the Web Locks API for cross-tab mutual exclusion so that two tabs
|
|
10686
11274
|
// cannot both consume the same cached room. Falls back to a direct call in
|
|
10687
11275
|
// environments where navigator.locks is unavailable (e.g. Node.js tests).
|
|
10688
11276
|
const webLocksAvailable = typeof navigator !== 'undefined' && !!navigator.locks;
|
|
10689
|
-
const
|
|
11277
|
+
const rooms = webLocksAvailable ? await navigator.locks.request(`${this.storageKey}-room-cache`, consume) : await consume();
|
|
10690
11278
|
var _DynamicWalletClient_rooms_numberOfParties_length;
|
|
10691
|
-
this.logger.info('[DynamicWaasWalletClient] getRoom result', {
|
|
11279
|
+
this.logger.info('[DynamicWaasWalletClient] getRoom result', _extends({
|
|
10692
11280
|
key: 'roomCache',
|
|
10693
11281
|
operation: 'getRoom',
|
|
10694
11282
|
numberOfParties,
|
|
10695
|
-
roomId:
|
|
11283
|
+
roomId: (_rooms_ = rooms[0]) == null ? void 0 : _rooms_.roomId,
|
|
11284
|
+
requestedCount: count,
|
|
11285
|
+
takenCount: rooms.length
|
|
11286
|
+
}, count > 1 ? {
|
|
11287
|
+
roomIds: rooms.map((room)=>room.roomId)
|
|
11288
|
+
} : {}, logContext, {
|
|
10696
11289
|
remainingInMemory: (_DynamicWalletClient_rooms_numberOfParties_length = (_DynamicWalletClient_rooms_numberOfParties = DynamicWalletClient.rooms[numberOfParties]) == null ? void 0 : _DynamicWalletClient_rooms_numberOfParties.length) != null ? _DynamicWalletClient_rooms_numberOfParties_length : 0,
|
|
10697
11290
|
webLocksAvailable
|
|
10698
|
-
});
|
|
11291
|
+
}));
|
|
10699
11292
|
// If less than or only 1 room left after removing one, trigger async creation for more rooms.
|
|
10700
11293
|
// The catch prevents an unhandled promise rejection when the API
|
|
10701
11294
|
// rate-limits this best-effort background call — createRooms already
|
|
@@ -10708,7 +11301,7 @@ class DynamicWalletClient {
|
|
|
10708
11301
|
roomCount: ROOM_CACHE_COUNT
|
|
10709
11302
|
}).catch(()=>{});
|
|
10710
11303
|
}
|
|
10711
|
-
return
|
|
11304
|
+
return rooms;
|
|
10712
11305
|
}
|
|
10713
11306
|
getTraceContext(traceContext) {
|
|
10714
11307
|
const now = Date.now();
|
|
@@ -10813,6 +11406,7 @@ class DynamicWalletClient {
|
|
|
10813
11406
|
this.startLoggerContext(authToken);
|
|
10814
11407
|
}
|
|
10815
11408
|
this.forwardMPCEnabled = this.featureFlags && this.featureFlags[core.FEATURE_FLAGS.ENABLE_FORWARD_MPC_CLIENT_FLAG] === true;
|
|
11409
|
+
this.enclaveSnapshot = this.forwardMPCEnabled ? trackForwardMpcEnclave(this.apiClient.forwardMPCClient) : undefined;
|
|
10816
11410
|
if (this.forwardMPCEnabled) {
|
|
10817
11411
|
this.connectForwardMPC();
|
|
10818
11412
|
}
|
|
@@ -10984,6 +11578,7 @@ exports.resolveLogLevel = resolveLogLevel;
|
|
|
10984
11578
|
exports.retryPromise = retryPromise;
|
|
10985
11579
|
exports.shouldReshareToSameBackups = shouldReshareToSameBackups;
|
|
10986
11580
|
exports.timeoutPromise = timeoutPromise;
|
|
11581
|
+
exports.trackForwardMpcEnclave = trackForwardMpcEnclave;
|
|
10987
11582
|
Object.keys(core).forEach(function (k) {
|
|
10988
11583
|
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|
|
10989
11584
|
enumerable: true,
|