diff --git a/vocs/docs/pages/guides/signers-vs-ethereum-wallet.md b/vocs/docs/pages/guides/signers-vs-ethereum-wallet.md index b037306..ed2a051 100644 --- a/vocs/docs/pages/guides/signers-vs-ethereum-wallet.md +++ b/vocs/docs/pages/guides/signers-vs-ethereum-wallet.md @@ -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> { +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. diff --git a/vocs/docs/pages/guides/testing.md b/vocs/docs/pages/guides/testing.md new file mode 100644 index 0000000..bcbae97 --- /dev/null +++ b/vocs/docs/pages/guides/testing.md @@ -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> { +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> { +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> { +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. diff --git a/vocs/docs/pages/introduction/choose-a-path.md b/vocs/docs/pages/introduction/choose-a-path.md index 60ccdfd..63de1a1 100644 --- a/vocs/docs/pages/introduction/choose-a-path.md +++ b/vocs/docs/pages/introduction/choose-a-path.md @@ -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) | diff --git a/vocs/sidebar.ts b/vocs/sidebar.ts index bbd3c75..b04fb56 100644 --- a/vocs/sidebar.ts +++ b/vocs/sidebar.ts @@ -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' },