From 290a2ac1bd703ea3f6384a5e29eb867b29d98056 Mon Sep 17 00:00:00 2001 From: Felix Zhang Date: Tue, 16 Dec 2025 13:48:35 -0800 Subject: [PATCH 1/3] provider measurement --- .../src/core/telemetry/events/provider.ts | 9 + .../builder/core/BaseAccountProvider.ts | 175 ++++++++---------- .../core/EphemeralBaseAccountProvider.ts | 114 +++++------- .../interface/builder/core/withMeasurement.ts | 54 ++++++ 4 files changed, 180 insertions(+), 172 deletions(-) create mode 100644 packages/account-sdk/src/interface/builder/core/withMeasurement.ts diff --git a/packages/account-sdk/src/core/telemetry/events/provider.ts b/packages/account-sdk/src/core/telemetry/events/provider.ts index 5422c3046..c48c675df 100644 --- a/packages/account-sdk/src/core/telemetry/events/provider.ts +++ b/packages/account-sdk/src/core/telemetry/events/provider.ts @@ -3,9 +3,11 @@ import { ActionType, AnalyticsEventImportance, ComponentType, logEvent } from '. export const logRequestStarted = ({ method, correlationId, + isEphemeral = false, }: { method: string; correlationId: string | undefined; + isEphemeral?: boolean; }) => { logEvent( 'provider.request.started', @@ -15,6 +17,7 @@ export const logRequestStarted = ({ method, signerType: 'base-account', correlationId, + isEphemeral, }, AnalyticsEventImportance.high ); @@ -24,10 +27,12 @@ export const logRequestError = ({ method, correlationId, errorMessage, + isEphemeral = false, }: { method: string; correlationId: string | undefined; errorMessage: string; + isEphemeral?: boolean; }) => { logEvent( 'provider.request.error', @@ -38,6 +43,7 @@ export const logRequestError = ({ signerType: 'base-account', correlationId, errorMessage, + isEphemeral, }, AnalyticsEventImportance.high ); @@ -46,9 +52,11 @@ export const logRequestError = ({ export const logRequestResponded = ({ method, correlationId, + isEphemeral = false, }: { method: string; correlationId: string | undefined; + isEphemeral?: boolean; }) => { logEvent( 'provider.request.responded', @@ -58,6 +66,7 @@ export const logRequestResponded = ({ method, signerType: 'base-account', correlationId, + isEphemeral, }, AnalyticsEventImportance.high ); diff --git a/packages/account-sdk/src/interface/builder/core/BaseAccountProvider.ts b/packages/account-sdk/src/interface/builder/core/BaseAccountProvider.ts index 27b57c307..373682a9b 100644 --- a/packages/account-sdk/src/interface/builder/core/BaseAccountProvider.ts +++ b/packages/account-sdk/src/interface/builder/core/BaseAccountProvider.ts @@ -9,18 +9,13 @@ import { ProviderInterface, RequestArguments, } from ':core/provider/interface.js'; -import { - logRequestError, - logRequestResponded, - logRequestStarted, -} from ':core/telemetry/events/provider.js'; -import { parseErrorMessageFromAny } from ':core/telemetry/utils.js'; import { hexStringFromNumber } from ':core/type/util.js'; import { Signer } from ':sign/base-account/Signer.js'; import { initSubAccountConfig } from ':sign/base-account/utils.js'; import { correlationIds } from ':store/correlation-ids/store.js'; import { store } from ':store/store.js'; import { checkErrorForInvalidRequestArgs, fetchRPCRequest } from ':util/provider.js'; +import { withMeasurement } from './withMeasurement.js'; export class BaseAccountProvider extends ProviderEventEmitter implements ProviderInterface { private readonly communicator: Communicator; @@ -46,111 +41,89 @@ export class BaseAccountProvider extends ProviderEventEmitter implements Provide }); } - public async request(args: RequestArguments): Promise { - // correlation id across the entire request lifecycle - const correlationId = crypto.randomUUID(); - correlationIds.set(args, correlationId); - logRequestStarted({ method: args.method, correlationId }); - - try { - const result = await this._request(args); - logRequestResponded({ - method: args.method, - correlationId, - }); - return result as T; - } catch (error) { - logRequestError({ - method: args.method, - correlationId, - errorMessage: parseErrorMessageFromAny(error), - }); - throw error; - } finally { - correlationIds.delete(args); - } - } - - private async _request(args: RequestArguments): Promise { - try { - checkErrorForInvalidRequestArgs(args); - if (!this.signer.isConnected) { - switch (args.method) { - case 'eth_requestAccounts': { - await this.signer.handshake({ method: 'handshake' }); - // We are translating eth_requestAccounts to wallet_connect always - await initSubAccountConfig(); - await this.signer.request({ - method: 'wallet_connect', - params: [ - { - version: '1', - capabilities: { - ...(store.subAccountsConfig.get()?.capabilities ?? {}), + public request = withMeasurement( + { isEphemeral: false }, + async (args: RequestArguments): Promise => { + try { + checkErrorForInvalidRequestArgs(args); + if (!this.signer.isConnected) { + switch (args.method) { + case 'eth_requestAccounts': { + await this.signer.handshake({ method: 'handshake' }); + // We are translating eth_requestAccounts to wallet_connect always + await initSubAccountConfig(); + await this.signer.request({ + method: 'wallet_connect', + params: [ + { + version: '1', + capabilities: { + ...(store.subAccountsConfig.get()?.capabilities ?? {}), + }, }, - }, - ], - }); + ], + }); - // wallet_connect will retrieve and save the account info in the store - // continue to requesting it again at L130 for emitting the connect event + - // returning the accounts - break; - } - case 'wallet_connect': { - await this.signer.handshake({ method: 'handshake' }); // exchange session keys - const result = await this.signer.request(args); // send diffie-hellman encrypted request - return result as T; - } - case 'wallet_switchEthereumChain': { - // wallet_switchEthereumChain does not need to be sent to the popup - // it is handled by the base account signer - // so we just return the result - const result = await this.signer.request(args); - return result as T; - } - case 'wallet_sendCalls': - case 'wallet_sign': { - try { + // wallet_connect will retrieve and save the account info in the store + // continue to requesting it again for emitting the connect event + + // returning the accounts + break; + } + case 'wallet_connect': { await this.signer.handshake({ method: 'handshake' }); // exchange session keys const result = await this.signer.request(args); // send diffie-hellman encrypted request return result as T; - } finally { - await this.signer.cleanup(); // clean up (rotate) the ephemeral session keys } - } - case 'wallet_getCallsStatus': { - const result = await fetchRPCRequest(args, CB_WALLET_RPC_URL); - return result as T; - } - case 'eth_accounts': { - return [] as T; - } - case 'net_version': { - const result = 1 as T; // default value - return result; - } - case 'eth_chainId': { - const result = hexStringFromNumber(1) as T; // default value - return result; - } - default: { - throw standardErrors.provider.unauthorized( - "Must call 'eth_requestAccounts' before other methods" - ); + case 'wallet_switchEthereumChain': { + // wallet_switchEthereumChain does not need to be sent to the popup + // it is handled by the base account signer + // so we just return the result + const result = await this.signer.request(args); + return result as T; + } + case 'wallet_sendCalls': + case 'wallet_sign': { + try { + await this.signer.handshake({ method: 'handshake' }); // exchange session keys + const result = await this.signer.request(args); // send diffie-hellman encrypted request + return result as T; + } finally { + await this.signer.cleanup(); // clean up (rotate) the ephemeral session keys + } + } + case 'wallet_getCallsStatus': { + const result = await fetchRPCRequest(args, CB_WALLET_RPC_URL); + return result as T; + } + case 'eth_accounts': { + return [] as T; + } + case 'net_version': { + const result = 1 as T; // default value + return result; + } + case 'eth_chainId': { + const result = hexStringFromNumber(1) as T; // default value + return result; + } + default: { + throw standardErrors.provider.unauthorized( + "Must call 'eth_requestAccounts' before other methods" + ); + } } } + const result = await this.signer.request(args); + return result as T; + } catch (error) { + const { code } = error as { code?: number }; + if (code === standardErrorCodes.provider.unauthorized) { + await this.disconnect(); + } + return Promise.reject(serializeError(error)); } - const result = await this.signer.request(args); - return result as T; - } catch (error) { - const { code } = error as { code?: number }; - if (code === standardErrorCodes.provider.unauthorized) { - await this.disconnect(); - } - return Promise.reject(serializeError(error)); } - } + ); async disconnect() { await this.signer.cleanup(); diff --git a/packages/account-sdk/src/interface/builder/core/EphemeralBaseAccountProvider.ts b/packages/account-sdk/src/interface/builder/core/EphemeralBaseAccountProvider.ts index 44a7fa6ef..8ad8f7e3f 100644 --- a/packages/account-sdk/src/interface/builder/core/EphemeralBaseAccountProvider.ts +++ b/packages/account-sdk/src/interface/builder/core/EphemeralBaseAccountProvider.ts @@ -9,17 +9,11 @@ import { ProviderInterface, RequestArguments, } from ':core/provider/interface.js'; -import { - logRequestError, - logRequestResponded, - logRequestStarted, -} from ':core/telemetry/events/provider.js'; -import { parseErrorMessageFromAny } from ':core/telemetry/utils.js'; import { hexStringFromNumber } from ':core/type/util.js'; import { EphemeralSigner } from ':sign/base-account/EphemeralSigner.js'; -import { correlationIds } from ':store/correlation-ids/store.js'; -import { createStoreInstance, type StoreInstance } from ':store/store.js'; +import { type StoreInstance, createStoreInstance } from ':store/store.js'; import { fetchRPCRequest } from ':util/provider.js'; +import { withMeasurement } from './withMeasurement.js'; /** * EphemeralBaseAccountProvider is a provider designed for single-use payment flows. @@ -65,75 +59,53 @@ export class EphemeralBaseAccountProvider }); } - public async request(args: RequestArguments): Promise { - // correlation id across the entire request lifecycle - const correlationId = crypto.randomUUID(); - correlationIds.set(args, correlationId); - logRequestStarted({ method: args.method, correlationId }); - - try { - const result = await this._request(args); - logRequestResponded({ - method: args.method, - correlationId, - }); - return result as T; - } catch (error) { - logRequestError({ - method: args.method, - correlationId, - errorMessage: parseErrorMessageFromAny(error), - }); - throw error; - } finally { - correlationIds.delete(args); - } - } - - private async _request(args: RequestArguments): Promise { - try { - // For ephemeral providers, we only support a subset of methods - // that are needed for payment flows - switch (args.method) { - case 'wallet_sendCalls': - case 'wallet_sign': { - try { - await this.signer.handshake({ method: 'handshake' }); // exchange session keys - const result = await this.signer.request(args); // send diffie-hellman encrypted request + public request = withMeasurement( + { isEphemeral: true }, + async (args: RequestArguments): Promise => { + try { + // For ephemeral providers, we only support a subset of methods + // that are needed for payment flows + switch (args.method) { + case 'wallet_sendCalls': + case 'wallet_sign': { + try { + await this.signer.handshake({ method: 'handshake' }); // exchange session keys + const result = await this.signer.request(args); // send diffie-hellman encrypted request + return result as T; + } finally { + await this.signer.cleanup(); // clean up (rotate) the ephemeral session keys + } + } + case 'wallet_getCallsStatus': { + const result = await fetchRPCRequest(args, CB_WALLET_RPC_URL); return result as T; - } finally { - await this.signer.cleanup(); // clean up (rotate) the ephemeral session keys + } + case 'eth_accounts': { + return [] as T; + } + case 'net_version': { + const result = 1 as T; // default value + return result; + } + case 'eth_chainId': { + const result = hexStringFromNumber(1) as T; // default value + return result; + } + default: { + throw standardErrors.provider.unauthorized( + `Method '${args.method}' is not supported by ephemeral provider. Ephemeral providers only support: wallet_sendCalls, wallet_sign, wallet_getCallsStatus` + ); } } - case 'wallet_getCallsStatus': { - const result = await fetchRPCRequest(args, CB_WALLET_RPC_URL); - return result as T; - } - case 'eth_accounts': { - return [] as T; - } - case 'net_version': { - const result = 1 as T; // default value - return result; - } - case 'eth_chainId': { - const result = hexStringFromNumber(1) as T; // default value - return result; - } - default: { - throw standardErrors.provider.unauthorized( - `Method '${args.method}' is not supported by ephemeral provider. Ephemeral providers only support: wallet_sendCalls, wallet_sign, wallet_getCallsStatus` - ); + } catch (error) { + const { code } = error as { code?: number }; + if (code === standardErrorCodes.provider.unauthorized) { + await this.disconnect(); } + return Promise.reject(serializeError(error)); } - } catch (error) { - const { code } = error as { code?: number }; - if (code === standardErrorCodes.provider.unauthorized) { - await this.disconnect(); - } - return Promise.reject(serializeError(error)); } - } + ); async disconnect() { // Cleanup ephemeral signer state and its isolated store diff --git a/packages/account-sdk/src/interface/builder/core/withMeasurement.ts b/packages/account-sdk/src/interface/builder/core/withMeasurement.ts new file mode 100644 index 000000000..e3fabbb28 --- /dev/null +++ b/packages/account-sdk/src/interface/builder/core/withMeasurement.ts @@ -0,0 +1,54 @@ +import { RequestArguments } from ':core/provider/interface.js'; +import { + logRequestError, + logRequestResponded, + logRequestStarted, +} from ':core/telemetry/events/provider.js'; +import { parseErrorMessageFromAny } from ':core/telemetry/utils.js'; +import { correlationIds } from ':store/correlation-ids/store.js'; + +type WithMeasurementOptions = { + isEphemeral?: boolean; +}; + +/** + * Higher-order function that wraps a request handler with correlation ID tracking and telemetry. + * + * Handles: + * - Generating and storing correlation ID + * - Logging request start/success/error + * - Cleaning up correlation ID after request completes + * + * @param handler - The actual request handler function + * @returns Wrapped handler with measurement instrumentation + */ +export function withMeasurement( + options: WithMeasurementOptions, + handler: (args: RequestArguments) => Promise +): (args: RequestArguments) => Promise { + return async (args: RequestArguments): Promise => { + const correlationId = crypto.randomUUID(); + correlationIds.set(args, correlationId); + + const measurementParams = { + method: args.method, + correlationId, + isEphemeral: !!options.isEphemeral, + }; + logRequestStarted(measurementParams); + + try { + const result = await handler(args); + logRequestResponded(measurementParams); + return result; + } catch (error) { + logRequestError({ + ...measurementParams, + errorMessage: parseErrorMessageFromAny(error), + }); + throw error; + } finally { + correlationIds.delete(args); + } + }; +} From 75a0b3b7e991080a9a54ddeb7a7eabc5ba6fe636 Mon Sep 17 00:00:00 2001 From: Felix Zhang Date: Tue, 16 Dec 2025 13:55:41 -0800 Subject: [PATCH 2/3] signer measurement --- .../src/sign/base-account/EphemeralSigner.ts | 79 ++-- .../src/sign/base-account/Signer.ts | 368 ++++++++---------- .../base-account/withSignerMeasurement.ts | 94 +++++ 3 files changed, 286 insertions(+), 255 deletions(-) create mode 100644 packages/account-sdk/src/sign/base-account/withSignerMeasurement.ts diff --git a/packages/account-sdk/src/sign/base-account/EphemeralSigner.ts b/packages/account-sdk/src/sign/base-account/EphemeralSigner.ts index c6c8c6a09..87dad48c1 100644 --- a/packages/account-sdk/src/sign/base-account/EphemeralSigner.ts +++ b/packages/account-sdk/src/sign/base-account/EphemeralSigner.ts @@ -4,14 +4,9 @@ import { RPCRequestMessage, RPCResponseMessage } from ':core/message/RPCMessage. import { RPCResponse } from ':core/message/RPCResponse.js'; import { AppMetadata, ProviderEventCallback, RequestArguments } from ':core/provider/interface.js'; import { - logHandshakeCompleted, - logHandshakeError, - logHandshakeStarted, - logRequestCompleted, - logRequestError, - logRequestStarted, -} from ':core/telemetry/events/scw-signer.js'; -import { parseErrorMessageFromAny } from ':core/telemetry/utils.js'; + withHandshakeMeasurement, + withSignerRequestMeasurement, +} from './withSignerMeasurement.js'; import { SDKChain, createClients } from ':store/chain-clients/utils.js'; import { correlationIds } from ':store/correlation-ids/store.js'; import { createStoreHelpers, type StoreInstance } from ':store/store.js'; @@ -72,11 +67,11 @@ export class EphemeralSigner { } } - async handshake(args: RequestArguments) { - const correlationId = correlationIds.get(args); - logHandshakeStarted({ method: args.method, correlationId, isEphemeral: true }); + handshake = withHandshakeMeasurement( + { isEphemeral: true }, + async (args: RequestArguments): Promise => { + const correlationId = correlationIds.get(args); - try { // Open the popup before constructing the request message. await this.communicator.waitForPopupLoaded?.(); @@ -101,52 +96,26 @@ export class EphemeralSigner { await this.keyManager.setPeerPublicKey(peerPublicKey); await this.decryptResponseMessage(response); - - logHandshakeCompleted({ method: args.method, correlationId, isEphemeral: true }); - } catch (error) { - logHandshakeError({ - method: args.method, - correlationId, - errorMessage: parseErrorMessageFromAny(error), - isEphemeral: true, - }); - throw error; - } - } - - async request(request: RequestArguments) { - const correlationId = correlationIds.get(request); - logRequestStarted({ method: request.method, correlationId, isEphemeral: true }); - - try { - const result = await this._request(request); - logRequestCompleted({ method: request.method, correlationId, isEphemeral: true }); - return result; - } catch (error) { - logRequestError({ - method: request.method, - correlationId, - errorMessage: parseErrorMessageFromAny(error), - isEphemeral: true, - }); - throw error; } - } - - private async _request(request: RequestArguments) { - // Ephemeral signer only supports sending requests to popup - // for wallet_sendCalls and wallet_sign methods - switch (request.method) { - case 'wallet_sendCalls': - case 'wallet_sign': { - return this.sendRequestToPopup(request); + ); + + request = withSignerRequestMeasurement( + { isEphemeral: true }, + async (request: RequestArguments): Promise => { + // Ephemeral signer only supports sending requests to popup + // for wallet_sendCalls and wallet_sign methods + switch (request.method) { + case 'wallet_sendCalls': + case 'wallet_sign': { + return this.sendRequestToPopup(request) as Promise; + } + default: + throw standardErrors.provider.unauthorized( + `Method '${request.method}' is not supported by ephemeral signer` + ); } - default: - throw standardErrors.provider.unauthorized( - `Method '${request.method}' is not supported by ephemeral signer` - ); } - } + ); private async sendRequestToPopup(request: RequestArguments) { // Open the popup before constructing the request message. diff --git a/packages/account-sdk/src/sign/base-account/Signer.ts b/packages/account-sdk/src/sign/base-account/Signer.ts index ac73c897f..8c8b9c6c4 100644 --- a/packages/account-sdk/src/sign/base-account/Signer.ts +++ b/packages/account-sdk/src/sign/base-account/Signer.ts @@ -13,14 +13,6 @@ import { import { FetchPermissionsResponse } from ':core/rpc/coinbase_fetchSpendPermissions.js'; import { WalletConnectResponse } from ':core/rpc/wallet_connect.js'; import { GetSubAccountsResponse } from ':core/rpc/wallet_getSubAccount.js'; -import { - logHandshakeCompleted, - logHandshakeError, - logHandshakeStarted, - logRequestCompleted, - logRequestError, - logRequestStarted, -} from ':core/telemetry/events/scw-signer.js'; import { logAddOwnerCompleted, logAddOwnerError, @@ -37,7 +29,7 @@ import { Address } from ':core/type/index.js'; import { ensureIntNumber, hexStringFromNumber } from ':core/type/util.js'; import { SDKChain, createClients, getClient } from ':store/chain-clients/utils.js'; import { correlationIds } from ':store/correlation-ids/store.js'; -import { createStoreHelpers, spendPermissions, store, type StoreInstance } from ':store/store.js'; +import { type StoreInstance, createStoreHelpers, spendPermissions, store } from ':store/store.js'; import { assertArrayPresence, assertPresence } from ':util/assertPresence.js'; import { assertSubAccount } from ':util/assertSubAccount.js'; import { @@ -67,6 +59,7 @@ import { findOwnerIndex } from './utils/findOwnerIndex.js'; import { handleAddSubAccountOwner } from './utils/handleAddSubAccountOwner.js'; import { handleInsufficientBalanceError } from './utils/handleInsufficientBalance.js'; import { routeThroughGlobalAccount } from './utils/routeThroughGlobalAccount.js'; +import { withHandshakeMeasurement, withSignerRequestMeasurement } from './withSignerMeasurement.js'; type ConstructorOptions = { metadata: AppMetadata; @@ -113,11 +106,11 @@ export class Signer { return this.accounts.length > 0; } - async handshake(args: RequestArguments) { - const correlationId = correlationIds.get(args); - logHandshakeStarted({ method: args.method, correlationId, isEphemeral: false }); + handshake = withHandshakeMeasurement( + { isEphemeral: false }, + async (args: RequestArguments): Promise => { + const correlationId = correlationIds.get(args); - try { // Open the popup before constructing the request message. // This is to ensure that the popup is not blocked by some browsers (i.e. Safari) await this.communicator.waitForPopupLoaded?.(); @@ -145,215 +138,190 @@ export class Signer { const decrypted = await this.decryptResponseMessage(response); this.handleResponse(args, decrypted); - logHandshakeCompleted({ method: args.method, correlationId, isEphemeral: false }); - } catch (error) { - logHandshakeError({ - method: args.method, - correlationId, - errorMessage: parseErrorMessageFromAny(error), - isEphemeral: false, - }); - throw error; } - } + ); + + request = withSignerRequestMeasurement( + { isEphemeral: false }, + async (request: RequestArguments): Promise => { + if (this.accounts.length === 0) { + switch (request.method) { + case 'wallet_switchEthereumChain': { + assertParamsChainId(request.params); + this.chain.id = Number(request.params[0].chainId); + return undefined as T; + } + case 'wallet_connect': { + // Wait for the popup to be loaded before making async calls + await this.communicator.waitForPopupLoaded?.(); + await initSubAccountConfig(); + + const subAccountsConfig = this.storeHelpers.subAccountsConfig.get(); + // Inject capabilities from config (e.g., addSubAccount when creation: 'on-connect') + const modifiedRequest = injectRequestCapabilities( + request, + subAccountsConfig?.capabilities ?? {} + ); + return this.sendRequestToPopup(modifiedRequest) as Promise; + } + case 'experimental_requestInfo': + case 'wallet_sendCalls': + case 'wallet_sign': { + return this.sendRequestToPopup(request) as Promise; + } + default: + throw standardErrors.provider.unauthorized(); + } + } - async request(request: RequestArguments) { - const correlationId = correlationIds.get(request); - logRequestStarted({ method: request.method, correlationId, isEphemeral: false }); + if (this.shouldRequestUseSubAccountSigner(request)) { + const correlationId = correlationIds.get(request); + logSubAccountRequestStarted({ method: request.method, correlationId }); + try { + const result = await this.sendRequestToSubAccountSigner(request); + logSubAccountRequestCompleted({ method: request.method, correlationId }); + return result as T; + } catch (error) { + logSubAccountRequestError({ + method: request.method, + correlationId, + errorMessage: parseErrorMessageFromAny(error), + }); + throw error; + } + } - try { - const result = await this._request(request); - logRequestCompleted({ method: request.method, correlationId, isEphemeral: false }); - return result; - } catch (error) { - logRequestError({ - method: request.method, - correlationId, - errorMessage: parseErrorMessageFromAny(error), - isEphemeral: false, - }); - throw error; - } - } + // Handle all experimental methods + if (request.method.startsWith('experimental_')) { + return this.sendRequestToPopup(request) as Promise; + } - async _request(request: RequestArguments) { - if (this.accounts.length === 0) { switch (request.method) { - case 'wallet_switchEthereumChain': { - assertParamsChainId(request.params); - this.chain.id = Number(request.params[0].chainId); - return; + case 'eth_requestAccounts': + case 'eth_accounts': { + const subAccount = this.storeHelpers.subAccounts.get(); + const subAccountsConfig = this.storeHelpers.subAccountsConfig.get(); + if (subAccount?.address) { + // if defaultAccount is 'sub' and we have a sub account, we need to return it as the first account + // otherwise, we just append it to the accounts array + this.accounts = + subAccountsConfig?.defaultAccount === 'sub' + ? prependWithoutDuplicates(this.accounts, subAccount.address) + : appendWithoutDuplicates(this.accounts, subAccount.address); + } + + this.callback?.('connect', { chainId: numberToHex(this.chain.id) }); + return this.accounts as T; } + case 'eth_coinbase': + return this.accounts[0] as T; + case 'net_version': + return this.chain.id as T; + case 'eth_chainId': + return numberToHex(this.chain.id) as T; + case 'wallet_getCapabilities': + return this.handleGetCapabilitiesRequest(request) as Promise; + case 'wallet_switchEthereumChain': + return this.handleSwitchChainRequest(request) as Promise; + case 'eth_ecRecover': + case 'personal_sign': + case 'wallet_sign': + case 'personal_ecRecover': + case 'eth_signTransaction': + case 'eth_sendTransaction': + case 'eth_signTypedData_v1': + case 'eth_signTypedData_v3': + case 'eth_signTypedData_v4': + case 'eth_signTypedData': + case 'wallet_addEthereumChain': + case 'wallet_watchAsset': + case 'wallet_sendCalls': + case 'wallet_showCallsStatus': + case 'wallet_grantPermissions': + return this.sendRequestToPopup(request) as Promise; case 'wallet_connect': { // Wait for the popup to be loaded before making async calls await this.communicator.waitForPopupLoaded?.(); await initSubAccountConfig(); - const subAccountsConfig = this.storeHelpers.subAccountsConfig.get(); - // Inject capabilities from config (e.g., addSubAccount when creation: 'on-connect') const modifiedRequest = injectRequestCapabilities( request, subAccountsConfig?.capabilities ?? {} ); - return this.sendRequestToPopup(modifiedRequest); - } - case 'experimental_requestInfo': - case 'wallet_sendCalls': - case 'wallet_sign': { - return this.sendRequestToPopup(request); - } - default: - throw standardErrors.provider.unauthorized(); - } - } - - if (this.shouldRequestUseSubAccountSigner(request)) { - const correlationId = correlationIds.get(request); - logSubAccountRequestStarted({ method: request.method, correlationId }); - try { - const result = await this.sendRequestToSubAccountSigner(request); - logSubAccountRequestCompleted({ method: request.method, correlationId }); - return result; - } catch (error) { - logSubAccountRequestError({ - method: request.method, - correlationId, - errorMessage: parseErrorMessageFromAny(error), - }); - throw error; - } - } - - // Handle all experimental methods - if (request.method.startsWith('experimental_')) { - return this.sendRequestToPopup(request); - } - - switch (request.method) { - case 'eth_requestAccounts': - case 'eth_accounts': { - const subAccount = this.storeHelpers.subAccounts.get(); - const subAccountsConfig = this.storeHelpers.subAccountsConfig.get(); - if (subAccount?.address) { - // if defaultAccount is 'sub' and we have a sub account, we need to return it as the first account - // otherwise, we just append it to the accounts array - this.accounts = - subAccountsConfig?.defaultAccount === 'sub' - ? prependWithoutDuplicates(this.accounts, subAccount.address) - : appendWithoutDuplicates(this.accounts, subAccount.address); - } - - this.callback?.('connect', { chainId: numberToHex(this.chain.id) }); - return this.accounts; - } - case 'eth_coinbase': - return this.accounts[0]; - case 'net_version': - return this.chain.id; - case 'eth_chainId': - return numberToHex(this.chain.id); - case 'wallet_getCapabilities': - return this.handleGetCapabilitiesRequest(request); - case 'wallet_switchEthereumChain': - return this.handleSwitchChainRequest(request); - case 'eth_ecRecover': - case 'personal_sign': - case 'wallet_sign': - case 'personal_ecRecover': - case 'eth_signTransaction': - case 'eth_sendTransaction': - case 'eth_signTypedData_v1': - case 'eth_signTypedData_v3': - case 'eth_signTypedData_v4': - case 'eth_signTypedData': - case 'wallet_addEthereumChain': - case 'wallet_watchAsset': - case 'wallet_sendCalls': - case 'wallet_showCallsStatus': - case 'wallet_grantPermissions': - return this.sendRequestToPopup(request); - case 'wallet_connect': { - // Wait for the popup to be loaded before making async calls - await this.communicator.waitForPopupLoaded?.(); - await initSubAccountConfig(); - const subAccountsConfig = this.storeHelpers.subAccountsConfig.get(); - const modifiedRequest = injectRequestCapabilities( - request, - subAccountsConfig?.capabilities ?? {} - ); - const result = await this.sendRequestToPopup(modifiedRequest); + const result = await this.sendRequestToPopup(modifiedRequest); - this.callback?.('connect', { chainId: numberToHex(this.chain.id) }); - return result; - } - // Sub Account Support - case 'wallet_getSubAccounts': { - const subAccount = this.storeHelpers.subAccounts.get(); - if (subAccount?.address) { - return { - subAccounts: [subAccount], - }; + this.callback?.('connect', { chainId: numberToHex(this.chain.id) }); + return result as T; } - - if (!this.chain.rpcUrl) { - throw standardErrors.rpc.internal('No RPC URL set for chain'); + // Sub Account Support + case 'wallet_getSubAccounts': { + const subAccount = this.storeHelpers.subAccounts.get(); + if (subAccount?.address) { + return { + subAccounts: [subAccount], + } as T; + } + + if (!this.chain.rpcUrl) { + throw standardErrors.rpc.internal('No RPC URL set for chain'); + } + const response = (await fetchRPCRequest( + request, + this.chain.rpcUrl + )) as GetSubAccountsResponse; + assertArrayPresence(response.subAccounts, 'subAccounts'); + if (response.subAccounts.length > 0) { + // cache the sub account + assertSubAccount(response.subAccounts[0]); + const subAccount = response.subAccounts[0]; + this.storeHelpers.subAccounts.set({ + address: subAccount.address, + factory: subAccount.factory, + factoryData: subAccount.factoryData, + }); + } + return response as T; } - const response = (await fetchRPCRequest( - request, - this.chain.rpcUrl - )) as GetSubAccountsResponse; - assertArrayPresence(response.subAccounts, 'subAccounts'); - if (response.subAccounts.length > 0) { - // cache the sub account - assertSubAccount(response.subAccounts[0]); - const subAccount = response.subAccounts[0]; - this.storeHelpers.subAccounts.set({ - address: subAccount.address, - factory: subAccount.factory, - factoryData: subAccount.factoryData, - }); + case 'wallet_addSubAccount': + return this.addSubAccount(request) as Promise; + case 'coinbase_fetchPermissions': { + assertFetchPermissionsRequest(request); + const completeRequest = fillMissingParamsForFetchPermissions(request); + const permissions = (await fetchRPCRequest( + completeRequest, + CB_WALLET_RPC_URL + )) as FetchPermissionsResponse; + const requestedChainId = hexToNumber(completeRequest.params?.[0].chainId); + this.storeHelpers.spendPermissions.set( + permissions.permissions.map((permission) => ({ + ...permission, + chainId: requestedChainId, + })) + ); + return permissions as T; } - return response; - } - case 'wallet_addSubAccount': - return this.addSubAccount(request); - case 'coinbase_fetchPermissions': { - assertFetchPermissionsRequest(request); - const completeRequest = fillMissingParamsForFetchPermissions(request); - const permissions = (await fetchRPCRequest( - completeRequest, - CB_WALLET_RPC_URL - )) as FetchPermissionsResponse; - const requestedChainId = hexToNumber(completeRequest.params?.[0].chainId); - this.storeHelpers.spendPermissions.set( - permissions.permissions.map((permission) => ({ - ...permission, - chainId: requestedChainId, - })) - ); - return permissions; - } - case 'coinbase_fetchPermission': { - const fetchPermissionRequest = request as FetchPermissionRequest; - const response = (await fetchRPCRequest( - fetchPermissionRequest, - CB_WALLET_RPC_URL - )) as FetchPermissionResponse; - - // Store the single permission if it has a chainId - if (response.permission && response.permission.chainId) { - this.storeHelpers.spendPermissions.set([response.permission]); + case 'coinbase_fetchPermission': { + const fetchPermissionRequest = request as FetchPermissionRequest; + const response = (await fetchRPCRequest( + fetchPermissionRequest, + CB_WALLET_RPC_URL + )) as FetchPermissionResponse; + + // Store the single permission if it has a chainId + if (response.permission && response.permission.chainId) { + this.storeHelpers.spendPermissions.set([response.permission]); + } + + return response as T; } - - return response; + default: + if (!this.chain.rpcUrl) { + throw standardErrors.rpc.internal('No RPC URL set for chain'); + } + return fetchRPCRequest(request, this.chain.rpcUrl) as Promise; } - default: - if (!this.chain.rpcUrl) { - throw standardErrors.rpc.internal('No RPC URL set for chain'); - } - return fetchRPCRequest(request, this.chain.rpcUrl); } - } + ); private async sendRequestToPopup(request: RequestArguments) { // Open the popup before constructing the request message. diff --git a/packages/account-sdk/src/sign/base-account/withSignerMeasurement.ts b/packages/account-sdk/src/sign/base-account/withSignerMeasurement.ts new file mode 100644 index 000000000..747c34445 --- /dev/null +++ b/packages/account-sdk/src/sign/base-account/withSignerMeasurement.ts @@ -0,0 +1,94 @@ +import { RequestArguments } from ':core/provider/interface.js'; +import { + logHandshakeCompleted, + logHandshakeError, + logHandshakeStarted, + logRequestCompleted, + logRequestError, + logRequestStarted, +} from ':core/telemetry/events/scw-signer.js'; +import { parseErrorMessageFromAny } from ':core/telemetry/utils.js'; +import { correlationIds } from ':store/correlation-ids/store.js'; + +type WithSignerMeasurementOptions = { + isEphemeral?: boolean; +}; + +/** + * Higher-order function that wraps a handshake handler with telemetry. + * + * Handles: + * - Retrieving correlation ID (set by provider) + * - Logging handshake start/completed/error + * + * @param options - Configuration options including isEphemeral flag + * @param handler - The actual handshake handler function + * @returns Wrapped handler with measurement instrumentation + */ +export function withHandshakeMeasurement( + options: WithSignerMeasurementOptions, + handler: (args: RequestArguments) => Promise +): (args: RequestArguments) => Promise { + return async (args: RequestArguments): Promise => { + const correlationId = correlationIds.get(args); + const measurementParams = { + method: args.method, + correlationId, + isEphemeral: !!options.isEphemeral, + }; + logHandshakeStarted(measurementParams); + + try { + await handler(args); + logHandshakeCompleted(measurementParams); + } catch (error) { + logHandshakeError({ + ...measurementParams, + errorMessage: parseErrorMessageFromAny(error), + }); + throw error; + } + }; +} + +/** + * Higher-order function that wraps a signer request handler with telemetry. + * + * Handles: + * - Retrieving correlation ID (set by provider) + * - Logging request start/completed/error + * + * Note: This is different from the provider's withMeasurement - it doesn't + * generate or clean up correlation IDs (the provider does that). + * + * @param options - Configuration options including isEphemeral flag + * @param handler - The actual request handler function + * @returns Wrapped handler with measurement instrumentation + */ +export function withSignerRequestMeasurement( + options: WithSignerMeasurementOptions, + handler: (args: RequestArguments) => Promise +): (args: RequestArguments) => Promise { + return async (args: RequestArguments): Promise => { + const correlationId = correlationIds.get(args); + const measurementParams = { + method: args.method, + correlationId, + isEphemeral: !!options.isEphemeral, + }; + logRequestStarted(measurementParams); + + try { + const result = await handler(args); + logRequestCompleted(measurementParams); + return result; + } catch (error) { + logRequestError({ + ...measurementParams, + errorMessage: parseErrorMessageFromAny(error), + }); + throw error; + } + }; +} + From 8f7b3538b9116f2dd2ba7c1d3c3d37a71a254ffc Mon Sep 17 00:00:00 2001 From: Felix Zhang Date: Tue, 16 Dec 2025 14:34:19 -0800 Subject: [PATCH 3/3] inheridence --- .../src/sign/base-account/EphemeralSigner.ts | 218 ++---------------- .../src/sign/base-account/Signer.ts | 36 +-- .../base-account/withSignerMeasurement.ts | 10 +- 3 files changed, 46 insertions(+), 218 deletions(-) diff --git a/packages/account-sdk/src/sign/base-account/EphemeralSigner.ts b/packages/account-sdk/src/sign/base-account/EphemeralSigner.ts index 87dad48c1..8d3692b2c 100644 --- a/packages/account-sdk/src/sign/base-account/EphemeralSigner.ts +++ b/packages/account-sdk/src/sign/base-account/EphemeralSigner.ts @@ -1,114 +1,32 @@ -import { Communicator } from ':core/communicator/Communicator.js'; import { standardErrors } from ':core/error/errors.js'; -import { RPCRequestMessage, RPCResponseMessage } from ':core/message/RPCMessage.js'; import { RPCResponse } from ':core/message/RPCResponse.js'; -import { AppMetadata, ProviderEventCallback, RequestArguments } from ':core/provider/interface.js'; -import { - withHandshakeMeasurement, - withSignerRequestMeasurement, -} from './withSignerMeasurement.js'; -import { SDKChain, createClients } from ':store/chain-clients/utils.js'; -import { correlationIds } from ':store/correlation-ids/store.js'; -import { createStoreHelpers, type StoreInstance } from ':store/store.js'; -import { - decryptContent, - encryptContent, - exportKeyToHexString, - importKeyFromHexString, -} from ':util/cipher.js'; -import { SCWKeyManager } from './SCWKeyManager.js'; - -type ConstructorOptions = { - metadata: AppMetadata; - communicator: Communicator; - callback: ProviderEventCallback | null; - storeInstance: StoreInstance; -}; +import { RequestArguments } from ':core/provider/interface.js'; +import { Signer } from './Signer.js'; +import { withSignerRequestMeasurement } from './withSignerMeasurement.js'; /** - * EphemeralSigner is designed for single-use payment flows. + * EphemeralSigner extends Signer for single-use payment flows. * * Key differences from Signer: - * 1. Uses its own isolated store instance to prevent race conditions - * 2. Cleanup clears the entire instance store (keys, state) - * 3. Does NOT pollute global store state used by other SDK instances - * 4. Optimized for one-shot operations like wallet_sendCalls and wallet_sign - * - * This prevents: - * - Race conditions when multiple ephemeral flows run concurrently - * - Pollution of global store state by ephemeral operations - * - KeyManager interference between concurrent ephemeral operations + * 1. isEphemeral: true for telemetry + * 2. Only supports wallet_sendCalls and wallet_sign methods + * 3. No-op handleResponse (no state updates) + * 4. More aggressive cleanup (clears all store state) */ -export class EphemeralSigner { - private readonly communicator: Communicator; - private readonly keyManager: SCWKeyManager; - private readonly storeHelpers: ReturnType; - private readonly storeInstance: StoreInstance; - - // Instance-local state (isolated from other signers) - private readonly chainId: number; - - constructor(params: ConstructorOptions) { - this.communicator = params.communicator; - this.storeInstance = params.storeInstance; - this.storeHelpers = createStoreHelpers(this.storeInstance); - this.keyManager = new SCWKeyManager(this.storeInstance); - // Note: We intentionally don't use the callback for ephemeral operations - // as we don't need to emit events for one-shot payment flows - - // Use the first chain from metadata as default - this.chainId = params.metadata.appChainIds?.[0] ?? 1; - - // Ensure chain clients exist for this chain - // Chain clients are shared infrastructure and can be safely shared - const chains = this.storeInstance.getState().chains; - if (chains && chains.length > 0) { - createClients(chains); - } +export class EphemeralSigner extends Signer { + // Ephemeral signer uses isEphemeral: true for telemetry + protected override get isEphemeral(): boolean { + return true; } - handshake = withHandshakeMeasurement( - { isEphemeral: true }, - async (args: RequestArguments): Promise => { - const correlationId = correlationIds.get(args); - - // Open the popup before constructing the request message. - await this.communicator.waitForPopupLoaded?.(); - - const handshakeMessage = await this.createRequestMessage( - { - handshake: { - method: args.method, - params: args.params ?? [], - }, - }, - correlationId - ); - const response: RPCResponseMessage = - await this.communicator.postRequestAndWaitForResponse(handshakeMessage); - - // store peer's public key - if ('failure' in response.content) { - throw response.content.failure; - } - - const peerPublicKey = await importKeyFromHexString('public', response.sender); - await this.keyManager.setPeerPublicKey(peerPublicKey); - - await this.decryptResponseMessage(response); - } - ); - - request = withSignerRequestMeasurement( - { isEphemeral: true }, + // Override request with limited method support + override request = withSignerRequestMeasurement( + () => ({ isEphemeral: this.isEphemeral }), async (request: RequestArguments): Promise => { - // Ephemeral signer only supports sending requests to popup - // for wallet_sendCalls and wallet_sign methods switch (request.method) { case 'wallet_sendCalls': - case 'wallet_sign': { + case 'wallet_sign': return this.sendRequestToPopup(request) as Promise; - } default: throw standardErrors.provider.unauthorized( `Method '${request.method}' is not supported by ephemeral signer` @@ -117,116 +35,20 @@ export class EphemeralSigner { } ); - private async sendRequestToPopup(request: RequestArguments) { - // Open the popup before constructing the request message. - await this.communicator.waitForPopupLoaded?.(); - - const response = await this.sendEncryptedRequest(request); - const decrypted = await this.decryptResponseMessage(response); - - return this.handleResponse(decrypted); - } - - private handleResponse(decrypted: RPCResponse) { + // No-op handleResponse - ephemeral signer doesn't update state + protected override async handleResponse(_request: RequestArguments, decrypted: RPCResponse) { const result = decrypted.result; - if ('error' in result) throw result.error; - - // For ephemeral signer, we don't update global store with response data - // We simply return the result value return result.value; } - /** - * Cleanup clears all instance-specific state including the entire ephemeral store. - * Since this signer has its own isolated store instance, we can safely clear everything. - * This prevents memory leaks and ensures complete isolation between ephemeral operations. - */ - async cleanup() { - // Clear the key manager (instance-specific cryptographic state) + // More aggressive cleanup - clears all store state + override async cleanup() { await this.keyManager.clear(); - - // Clear all store state - safe because this is an ephemeral store instance - // not shared with any other SDK instance this.storeHelpers.account.clear(); this.storeHelpers.subAccounts.clear(); this.storeHelpers.spendPermissions.clear(); this.storeHelpers.chains.clear(); this.storeHelpers.subAccountsConfig.clear(); - - // Note: The store instance itself will be garbage collected when - // the EphemeralSigner is no longer referenced - } - - private async sendEncryptedRequest(request: RequestArguments): Promise { - const sharedSecret = await this.keyManager.getSharedSecret(); - if (!sharedSecret) { - throw standardErrors.provider.unauthorized('No shared secret found when encrypting request'); - } - - const encrypted = await encryptContent( - { - action: request, - chainId: this.chainId, - }, - sharedSecret - ); - const correlationId = correlationIds.get(request); - const message = await this.createRequestMessage({ encrypted }, correlationId); - - return this.communicator.postRequestAndWaitForResponse(message); - } - - private async createRequestMessage( - content: RPCRequestMessage['content'], - correlationId: string | undefined - ): Promise { - const publicKey = await exportKeyToHexString('public', await this.keyManager.getOwnPublicKey()); - - return { - id: crypto.randomUUID(), - correlationId, - sender: publicKey, - content, - timestamp: new Date(), - }; - } - - private async decryptResponseMessage(message: RPCResponseMessage): Promise { - const content = message.content; - - // throw protocol level error - if ('failure' in content) { - throw content.failure; - } - - const sharedSecret = await this.keyManager.getSharedSecret(); - if (!sharedSecret) { - throw standardErrors.provider.unauthorized( - 'Invalid session: no shared secret found when decrypting response' - ); - } - - const response: RPCResponse = await decryptContent(content.encrypted, sharedSecret); - - // Process chain data from response if available - // We still update the shared ChainClients store since chain clients are meant to be shared - const availableChains = response.data?.chains; - if (availableChains) { - const nativeCurrencies = response.data?.nativeCurrencies; - const chains: SDKChain[] = Object.entries(availableChains).map(([id, rpcUrl]) => { - const nativeCurrency = nativeCurrencies?.[Number(id)]; - return { - id: Number(id), - rpcUrl, - ...(nativeCurrency ? { nativeCurrency } : {}), - }; - }); - - // Update shared chain clients (this is intentionally shared) - createClients(chains); - } - - return response; } } diff --git a/packages/account-sdk/src/sign/base-account/Signer.ts b/packages/account-sdk/src/sign/base-account/Signer.ts index 8c8b9c6c4..9a15eacf8 100644 --- a/packages/account-sdk/src/sign/base-account/Signer.ts +++ b/packages/account-sdk/src/sign/base-account/Signer.ts @@ -69,14 +69,14 @@ type ConstructorOptions = { }; export class Signer { - private readonly communicator: Communicator; - private readonly keyManager: SCWKeyManager; - private callback: ProviderEventCallback | null; - private readonly storeHelpers: ReturnType; - private readonly storeInstance: StoreInstance; + protected readonly communicator: Communicator; + protected readonly keyManager: SCWKeyManager; + protected callback: ProviderEventCallback | null; + protected readonly storeHelpers: ReturnType; + protected readonly storeInstance: StoreInstance; - private accounts: Address[]; - private chain: SDKChain; + protected accounts: Address[]; + protected chain: SDKChain; constructor(params: ConstructorOptions) { this.communicator = params.communicator; @@ -85,8 +85,8 @@ export class Signer { this.storeInstance = params.storeInstance ?? store; // Reuse global store helpers if using global store (important for testing/mocking) // Otherwise create new helpers for the custom store instance - this.storeHelpers = - this.storeInstance === store ? store : createStoreHelpers(this.storeInstance); + const isGloabalStore = this.storeInstance === store; + this.storeHelpers = isGloabalStore ? store : createStoreHelpers(this.storeInstance); this.keyManager = new SCWKeyManager(this.storeInstance); const { account, chains } = this.storeInstance.getState(); @@ -106,8 +106,12 @@ export class Signer { return this.accounts.length > 0; } + protected get isEphemeral(): boolean { + return false; + } + handshake = withHandshakeMeasurement( - { isEphemeral: false }, + () => ({ isEphemeral: this.isEphemeral }), async (args: RequestArguments): Promise => { const correlationId = correlationIds.get(args); @@ -142,7 +146,7 @@ export class Signer { ); request = withSignerRequestMeasurement( - { isEphemeral: false }, + () => ({ isEphemeral: this.isEphemeral }), async (request: RequestArguments): Promise => { if (this.accounts.length === 0) { switch (request.method) { @@ -323,7 +327,7 @@ export class Signer { } ); - private async sendRequestToPopup(request: RequestArguments) { + protected async sendRequestToPopup(request: RequestArguments) { // Open the popup before constructing the request message. // This is to ensure that the popup is not blocked by some browsers (i.e. Safari) await this.communicator.waitForPopupLoaded?.(); @@ -334,7 +338,7 @@ export class Signer { return this.handleResponse(request, decrypted); } - private async handleResponse(request: RequestArguments, decrypted: RPCResponse) { + protected async handleResponse(request: RequestArguments, decrypted: RPCResponse) { const result = decrypted.result; if ('error' in result) throw result.error; @@ -492,7 +496,7 @@ export class Signer { return filteredCapabilities; } - private async sendEncryptedRequest(request: RequestArguments): Promise { + protected async sendEncryptedRequest(request: RequestArguments): Promise { const sharedSecret = await this.keyManager.getSharedSecret(); if (!sharedSecret) { throw standardErrors.provider.unauthorized('No shared secret found when encrypting request'); @@ -511,7 +515,7 @@ export class Signer { return this.communicator.postRequestAndWaitForResponse(message); } - private async createRequestMessage( + protected async createRequestMessage( content: RPCRequestMessage['content'], correlationId: string | undefined ): Promise { @@ -526,7 +530,7 @@ export class Signer { }; } - private async decryptResponseMessage(message: RPCResponseMessage): Promise { + protected async decryptResponseMessage(message: RPCResponseMessage): Promise { const content = message.content; // throw protocol level error diff --git a/packages/account-sdk/src/sign/base-account/withSignerMeasurement.ts b/packages/account-sdk/src/sign/base-account/withSignerMeasurement.ts index 747c34445..a7f1a5b0d 100644 --- a/packages/account-sdk/src/sign/base-account/withSignerMeasurement.ts +++ b/packages/account-sdk/src/sign/base-account/withSignerMeasurement.ts @@ -21,15 +21,16 @@ type WithSignerMeasurementOptions = { * - Retrieving correlation ID (set by provider) * - Logging handshake start/completed/error * - * @param options - Configuration options including isEphemeral flag + * @param getOptions - Getter function for options (evaluated at call time, not definition time) * @param handler - The actual handshake handler function * @returns Wrapped handler with measurement instrumentation */ export function withHandshakeMeasurement( - options: WithSignerMeasurementOptions, + getOptions: () => WithSignerMeasurementOptions, handler: (args: RequestArguments) => Promise ): (args: RequestArguments) => Promise { return async (args: RequestArguments): Promise => { + const options = getOptions(); const correlationId = correlationIds.get(args); const measurementParams = { method: args.method, @@ -61,15 +62,16 @@ export function withHandshakeMeasurement( * Note: This is different from the provider's withMeasurement - it doesn't * generate or clean up correlation IDs (the provider does that). * - * @param options - Configuration options including isEphemeral flag + * @param getOptions - Getter function for options (evaluated at call time, not definition time) * @param handler - The actual request handler function * @returns Wrapped handler with measurement instrumentation */ export function withSignerRequestMeasurement( - options: WithSignerMeasurementOptions, + getOptions: () => WithSignerMeasurementOptions, handler: (args: RequestArguments) => Promise ): (args: RequestArguments) => Promise { return async (args: RequestArguments): Promise => { + const options = getOptions(); const correlationId = correlationIds.get(args); const measurementParams = { method: args.method,