RPC methods reference
Audience: Users and library authors who want to know which JSON-RPC calls the plugin handles.
RPC behaviour
The plugin installs a network hook on every connection. On a connection whose network has no kmsAccounts, it passes every request through. Otherwise it behaves as follows:
eth_accountsandeth_requestAccountsreturn the network's own accounts followed by the KMS addresses, without duplicates.eth_requestAccountsis passed on aseth_accounts, as Hardhat's local accounts do, since many nodes implement only that. If the downstream call fails, only the KMS addresses are returned.- A KMS address comes from the
addresspin when one is set, with no KMS call. Otherwise it comes from a public-key lookup, run in parallel across keys on first use. A failed lookup is not cached; the next request retries it. Two keys that resolve to the same address are an error:<key> and <key> are the same account (<address>); list each key once. - Five signing methods are intercepted, but only when the address is a KMS account:
eth_sendTransaction,eth_signTransaction,eth_sign,personal_signandeth_signTypedData_v4. eth_sign([address, data]) andpersonal_sign([data, address]) use EIP-191 with the message prefix, the same semantics as Hardhat core. Their params are checked with Hardhat's own validators: data must be strict hex (0xand an even number of digits).eth_signTypedData_v4([address, typedData]) accepts the typed data as an object or a JSON string. Its shape is checked before any KMS call. Whendomain.chainIdis set, it must equal the connection's chain (read witheth_chainId) unlesskms.allowCrossChainTypedDataistrue, and a mismatch fails before any KMS signing call. A number, bigint, hex string with a lowercase0xprefix or decimal string is accepted, and0is checked like any other value. Typed data withoutdomain.chainIdis signed, as MetaMask, Hardhat and Foundry do; its signature is valid on every chain.eth_sendTransactionfills the same fields as Hardhat does for a local account (fees, gas, nonce, chain id), signs the transaction with the KMS key and sends it to the node aseth_sendRawTransaction. It returns the transaction hash. The nonce can be higher than a local account's, since the plugin also counts the nonces the node accepted on the same connection and those held by library accounts (Which nonce a send gets). The signature bytes can differ from a local account's for the same request, and so can the transaction hash (Every signature is verified).eth_signTransactionfills and signs the same way, and returns the signed raw transaction as hex without sending it, likecast mktxand viem's json-rpcsignTransaction. The nonce is read from the node and not reserved: sending another transaction first makes the signed one stale. Hardhat's local accounts do not signeth_signTransaction. Thekms sign-txtask does the same from the command line, for any configured key.- A transaction's
chainId, when set, must equal the connection's chain id (read witheth_chainId, and checked against the network'schainIdwhen the config sets one). A mismatch fails before any KMS call. - No RPC method signs a bare digest.
connection.kms.getAccount(address)gives library code a viem account for a KMS key, outside the JSON-RPC path. viem sends its transactions witheth_sendRawTransaction, oreth_sendRawTransactionSyncforsendTransactionSyncandwriteContractSync; see Library accounts.eth_sendRawTransactionandeth_sendRawTransactionSync(EIP-7966) from a KMS account go to the node unchanged, in turn with that account's sends (see below). No answer is rewritten:eth_getTransactionCountand every other read pass through untouched.wallet_sendTransactionfrom a KMS account fails with JSON-RPC error-32601(method not found) and never reaches the node. The plugin sends a KMS account's transactions only througheth_sendTransaction. viem sendswallet_sendTransactiononce after aneth_sendTransactionerror such as-32000. On-32601it throws theeth_sendTransactionerror and keeps usingeth_sendTransactionon that client, so an endpoint that answerswallet_sendTransactioncannot take later sends away from the KMS key. The plugin reads the sender fromfromin the first param, in any letter case of its hex digits. Awallet_sendTransactionwithoutfrom, or from another address, passes through unchanged.wallet_sendCalls(EIP-5792) from a KMS account fails the same way, with-32601, and never reaches the node. The plugin does not implement EIP-5792. viem'ssendCallssends this method. Withexperimental_fallback: true, viem answers-32601by sending each call as its own transaction, so the KMS key signs each one. Without it, viem throws aTransactionExecutionErrorwith the-32601error in its cause chain. viem does not fall back, and throwsAtomicityNotSupportedErrororUnsupportedNonOptionalCapabilityError, when the batch hasforceAtomicand more than one call, or a capability withoutoptional: true. A client created withdataSuffix: { required: true }adds such a capability to every batch. The plugin reads the sender fromfromin the first param. EIP-5792 makesfromoptional: awallet_sendCallswithoutfrom, or from another address, passes through unchanged.- Every other method passes through untouched.
When from (or the address param) is not a KMS address, the request passes through. If the sender is not a local account either, the node or Hardhat refuses it, and the plugin appends the network's checksummed KMS addresses to that error. On a simulated network with one KMS account, a request from 0x1111111111111111111111111111111111111111 fails with:
Unknown account 0x1111111111111111111111111111111111111111. The KMS account on this network is 0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826.The error then shows a mistyped address or a key missing from kmsAccounts. It keeps its class, code and data, and Hardhat's CLI prints the list too. At most 10 addresses are listed, then "and N more". No key id is ever named. The plugin recognizes these errors:
- Hardhat's simulated network:
Unknown account <address>, code -32000. - Hardhat's local accounts on a network with
accounts:HHE716: Account "<address>" is not managed by the node you are connected to. - Geth:
unknown account, code -32000. - Reth:
unknown account, code -32602. - Anvil:
No Signer available, code -32602.
Every other error passes through unchanged.
An eth_sendTransaction or eth_signTransaction without from gets the sender Hardhat would give it: the network's from when the config sets one, otherwise the first address of eth_accounts, in the plugin's order (the network's own accounts, then the KMS addresses). The plugin sets from to that sender. When the sender is a KMS account, the plugin signs; otherwise the request passes on with from set, and Hardhat or the node signs it. A transaction therefore never reaches the node unsigned with a KMS address as its sender. Hardhat's sender handlers cover eth_sendTransaction but not eth_signTransaction; the plugin chooses the sender for both. When there is no sender (an empty or invalid eth_accounts answer), the request passes on unchanged.
This covers only a request that reaches the network without from. hardhat-viem, hardhat-ethers and Ignition set from themselves, and their default wallet client, signer or sender is the first address of eth_accounts, whatever the network's from says. Name the KMS key's address instead, as shown in Choose the sender by address.
The plugin copies a KMS account's transaction when the request arrives, so changing the request object after the call does not change what is signed. A KMS account's transaction that cannot be copied (one holding a function, for example) fails with the transaction must be plain data, and nothing is sent. Requests from other senders pass through as they came; for a transaction without from, only from is added, on a shallow copy.
kmsAccounts on the default network prints a warning, because tasks and tests use that network when no --network is given; see the configuration reference.
Parallel sends and failed broadcasts
eth_sendTransaction calls from one KMS account on one chain run one at a time within a process, across every connection the process has to that chain. Which nonce a send gets gives the nonce rule. A library account's send through the connection takes its turn the same way, from its nonce to its broadcast; see Library accounts. Sends from other accounts, or to other chains, do not wait for each other. eth_signTransaction does not wait for sends.
A send whose broadcast hangs keeps the account's turn until Hardhat's network timeout (the network's timeout, 300 seconds by default) ends it. The account's sends waiting behind it do not wait that long: each fails after 120 seconds in which none of the account's earlier sends finished. A slow RPC endpoint can trigger this.
Three limits keep a waiting send from hanging. Each one fails the send with an error that names the account and the chain, and that send is not signed or sent:
- A send from an account and chain, made by code that runs inside a send from the same account and chain, fails at once. Another plugin's network hook that sends while the plugin fills a transaction is one example. It would otherwise wait for itself forever.
- A send that has waited 120 seconds while none of the account's earlier sends finished fails.
- When 1024 sends from one account on one chain are already waiting, the next one fails at once.
The first limit also covers a send that nothing waits for. A send started from inside another send's hook, or from a listener that the hook triggers (an EventEmitter.emit, or a callback queued with setImmediate or queueMicrotask during the hook), fails at once even if no code awaits it. If nothing catches that error, it is an unhandled rejection, which ends the Node.js process. Await such a send and catch its error, or start it after the outer send returns.
The numbers 120 and 1024 are fixed, and cannot be configured in 1.0.
A send whose connection closes while it waits for its KMS signature or for the account's turn is not sent. It fails with core.tx.connection-closed, and nothing reaches the node; send it again on an open connection. A send that is already being broadcast when the connection closes is not stopped.
Which nonce a send gets
On an http network, a send whose caller gives no nonce takes the highest of:
- the node's pending count for the account (
eth_getTransactionCountwith"pending"); - one more than the highest nonce the node accepted from the account on the same connection;
- one more than any nonce handed to a library account's client on the same connection that still counts.
A nonce in the request is always used, even one that was already sent, so a replacement transaction with the same nonce and higher fees goes through, as Hardhat Ignition sends for a stuck transaction. On edr-simulated networks the node's pending count is always up to date, so a send takes it as is, except past a gap. While a nonce is kept for a library account's client, the node's pending count stops at it, so the plugin also counts the nonces the node accepted on the same connection that its pending count has not reached. For this, the plugin records the hashes of the transactions the node accepted on the connection, up to 256 per account. When the pending count falls while a nonce is kept, as after evm_revert, hardhat_reset or hardhat_dropTransaction, the kept nonce still counts. A send then takes the lowest nonce at or above the new pending count that no kept nonce uses and at which the node holds none of the connection's recorded transactions. The plugin asks the node with eth_getTransactionByHash, and skips a nonce whose lookup fails. It does not see a revert that runs while one of your sends is still in flight; await a send before you revert.
The send lock and the nonce memory have different scopes:
| Scope | Sends run one at a time | Nonces the plugin remembers |
|---|---|---|
| One connection | Yes | Yes |
| Several connections in one process | Yes | No: one memory per connection |
| Several processes | No | No |
What this means for nonces:
- On one connection, each send gets a nonce above every nonce the node accepted from the account on that connection, even from a node whose pending count lags behind, such as a load-balanced RPC endpoint. Parallel sends on one connection therefore get distinct nonces, unless a broadcast got no answer (Failed broadcasts).
- Each
network.create()call opens a new connection, with its own memory. Sends on two connections to the same chain still wait for each other, but a send on the second connection knows only the node's pending count and its own connection's nonces. If the node's pending count has not caught up with a send from the first connection, the second gets the same nonce.network.getOrCreate()returns the same connection on each call for a network, so a script that uses it has one connection per network and avoids this. - Separate processes are not coordinated: two
hardhat runcommands that send from the same KMS key at the same time can choose the same nonce.
When two transactions share a nonce, at most one of them is mined. While the first is still in the pool, a second with fees at least 10 % higher (Geth's default) replaces it, and neither sender gets an error. With lower fees the second send fails with replacement transaction underpriced, and once the first is mined, with nonce too low.
Failed broadcasts
When the node answers a broadcast with an error, such as a revert or "nonce too low", eth_sendTransaction fails with that error, unchanged, as it would for a local account. A reverted transaction keeps its revert data and transactionHash. A rate limit that outlasts Hardhat's own retries (HTTP 429) is such an answer too.
When Hardhat cannot connect to the node, nothing was sent, and eth_sendTransaction fails with Hardhat's "Cannot connect to the network" error, unchanged.
When no answer comes back, because the request times out or fails with an HTTP error status, the transaction may still be on its way. eth_sendTransaction then fails with JSON-RPC error code -32000. The error's transactionHash and data.hash are the transaction hash, so you can look the transaction up; Hardhat Ignition reads transactionHash and follows the transaction. A gateway's answer that its backend timed out (code -32603, or a message saying the request timed out or a deadline was exceeded) leaves the outcome open in the same way, but it is passed on unchanged, without the hash.
After either of these, if the same request, with the same params, is sent again on the same connection within 120 seconds, the plugin sends the same signed transaction again instead of signing a new one, and returns its hash. A node that answers "already known" counts as success, and so does a refusal of those bytes when the node turns out to have the transaction. Each such retry uses up the kept transaction; if the retry also gets no answer, a gateway's timeout answer or a refused connection, the transaction is kept again for another 120 seconds. If another send has used the transaction's nonce in the meantime and the node does not have it, the retry is signed afresh instead. A request that succeeded is never repeated this way, so sending the same transaction twice on purpose still sends two transactions. viem does not repeat eth_sendTransaction by itself, so only a caller that repeats the request gets this. The wallet_sendTransaction that viem sends once after a -32000 error is refused for a KMS account (see above), so viem throws the eth_sendTransaction error; see Recover from an uncertain transaction send.
The plugin remembers each of the account's uncertain transactions on the connection that sent it, whether the plugin or the caller chose its nonce. The account's next send on that connection without a caller-supplied nonce first asks the node about each of them with eth_getTransactionByHash, so that the send does not reuse a nonce the node has. If the node has one, or the lookup itself fails, the send takes a higher nonce; a failed lookup can leave a gap, but never reuses a nonce the node may have. If the node answers that it does not have it, the node's pending count decides as usual, and the send can take the same nonce. If the first transaction reaches the node after all, the two then share a nonce, as described above. The question is asked once; behind a load balancer it can reach a backend that does not have the transaction yet. A send with a nonce does not ask, but its own uncertain transaction joins the others, so it never hides an earlier one. A send on another connection never learns of the transaction. The plugin keeps up to 256 per account; beyond that, the oldest counts as in the node without a lookup, so later sends skip its nonce, which can leave a gap. On edr-simulated networks the plugin remembers them and asks too: the node's pending count stops at a nonce kept for a library account's client, so a transaction past that gap would not show in it.
To check whether such a transaction arrived, and to fill or replace a nonce, follow Recover from an uncertain transaction send.
Supported transaction types
| Type | Status |
|---|---|
| Legacy | Supported, EIP-155 only. |
| EIP-2930 | Supported. |
| EIP-1559 | Supported. |
| EIP-7702 | Supported with a pre-signed authorizationList. |
| EIP-4844 (blob) | Not supported. Hardhat core does not support it either. |
The fields decide the type, as in Hardhat: authorizationList gives EIP-7702, maxFeePerGas EIP-1559, accessList EIP-2930, and anything else legacy. With the network's default gasPrice: "auto", a request without fee fields gets EIP-1559 fees when the node supports them. A transaction with blobs, blobVersionedHashes, maxFeePerBlobGas or type 3 from a KMS account fails before any request to the node. Type 3 counts in any form: a hex quantity in either case and with any leading zeros ("0x3", "0x03", "0X3"), a number, a bigint, the decimal string "3" or "eip4844", with or without surrounding whitespace. Type 3 is refused even with fee fields.
A type that is a number is checked before any request to the node; Hardhat's local accounts ignore it (decision 0018). It counts as a number in the same forms: a hex quantity in either case and with any leading zeros ("0x0", "0x00", "0X2"), a decimal string, a non-negative integer or a bigint, trimmed.
- A number other than 0, 1, 2, 3 and 4, such as Celo's fee-currency type
"0x7b", zkSync's"0x71"or the OP Stack deposit type"0x7e", fails withcore.tx.unsupported-type, with or without fee fields. - When the request also sets
gasPrice,maxFeePerGasormaxPriorityFeePerGas, itstypemust be the type those fields give, or the request fails withcore.tx.type-mismatchand nothing is signed. For example,type: "0x2"withgasPrice,type: "0x0"withmaxFeePerGas, andtype: "0x4"without anauthorizationListall fail; the message says what to change, naming only the fields you set. Set the fee fields of that type, add anauthorizationListfor type 4, or removetype. type: "0x1"withgasPriceand noaccessListis signed as an EIP-2930 transaction with an empty access list, as viem'seip2930and ethers'type: 1expect.- A request with a
nullgasPrice,maxFeePerGas,maxPriorityFeePerGas,accessListorauthorizationListfails withcore.tx.null-field, which names the field, whatever itstype. Nothing is sent to the node. With the network'sgasPriceset to"auto"(the default), a lonenullmaxFeePerGasormaxPriorityFeePerGas, with nogasPriceand the other EIP-1559 field left out, is still filled like an absent field, as Hardhat does. It counts as an EIP-1559 fee field for thetypecheck, sotype: "0x0"withmaxFeePerGas: nullfails withcore.tx.type-mismatch. - A request with a
typeand no fee field is not checked: the plugin fills the fees and the fields decide, as in Hardhat. viem'stype: "legacy"without fees, which reaches the node as"0x0", therefore gets EIP-1559 fees when the node supports them. - A
typethat is not a number, such as"legacy"or"eip1559", is ignored, as in Hardhat.
For a KMS sender, the plugin asks the node's eth_estimateGas at the pending block, the state the pending nonce counts. Hardhat's local accounts, viem and ethers send no block tag, and most nodes then estimate at latest. That matters when the account has a transaction still pending, or when a load-balanced RPC answers from a node a block behind: a self-sponsored EIP-7702 authorization signed for the transaction's nonce plus one does not match the older nonce, the node skips it, and the estimate is too low. When the node refuses the pending tag, the plugin asks again without it. A backend that is itself behind can still give a low estimate, so set gas on such a transaction when the RPC balances across nodes.
A replacement is the exception. When the request's nonce is below the account's pending count, it replaces a pending transaction, and the pending state already holds the transaction it replaces. A self-sponsored authorization in the replacement would not match the account's nonce there. The plugin reads the pending count before it estimates, and estimates such a request without a block tag, as Hardhat does. On a node whose latest already includes later transactions from the account, that estimate can still be wrong, so set gas on a replacement that carries an authorization or depends on the transaction it replaces.
If automatic EIP-1559 fee estimation is unavailable for an EIP-7702 transaction, the request fails with a fee-history error. Retry or set both maxFeePerGas and maxPriorityFeePerGas explicitly.
For EIP-7702 the RPC path does not sign unsigned authorization entries. No client emits that format, and Hardhat's schema requires signed tuples. To have the KMS key sign an authorization, run kms sign-auth, with --self-broadcast when the same key also sends the transaction, and put the JSON tuple it prints in the authorizationList of a raw eth_sendTransaction, sent with provider.request, as it is. viem and ethers take other shapes; see sending the authorization. In library code, connection.kms.getAccount(address) returns a viem account whose signAuthorization signs with the KMS key (Library accounts).
An authorization's r and s may be 32-byte values or quantities, the form viem sends, which drops leading zero bytes (about one authorization in 85). For a KMS sender, the plugin sends them to the node's eth_estimateGas as quantities, the form the execution APIs specify and geth requires, and pads them to 32 bytes only to check and sign the transaction, so the signed bytes are the same either way. An r or s of 0, or of the secp256k1 curve order or more, is refused before any request to the node. Hardhat's own local accounts on an http network accept only 32-byte values.
When a KMS sender's transaction carries pre-signed tuples, the plugin lints them: each signature must be low-S, and an authority must recover from it over the EIP-7702 authorization hash, keccak256(0x05 || rlp([chainId, address, nonce])). A tuple that fails either check prints a warning that names its index in authorizationList, and the transaction is still signed and sent. A node accepts such a transaction but skips the failing authorization.
For a recoverable low-S tuple signed by the transaction sender, the plugin also checks the final transaction nonce and chain before the KMS signs. The first self-authorization needs the transaction nonce plus 1; each further valid self-authorization needs the next consecutive nonce. Its chain must be 0 or the transaction chain. A mismatch refuses the transaction. Choose an explicit transaction nonce before signing a self-authorization, since pending transactions and library reservations can raise an automatically chosen nonce. These checks also apply to kms sign-tx and the library account's signTransaction. They do not check another authority's on-chain nonce or code.