Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 94 additions & 36 deletions vocs/docs/pages/guides/signers-vs-ethereum-wallet.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,118 @@
---
description: Manage different signer types within a single EthereumWallet for flexible transaction signing strategies
title: Signers and EthereumWallet
description: Choose an Alloy signer, register it with EthereumWallet, and connect it to a provider safely
---

# Signers vs Ethereum Wallet
# Signers and `EthereumWallet`

## Signer
A signer owns or delegates access to signing material. A network wallet selects a signer for a
transaction and turns the network's unsigned transaction into a signed envelope. Keeping these
roles separate lets the same provider work with local keys, hardware devices, or remote KMS
services.

Signers implement the [`Signer` trait](https://github.com/alloy-rs/alloy/blob/main/crates/signer/src/signer.rs) which enables them to sign hashes, messages and typed data.
## Signer traits

Alloy provides access to various signers out of the box such as [`PrivateKeySigner`](https://github.com/alloy-rs/alloy/blob/a3d521e18fe335f5762be03656a3470f5f6331d8/crates/signer-local/src/lib.rs#L37), [`AwsSigner`](https://github.com/alloy-rs/alloy/blob/main/crates/signer-aws/src/signer.rs), [`LedgerSigner`](https://github.com/alloy-rs/alloy/blob/main/crates/signer-ledger/src/signer.rs) etc.
The [`Signer`](https://docs.rs/alloy-signer/latest/alloy_signer/trait.Signer.html) trait signs hashes,
messages, and EIP-712 typed data. Transaction signing also uses
[`TxSigner`](https://docs.rs/alloy-network/latest/alloy_network/trait.TxSigner.html), which exposes
the signer address and signs a network transaction in place.

These signers can directly be passed to a `Provider` using the `ProviderBuilder`. These signers are housed in the [`WalletFiller`](https://github.com/alloy-rs/alloy/blob/main/crates/provider/src/fillers/wallet.rs), which is responsible for signing transactions in the provider stack.
Most applications do not call the transaction trait directly. They put a signer in an
`EthereumWallet`, attach that wallet to `ProviderBuilder`, and let the wallet filler sign prepared
transactions.

For example:
## Available integrations

```rust
| Signer | `alloy` feature | Configuration | Runnable example |
| --- | --- | --- | --- |
| Private key | `signer-local` | key string or generated key | [Private key](/examples/wallets/private_key_signer) |
| Mnemonic | `signer-mnemonic` | phrase, derivation path, optional password | [Mnemonic](/examples/wallets/mnemonic_signer) |
| Encrypted keystore | `signer-keystore` | keystore file and password | [Create](/examples/wallets/create_keystore), [load](/examples/wallets/keystore_signer) |
| YubiHSM | `signer-yubihsm` | connector and key ID | [YubiHSM](/examples/wallets/yubi_signer) |
| AWS KMS | `signer-aws` | AWS credentials, region, key ID | [AWS](/examples/wallets/aws_signer) |
| GCP KMS | `signer-gcp` | Google credentials and KMS key version | [GCP](/examples/wallets/gcp_signer) |
| Ledger | `signer-ledger` | device transport, derivation path, chain ID | [Ledger](/examples/wallets/ledger_signer) |
| Trezor | `signer-trezor` | device transport, derivation path, chain ID | [Trezor](/examples/wallets/trezor_signer) |
| Turnkey | `signer-turnkey` | Turnkey organization, key, and API credentials | [API docs](https://docs.rs/alloy-signer-turnkey/latest/alloy_signer_turnkey/) |

let signer: PrivateKeySigner = "0x...".parse()?;
`signer-local` is part of the default `essentials` set. The other integrations are opt-in. Some
local capabilities, including mnemonics and keystores, also have their own feature because they add
dependencies or platform requirements.

let provider = ProviderBuilder::new()
.wallet(signer)
.connect_http("http://localhost:8545")?;
## Attach a signer to a provider

Read secrets from a secret manager or environment rather than source code:

```rust
use alloy::{
network::EthereumWallet,
providers::ProviderBuilder,
signers::local::PrivateKeySigner,
};

# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY")?.parse()?;
let signer_address = signer.address();
let wallet = EthereumWallet::new(signer);

let rpc_url = std::env::var("RPC_URL")?;
let provider = ProviderBuilder::new()
.wallet(wallet)
.connect(&rpc_url)
.await?;
# let _ = (provider, signer_address);
# Ok(())
# }
```

## `EthereumWallet`
`ProviderBuilder::wallet(signer)` also accepts compatible signer types directly and converts them
to the network wallet. Construct `EthereumWallet` explicitly when registering more than one signer
or when wallet behavior is part of the application boundary.

EthereumWallet is a type that can hold multiple different signers such `PrivateKeySigner`, `AwsSigner`, `LedgerSigner` etc and also be passed to the `Provider` using the `ProviderBuilder`.
## Multiple signers

The signer that instantiates `EthereumWallet` is set as the default signer. This signer is used to sign [`TransactionRequest`] and [`TypedTransaction`] objects that do not specify a signer address in the `from` field.
`EthereumWallet::new(signer)` registers the first signer as the default. Add others with
`register_signer`; use `register_default_signer` when changing the default.

For example:
The wallet filler selects a signer as follows:

```rust
let ledger_signer = LedgerSigner::new(HDPath::LedgerLive(0), Some(1)).await?;
let aws_signer = AwsSigner::new(client, key_id, Some(1)).await?;
let pk_signer: PrivateKeySigner = "0x...".parse()?;
1. If a transaction has a `from` address, use the signer registered for that address.
2. Otherwise, use the default signer and populate `from` with its address.
3. Return an error if the requested address has no registered signer.

let mut wallet = EthereumWallet::from(pk_signer) // pk_signer will be registered as the default signer.
.register_signer(aws_signer)
.register_signer(ledger_signer);
See the complete [multi-signer wallet example](/examples/wallets/ethereum_wallet), which combines a
local key, AWS KMS, and Ledger.

let provider = ProviderBuilder::new()
.wallet(wallet)
.connect_http("http://localhost:8545")?;
```
Prefer separate wallets or providers when accounts have different authorization, retry, audit, or
rate-limit policies. A large shared wallet can hide which external system will be contacted for a
given transaction.

The `PrivateKeySigner` will set to the default signer if the `from` field is not specified. One can hint the `WalletFiller` which signer to use by setting its corresponding address in the `from` field of the `TransactionRequest`.
## Chain ID and transaction intent

If you wish to change the default signer after instantiating `EthereumWallet`, you can do so by using the `register_default_signer` method.
Remote and hardware signer constructors may accept a chain ID. The provider's fillers also populate
and validate transaction fields. Configure the signer and provider for the same chain, and check the
connected chain ID at startup before allowing writes.

```rust
// `pk_signer` will be registered as the default signer
let mut wallet = EthereumWallet::from(pk_signer)
.register_signer(ledger_signer);
Always review or log safe transaction intent before asking an interactive or remote signer to sign:
sender, chain ID, destination, value, method selector, gas bounds, and nonce. Never log the private
key, mnemonic, keystore password, cloud credential, or raw secrets returned by a signing service.

// Changes the default signer to `aws_signer`
wallet.register_default_signer(aws_signer);
```
## Message and typed-data signing

Signing a message is not the same as signing a transaction. Use the signer methods directly for
messages and EIP-712 data, and verify with the matching recovery rules. See [sign and verify
message](/examples/wallets/sign_message) and [Permit hash signing](/examples/wallets/sign_permit_hash).

Enable the `eip712` feature when the selected signer integration requires typed-data support. Check
the concrete signer documentation because not every hardware or remote backend supports every
signing mode.

## Secret-handling checklist

- Keep development keys clearly separated from funded accounts.
- Use a secret manager or protected environment injection in production.
- Give cloud KMS identities only the key permissions they need.
- Pin the expected chain ID and verify it before signing.
- Apply human or policy approval for high-value operations.
- Zeroize or drop plaintext secret buffers as soon as practical.
- Never commit example credentials, even if an account is believed to be empty.
120 changes: 120 additions & 0 deletions vocs/docs/pages/guides/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
title: Testing Alloy Applications
description: Choose mock transports, local execution nodes, forks, and live RPC tests for Alloy code
---

# Testing Alloy applications

Use the smallest test environment that exercises the behavior you own. Alloy supports deterministic
mocked RPC calls, local execution clients, forked chain state, and explicit live-network tests.

| Test level | Best for | External requirements |
| --- | --- | --- |
| Mock transport | request shape, response handling, error paths | none |
| Local Anvil | transactions, contracts, mining, node controls | `anvil` binary |
| Local Geth or Reth | client-specific and operational APIs | matching client binary |
| Forked node | behavior against existing contracts and state | node binary plus upstream RPC |
| Live RPC | provider compatibility and deployment smoke tests | credentials, network, funded account if writing |

## Mock a provider

`Asserter` supplies a FIFO queue of serialized successes and JSON-RPC failures to a mock transport.
This keeps unit tests offline and makes error cases deterministic.

```rust
use alloy::{
providers::{Provider, ProviderBuilder},
transports::mock::Asserter,
};

# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let asserter = Asserter::new();
let provider = ProviderBuilder::new().connect_mocked_client(asserter.clone());

asserter.push_success(&42_u64);
assert_eq!(provider.get_block_number().await?, 42);

asserter.push_failure_msg("rate limited");
let error = provider.get_block_number().await.unwrap_err();
assert_eq!(error.as_error_resp().unwrap().message, "rate limited");
# Ok(())
# }
```

Push responses in the exact order the code will request them. A mock validates your client logic;
it does not prove that a real node accepts the request or returns the same shape. See the runnable
[mock provider example](/examples/providers/mocking).

For application code, accept `impl Provider`, a generic `P: Provider`, or a narrow trait owned by
the application. This allows a mock provider without hiding every call behind dynamic dispatch.

## Start a local Anvil provider

Enable `provider-anvil-node` to use the `ProviderBuilder` helpers. Install Foundry separately and
ensure `anvil` is on `PATH`.

```rust
use alloy::providers::{ext::AnvilApi, ProviderBuilder};

# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let provider = ProviderBuilder::new()
.connect_anvil_with_config(|anvil| anvil.chain_id(31_337).block_time(1));

let info = provider.anvil_node_info().await?;
assert_eq!(info.environment.chain_id, 31_337);
# Ok(())
# }
```

The node process is stopped when its owning handle or provider state is dropped. Use dynamically
assigned ports for parallel tests and avoid assuming a globally running node.

The [node-binding examples](/examples/node-bindings/README) also cover Anvil deployment and storage
overrides, plus local Geth and Reth processes.

## Fork existing state

Fork tests are useful for integration with deployed contracts, but they are not deterministic by
default. Read the upstream endpoint from `RPC_URL` and pin a block number whenever the test permits:

```rust
use alloy::providers::ProviderBuilder;

# fn provider() -> Result<impl alloy::providers::Provider, Box<dyn std::error::Error>> {
let rpc_url = std::env::var("RPC_URL")?;
let provider = ProviderBuilder::new().connect_anvil_with_config(|anvil| {
anvil.fork(rpc_url).fork_block_number(20_000_000_u64)
});
# Ok(provider)
# }
```

A pinned height stabilizes state but still depends on upstream availability and archive access. Use
a fixture or mock for unit tests, and reserve forks for cases where real bytecode and storage are
the point of the test.

## Client-specific tests

Use `alloy-node-bindings` to start Geth, Reth, or Anvil when testing an extension namespace or
client-specific behavior. The binding does not install the binary. Pin the client version in CI and
skip with an explicit message when a developer has not installed an optional binary.

Do not treat one client as proof of compatibility with every other client. Run a small matrix for
the JSON-RPC methods your library promises to support.

## Live-network tests

Keep live RPC tests outside the default offline suite. Require explicit environment variables,
avoid shared public endpoints, and fail early with an actionable message when configuration is
missing. Read-only smoke tests should assert stable invariants; write tests should use a dedicated
ephemeral account and bounded funds.

## CI recommendations

- Run mock tests and `cargo check --all-targets` on every change.
- Run deterministic local-node tests when the required binary is provisioned in CI.
- Pin fork block numbers, node versions, dependency versions, and the Rust toolchain.
- Serialize tests that mutate one shared node, or give each test its own node and ports.
- Set timeouts so a missing block, device, or endpoint does not hang the job.
- Mark tests that need credentials, hardware, or paid services and run them in protected jobs.
- Never expose RPC tokens, private keys, or cloud credentials in test output.
2 changes: 1 addition & 1 deletion vocs/docs/pages/introduction/choose-a-path.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ then to runnable code where one exists.
| Choose a signer or wallet | [Signers vs. Ethereum wallet](/guides/signers-vs-ethereum-wallet) | [Wallets and signers](/examples/wallets/README) |
| Work with more than one chain or custom network types | [Interacting with multiple networks](/guides/interacting-with-multiple-networks) | [Any network](/examples/advanced/any_network) |
| Add retries, rate limits, fallbacks, or observability | [Transport layers](/guides/layers) | [Layers](/examples/layers/README) |
| Run against Anvil, Geth, or Reth locally | [How Alloy fits together](/introduction/architecture#local-nodes-and-tests) | [Node bindings](/examples/node-bindings/README) |
| Test with mocks, Anvil, Geth, Reth, or a fork | [Testing Alloy applications](/guides/testing) | [Mock provider](/examples/providers/mocking) and [node bindings](/examples/node-bindings/README) |
| Decode ABI data, logs, or transaction input | [Using `sol!`](/contract-interactions/using-sol!) | [`sol!` examples](/examples/sol-macro/README) and [transaction decoding](/examples/transactions/decode_input) |
| Select a smaller dependency set | [Feature flags](/reference/feature-flags) | [Alloy features on docs.rs](https://docs.rs/crate/alloy/latest/features) |
| Migrate from ethers-rs | [Migration reference](/migrating-from-ethers/reference) | [Conversions](/migrating-from-ethers/conversions) |
Expand Down
3 changes: 2 additions & 1 deletion vocs/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export const sidebar: Sidebar = [
{ text: 'Performant ABI Encoding', link: '/guides/static-dynamic-abi-in-alloy' },
{ text: 'Multicall', link: '/guides/multicall' },
{ text: 'Interacting with multiple networks', link: '/guides/interacting-with-multiple-networks' },
{ text: 'Signers vs Ethereum Wallet', link: '/guides/signers-vs-ethereum-wallet' },
{ text: 'Signers and EthereumWallet', link: '/guides/signers-vs-ethereum-wallet' },
{ text: 'Testing Alloy Applications', link: '/guides/testing' },
{ text: 'RPC provider abstractions', link: '/guides/rpc-provider-abstraction' },
{ text: 'High-Priority Transaction Queue with Fillers', link: '/guides/fillers' },
{ text: 'Overriding Transport behavior with Layers', link: '/guides/layers' },
Expand Down
Loading