import { type GetEthCodeFn, type PrepareDelegationResult } from "@toruslabs/ethereum-controllers"; import { type Hex, type TransactionReceipt } from "viem"; import { type EvmChain, type Signature } from "../../../core"; import { evmSigner, type JobHandle } from "../keyring"; import { type JobStatus, type MfaApprovalDetails, type PollOpts, type RemoteRequestOpts, type SignatureRequestStatus, type TxJobStatus, TxJobStatusValue } from "../remote-signing"; import type { EVMReadRequest, EVMReadResult, EVMTransaction, EVMTypedData } from "../types"; import type { Delegation, DelegationTypedData, Execution, RelayAuthorization, UnsignedDelegation } from "../types/Delegation"; import type { ControllerStack } from "./createEvmControllerStack"; import { EVMWalletClient, type EVMWalletClientCtorParams } from "./EVMWalletClient"; import type { SignTypedDataRequestOptions } from "./signTypedDataRequestOptions"; export type { RemoteRequestOpts } from "../remote-signing"; export type { SignTypedDataRequestOptions } from "./signTypedDataRequestOptions"; export type ControllerEVMWalletClientOpts = EVMWalletClientCtorParams & { stack: ControllerStack; chain: EvmChain; /** * Polling options applied when `sendTransaction` / `signMessage` / * `signTypedData` wait for terminal status internally in server mode. * Callers using the low-level `submit*` methods + `awaitJob` can pass * per-call overrides; this is the default for the high-level * convenience methods. Useful for tests (tight delays) and for the CLI * to tune the MFA wait window. Ignored in BYOK mode. */ pollOpts?: PollOpts; }; export type FeeOverrides = { maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint; gasPrice?: bigint; }; /** * Controller-backed EVM wallet client. The behavior depends on the * keyring kind on `stack.keyring`: * * - `kind === "byok"`: every on-chain path delegates to toruslabs's * `TransactionController` (nonce locking, gas defaults, EIP-1559 * handling, pending-tx detection, resubmission). Signing is synchronous * in-process. * * - `kind === "server"`: every signing path (`sendTransaction`, * `signMessage`, `signTypedData`) submits the unsigned payload to the * remote signing service via the keyring's HTTP methods, polls for * terminal status, and returns the signed result. The in-process * `TransactionController` is bypassed entirely. The CLI's * `mm watch ` wires through {@link submitTransaction} / * {@link submitPersonalSign} / {@link submitSignTypedData} (returning * immediately with a `pollingId`) and {@link getJobStatus}. */ export declare class ControllerEVMWalletClient extends EVMWalletClient { private readonly stack; private readonly chain; private readonly publicClient; private readonly pollOpts?; /** * Persistent auto-approve handler. A single listener is attached to * `TX_UNAPPROVED` at construction and filters by `req.origin === REQ_ORIGIN` * so concurrent `sendTransaction` calls don't race on per-call `once()` * registrations (two `once`s on the same event fire on the first emit, * leaving the second tx with no listener and hanging forever). * * Only attached in BYOK mode; in server mode the controller stack's * transaction controller is never invoked. */ private readonly autoApproveHandler?; constructor(opts: ControllerEVMWalletClientOpts); getAddress(): string; getChain(): EvmChain; /** * BYOK mode: applies the EIP-191 prefix in the keyring (toruslabs' * `signPersonalMessage`) and returns the signature. * * Server mode: submits the message to the remote signing service, polls * until terminal, and returns the signature on `SIGNED`. Throws * {@link JobFailedError} on any other terminal status. For non-blocking * submission, use {@link submitPersonalSign}. * * `opts` overrides the constructor-level {@link PollOpts} for this call. * Notably, `opts.signal` lets callers abort the poll early (e.g. CLI * `SIGINT` while waiting on MFA). Ignored in BYOK mode (signing is * in-process). */ signMessage(message: string, intent?: string, opts?: RemoteRequestOpts): Promise; /** * BYOK mode: delegates to the keyring's typed-data path. * * Server mode: submits the typed-data payload to the remote signing * service, polls until terminal, and returns the signature on `SIGNED`. * Throws {@link JobFailedError} on any other terminal status. For * non-blocking submission, use {@link submitSignTypedData}. * * `opts` overrides the constructor-level {@link PollOpts} for this call. * Notably, `opts.signal` lets callers abort the poll early. Ignored in * BYOK mode. * * `request` supplies intent and optional trading-protocol context for * server-mode `eth_signTypedData_v4` requests; ignored in BYOK mode. */ signTypedData(data: EVMTypedData, request?: SignTypedDataRequestOptions, opts?: PollOpts): Promise; read(request: EVMReadRequest): Promise; getEthCode(): GetEthCodeFn; getNativeBalance(): Promise; /** * Block until `hash` is included in a block (and, optionally, has accrued * `confirmations` blocks of depth) and return the receipt. Routes through * the same RPC the controller uses, so callers don't need to construct a * separate viem client. */ waitForReceipt(hash: string, opts?: { confirmations?: number; timeout?: number; pollingInterval?: number; }): Promise; /** * BYOK mode: builds tx params, hands them to `TransactionController`, * which signs and broadcasts in-process; resolves at submitted-status * with the broadcast hash. * * Server mode: submits the unsigned params to the remote signing * service's transaction engine, polls until terminal, and resolves * with the confirmed `txHash`. Throws {@link JobFailedError} if the * service ends in a non-`CONFIRMED` terminal status. For non-blocking * submission, use {@link submitTransaction} + {@link awaitJob}. * * `opts` overrides the constructor-level {@link PollOpts} for this call. * Notably, `opts.signal` lets callers abort the poll early (e.g. CLI * `SIGINT` while waiting on MFA or broadcast). Ignored in BYOK mode * (broadcasting is owned by `TransactionController` in-process). */ /** * The one remaining mode branch. BYOK mode broadcasts in-process via * `TransactionController`; server mode submits + polls + extracts `txHash`. * These mechanics are irreducibly different — no abstraction collapses them * without losing observability into either path. * * Concurrency note: the server and server-backed BYOK paths route through * `prepareTransaction` with `fillNonce: true`, which releases its * `nonceTracker` lock as soon as it reads `nextNonce`. The same nonce-race * caveat documented on {@link submitTransaction} applies here — callers * issuing back-to-back server-backed `sendTransaction` calls without * awaiting terminal status may receive duplicate nonces, so serialise * per-address at the call site. */ sendTransaction(tx: EVMTransaction, intent?: string, opts?: RemoteRequestOpts): Promise<{ hash: string; status: TxJobStatusValue; }>; /** * Server / server-backed BYOK: submit an async transaction-sign + broadcast * job to the remote signing service without waiting for it to finish. * Returns the `pollingId` (the service's `requestId`) plus the initial * server-reported status. Pair with {@link getJobStatus} or * {@link awaitJob} (or `mm watch ` from the CLI). * * The SDK fills missing `nonce` / `gas` / fee fields by routing the * caller's payload through `TransactionController.prepareTransaction` * with `fillNonce: true`, so the service always receives a fully * specified tx. The service is EIP-1559 only; legacy `gasPrice` is * rejected client-side via {@link assertEip1559Tx} before any request * goes out, and again after `prepareTransaction` runs in case the * controller fell back to a legacy estimate. * * Concurrency note: `prepareTransaction` releases its `nonceTracker` * lock as soon as it reads `nextNonce`. Callers issuing multiple * `submitTransaction` calls back-to-back without waiting for terminal * status may receive duplicate nonces; the backend currently does not * dedupe, so serialise per-address at the call site or let the remote * signing service own nonce assignment once that lands. */ submitTransaction(tx: EVMTransaction, intent?: string, requestId?: string, bridgeQuoteId?: string): Promise; /** * Server / server-backed BYOK: submit an async `personal_sign` request to * the remote signing service's unified signature endpoint. The service * evaluates policy + collects MFA + signs; the signature is returned on * the terminal `SIGNED` status payload. */ submitPersonalSign(message: string, intent?: string, requestId?: string): Promise; /** * Server / server-backed BYOK: submit an async `eth_signTypedData_v4` * request. Same flow as {@link submitPersonalSign} but for EIP-712 * typed-data payloads. * * `request` supplies intent and optional trading-protocol context forwarded * in the signature-request payload. */ submitSignTypedData(data: EVMTypedData, request?: SignTypedDataRequestOptions): Promise; /** * Server / server-backed BYOK: one-shot status fetch. Powers * `mm watch `. Callers must supply a {@link JobHandle} — the * `kind` discriminator picks the right backend endpoint. */ getJobStatus(handle: JobHandle): Promise; /** Server / server-backed BYOK: poll until terminal with exponential backoff. */ awaitJob(handle: JobHandle, opts?: PollOpts): Promise; /** * Server / server-backed BYOK: unified MFA approval lookup by request id * (`pollingId`). Throws `RemoteSigningError` `NOT_FOUND` when no approval * exists for the request yet. */ getMfaApproval(requestId: string): Promise; /** * Non-throwing probe: is native balance sufficient for `value + gas * feePerGas`? * * Populates gas and fees via `TransactionController.prepareTransaction` (same * path as {@link sendTransaction}), then applies the same balance check as * MetaMask extension's `useHasInsufficientBalance` / * {@link assertSufficientNativeFunds}. * * Returns `false` on {@link InsufficientNativeFundsError}. On RPC/prepare * failures returns `true` (fail-safe → do not force gasless routing). */ isNativeBalanceSufficient(tx: EVMTransaction): Promise; /** * Sign `tx` without broadcasting. BYOK-mode only — the remote signing * service always broadcasts, so this throws * {@link UnsupportedInRemoteMode} in server mode. */ signTransactionOnly(tx: EVMTransaction): Promise<{ signedTx: string; }>; /** * BYOK-mode only. Throws {@link UnsupportedInRemoteMode} in server mode. * * Reports whether EIP-7702 upgrade is supported on this chain and whether * the wallet already delegates via `eth_getCode` (delegation prefix * `0xef0100`). `isUpgraded` is true when a delegation contract address is * present in code. */ getUpgradeStatus(): Promise<{ isSupported: boolean; isUpgraded: boolean; upgradeContractAddress?: Hex; delegationAddress: Hex | null; }>; /** * BYOK-mode only. Throws {@link UnsupportedInRemoteMode} in server mode. * * Signs an EIP-7702 authorization for this EOA delegating to the * {@link MetaMask_EIP7702_Stateless_Delegator}. Upgrading to any other * contract is intentionally not supported. Throws if the current chain * does not support the MetaMask delegator. The auth nonce is always * fetched at `pending` so it matches the account nonce at inclusion. * * Returns the keyring-signed authorization and the same tuple in relay * wire shape for gasless `authorizationList` payloads. */ sign7702Authorization(): Promise<{ authorization: Awaited["signEip7702Authorization"]>>; relay: RelayAuthorization; }>; /** * BYOK-mode only. Throws {@link UnsupportedInRemoteMode} in server mode. * * Builds an unsigned EIP-712 delegation for `executions` via * `prepareDelegation`. Pair with {@link signDelegation}, then encode with * `encodeRedeemDelegationsRequest` before relay submission. */ buildGaslessBatch(executions: Execution[]): PrepareDelegationResult; /** * BYOK-mode only. Throws {@link UnsupportedInRemoteMode} in server mode. * * Signs `unsigned` against `typedData` through the keyring EIP-712 path * and returns a signed {@link Delegation} ready for * `encodeRedeemDelegationsRequest`. */ signDelegation(unsigned: UnsignedDelegation, typedData: DelegationTypedData): Promise; dispose(): void; /** * Submit a paid ERC-7821 `execute()` batch (`encoding: erc7821`) without * waiting for a terminal status. Pre-encodes `execute(executions)` via * {@link generateEIP7702BatchTransaction}. * * BYOK: signs the type-4 tx in-process (attaching an EIP-7702 * `authorizationList` on first use when `autoUpgrade !== false`) and posts * the signed raw tx for policy evaluation + broadcast. * * Server: posts the unsigned `execute()` calldata with `encoding: erc7821`; * Mimir injects/signs the 7702 authorization after MFA. * * Pair with {@link getJobStatus} / {@link awaitJob} (or * {@link sendErc7821Batch} for the blocking convenience path). */ submitErc7821Batch(executions: Execution[], opts?: { intent?: string; autoUpgrade?: boolean; requestId?: string; bridgeQuoteId?: string; }): Promise; /** * Blocking ERC-7821 batch: {@link submitErc7821Batch} + poll until * `CONFIRMED` or `BROADCASTED`. Throws {@link JobFailedError} on other * terminal statuses. Mirrors {@link sendGaslessRelay} for paid batches. */ sendErc7821Batch(executions: Execution[], intent?: string, opts?: RemoteRequestOpts & { autoUpgrade?: boolean; }): Promise<{ hash?: Hex; requestId: string; status: TxJobStatusValue; }>; /** * Submit a gasless EIP-7702 relay batch (`eth_sendRelayTransaction` / * `redeemDelegations`) to the remote signing service without waiting for a * terminal status. BYOK mode signs the delegation locally (and optionally * attaches an EIP-7702 upgrade authorization); server mode sends an * unsigned delegation for the service to sign after MFA. * * Pair with {@link getGaslessRelayJobStatus} / {@link awaitGaslessRelayJob} * (or {@link sendGaslessRelay} for the blocking convenience path). */ submitGaslessRelay(executions: Execution[], opts?: { intent?: string; autoUpgrade?: boolean; requestId?: string; bridgeQuoteId?: string; }): Promise; /** * Blocking gasless relay: {@link submitGaslessRelay} + poll until * `CONFIRMED` or `BROADCASTED`. Throws {@link JobFailedError} on other * terminal statuses. Mirrors {@link sendTransaction} for relay batches. */ sendGaslessRelay(executions: Execution[], intent?: string, opts?: RemoteRequestOpts & { autoUpgrade?: boolean; }): Promise<{ hash?: Hex; requestId: string; status: TxJobStatusValue; }>; /** One-shot status fetch for a gasless relay job (server or server-backed BYOK). */ getGaslessRelayJobStatus(handle: JobHandle): Promise; /** Poll a gasless relay job until terminal with exponential backoff. */ awaitGaslessRelayJob(handle: JobHandle, opts?: PollOpts): Promise; /** * Unguarded upgrade-status probe (no BYOK assertion). Reads on-chain code * via `eth_getCode`, which works in both keyring modes — server mode needs * it too, to estimate ERC-7821 gas against the delegated code path even * though Mimir (not the client) signs the authorization. The public * {@link getUpgradeStatus} wraps this behind `assertByok`. */ private resolveUpgradeStatus; /** * Gas for a same-sender ERC-7821 batch that also upgrades the account via * EIP-7702, estimated against the *simulated* post-upgrade state. * * The controller's own estimate is unusable here: `TransactionGasUtil` * injects a dummy authorization signature, so the node can't recover the * authority and never applies the delegation during `eth_estimateGas`. The * `to === from` call then hits a still-codeless EOA and collapses to a no-op * (~56k on Polygon) — far below the ~400k the batch needs once the * delegation executes, guaranteeing an out-of-gas revert. * * We instead state-override the sender's code with the EIP-7702 delegation * designator (`0xef0100 ‖ delegator`) so the node runs the delegated * `execute()` and returns a realistic figure, then apply the controller's * default 1.5x `addGasBuffer` multiplier. Both modes rely on this: Mimir * trusts the client's gasLimit verbatim and never re-estimates. */ private estimateUpgradeBatchGas; private buildGaslessRelayWire; private submitGaslessRelayWire; /** * Single tx-population path for both modes. Translates an `EVMTransaction` * (bigints + viem-style fields) into upstream `TransactionParams` (hex * strings) and hands it to `TransactionController.prepareTransaction`, * which fills `gas`, `maxFeePerGas`/`maxPriorityFeePerGas` (or `gasPrice` * on legacy chains), and optionally a nonce. * * Caller-supplied values always win: pre-populated fields short-circuit * the controller's defaults. The mode-specific call sites pick `fillNonce` * based on who owns nonce assignment — BYOK mode lets the controller's * approval flow assign it inside `addNewUnapprovedTransaction`; server * mode owns it client-side because the remote signing service does not. * * Caller-supplied nonces are threaded through `TransactionParams.nonce` * (NOT `customNonceValue`): upstream `prepareTransaction` only short- * circuits its `getNonceLock` call when `txMeta.transaction.nonce` is set, * and only `nonce` ends up on the prepared output payload. */ /** * Shared tail for the server-backed transaction paths: if `initial` is not * already terminal, await the job to terminal status via `awaitJob`, then * assert it confirmed/broadcasted and extract the `txHash`. Throws * {@link JobFailedError} on a non-terminal-success status. */ /** Shared remote-job adapter for server mode and BYOK (submit + poll + status fetch). */ private remoteJobAdapter; private awaitTerminalTxHash; private prepareViaController; /** * Reject before submission if the sender can't cover `value + gas * fee`. * * Upstream `TransactionGasUtil.analyzeGasUsage` swallows `eth_estimateGas` * failures (including "insufficient funds") and substitutes 95% of the * block gas limit as a fallback, so by the time we hold a `PreparedTransaction` * the controller has erased the signal. We re-check directly against * `eth_getBalance` here — the populated `gas` + `maxFeePerGas` (or * `gasPrice` on legacy chains) gives us the worst-case cost, and one * extra RPC is cheap relative to a wasted broadcast or a hung remote * signing job. * * Mirrors the wallet-services frontend's `isNotSufficientBalance` gate * (`totalFee = value + gas*fee > fromAddressBalance`), surfaced here as * a throw because there is no human in the loop to disable a Sign button. */ private assertSufficientNativeFunds; /** * Convert the upstream `PreparedTransaction` (hex strings, fully populated) * to a `TransactionParams` shape consumable by `addNewUnapprovedTransaction`. * Near pass-through — both shapes share field names — but kept explicit * so the type contract at the call site is obvious. */ private preparedToByokTxParams; /** Convert a fully-prepared tx to the remote signing service's wire DTO. */ private preparedToRemoteTx; private signatureCtx; private typedDataSignatureCtx; /** * Merge constructor-level `pollOpts` (defaults) with per-call `opts` * (overrides). Per-call fields win, including `signal` — callers can * attach a fresh `AbortSignal` for a single send/sign without disturbing * the client-wide defaults. Strips `requestId` before merging poll fields. */ private resolvePollOpts; private assertByok; }