diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8e64e23dd9..a5d3f3a536d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,18 @@ jobs: if: ${{ matrix.rust != '1.75' }} # MSRV run: cargo nextest run --workspace ${{ matrix.flags }} + doctest: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - run: cargo test --workspace --doc + - run: cargo test --all-features --workspace --doc + wasm: runs-on: ubuntu-latest timeout-minutes: 30 @@ -68,6 +80,7 @@ jobs: run: | cargo hack check --workspace --target wasm32-unknown-unknown \ --exclude alloy-contract \ + --exclude alloy-network \ --exclude alloy-node-bindings \ --exclude alloy-providers \ --exclude alloy-signer \ diff --git a/Cargo.toml b/Cargo.toml index 8c766579c3b..02b391eb1fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,10 +41,18 @@ alloy-transport-ipc = { version = "0.1.0", default-features = false, path = "cra alloy-transport-ws = { version = "0.1.0", default-features = false, path = "crates/transport-ws" } alloy-core = { version = "0.6.4", default-features = false, features = ["std"] } -alloy-dyn-abi = { version = "0.6.4", default-features = false, features = ["std"] } -alloy-json-abi = { version = "0.6.4", default-features = false, features = ["std"] } -alloy-primitives = { version = "0.6.4", default-features = false, features = ["std"] } -alloy-sol-types = { version = "0.6.4", default-features = false, features = ["std"] } +alloy-dyn-abi = { version = "0.6.4", default-features = false, features = [ + "std", +] } +alloy-json-abi = { version = "0.6.4", default-features = false, features = [ + "std", +] } +alloy-primitives = { version = "0.6.4", default-features = false, features = [ + "std", +] } +alloy-sol-types = { version = "0.6.4", default-features = false, features = [ + "std", +] } alloy-rlp = "0.3" @@ -53,8 +61,13 @@ ethereum_ssz_derive = "0.5" ethereum_ssz = "0.5" # crypto -elliptic-curve = { version = "0.13", default-features = false, features = ["std"] } -k256 = { version = "0.13", default-features = false, features = ["ecdsa", "std"] } +elliptic-curve = { version = "0.13", default-features = false, features = [ + "std", +] } +k256 = { version = "0.13", default-features = false, features = [ + "ecdsa", + "std", +] } sha2 = { version = "0.10", default-features = false, features = ["std"] } spki = { version = "0.7", default-features = false, features = ["std"] } @@ -102,5 +115,5 @@ tempfile = "3.10" # TODO: Keep around until alloy-contract is stable. # This should only be used in `alloy-contract` tests. -# [patch.crates-io] -# alloy-sol-macro = { git = "https://github.com/alloy-rs/core", rev = "18b0509950c90d9ec38f25913b692ae4cdd6f227" } +[patch.crates-io] +alloy-sol-macro = { git = "https://github.com/alloy-rs/core", rev = "ab0cab1047a088be6cffa4e7a2fcde7cf77aa460" } diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml index 20532ad64cd..7d79f8e526f 100644 --- a/crates/consensus/Cargo.toml +++ b/crates/consensus/Cargo.toml @@ -12,7 +12,6 @@ repository.workspace = true exclude.workspace = true [dependencies] -alloy-network.workspace = true alloy-primitives = { workspace = true, features = ["rlp"] } alloy-rlp.workspace = true alloy-eips.workspace = true @@ -24,10 +23,13 @@ sha2 = { version = "0.10", optional = true } arbitrary = { workspace = true, features = ["derive"], optional = true } [dev-dependencies] +alloy-signer.workspace = true # arbitrary arbitrary = { workspace = true, features = ["derive"] } +k256.workspace = true +tokio = { workspace = true, features = ["macros"] } [features] -k256 = ["alloy-primitives/k256", "alloy-network/k256"] +k256 = ["alloy-primitives/k256"] kzg = ["dep:c-kzg", "dep:sha2", "dep:thiserror"] arbitrary = ["dep:arbitrary", "alloy-eips/arbitrary"] diff --git a/crates/consensus/README.md b/crates/consensus/README.md index e16ce91961f..ff033c238ca 100644 --- a/crates/consensus/README.md +++ b/crates/consensus/README.md @@ -4,8 +4,7 @@ Ethereum consensus interface. This crate contains constants, types, and functions for implementing Ethereum EL consensus and communication. This includes headers, blocks, transactions, -eip2718 envelopes, eip2930, eip4844, and more. The types in this crate -implement many of the traits found in [alloy_network]. +[EIP-2718] envelopes, [EIP-2930], [EIP-4844], and more. In general a type belongs in this crate if it is committed to in the EL block header. This includes: @@ -18,6 +17,8 @@ header. This includes: [alloy-network]: ../network [EIP-2718]: https://eips.ethereum.org/EIPS/eip-2718 +[EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 +[EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844 ## Provenance diff --git a/crates/consensus/src/header.rs b/crates/consensus/src/header.rs index 8a0c5561bdf..ea316c53255 100644 --- a/crates/consensus/src/header.rs +++ b/crates/consensus/src/header.rs @@ -1,8 +1,8 @@ +use crate::Sealable; use alloy_eips::{ eip1559::{calc_next_block_base_fee, BaseFeeParams}, eip4844::{calc_blob_gasprice, calc_excess_blob_gas}, }; -use alloy_network::Sealable; use alloy_primitives::{b256, keccak256, Address, BlockNumber, Bloom, Bytes, B256, B64, U256}; use alloy_rlp::{ length_of_length, Buf, BufMut, Decodable, Encodable, EMPTY_LIST_CODE, EMPTY_STRING_CODE, diff --git a/crates/consensus/src/lib.rs b/crates/consensus/src/lib.rs index 6b253d00dd5..edebfddd184 100644 --- a/crates/consensus/src/lib.rs +++ b/crates/consensus/src/lib.rs @@ -21,15 +21,19 @@ mod header; pub use header::{Header, EMPTY_OMMER_ROOT_HASH, EMPTY_ROOT_HASH}; mod receipt; -pub use receipt::{Receipt, ReceiptEnvelope, ReceiptWithBloom}; +pub use receipt::{Receipt, ReceiptEnvelope, ReceiptWithBloom, TxReceipt}; mod transaction; pub use transaction::{ - BlobTransactionSidecar, TxEip1559, TxEip2930, TxEip4844, TxEip4844Variant, - TxEip4844WithSidecar, TxEnvelope, TxLegacy, TxType, + BlobTransactionSidecar, SignableTransaction, Transaction, TxEip1559, TxEip2930, TxEip4844, + TxEip4844Variant, TxEip4844WithSidecar, TxEnvelope, TxLegacy, TxType, TypedTransaction, }; #[cfg(feature = "kzg")] pub use transaction::BlobTransactionValidationError; -pub use alloy_network::TxKind; +mod sealed; +pub use sealed::{Sealable, Sealed}; + +mod signed; +pub use signed::Signed; diff --git a/crates/consensus/src/receipt/mod.rs b/crates/consensus/src/receipt/mod.rs index a1e4de4dcfa..c2fca3d0766 100644 --- a/crates/consensus/src/receipt/mod.rs +++ b/crates/consensus/src/receipt/mod.rs @@ -1,14 +1,38 @@ +use alloy_primitives::{Bloom, Log}; + mod envelope; pub use envelope::ReceiptEnvelope; mod receipts; pub use receipts::{Receipt, ReceiptWithBloom}; +/// Receipt is the result of a transaction execution. +pub trait TxReceipt { + /// Returns true if the transaction was successful. + fn success(&self) -> bool; + + /// Returns the bloom filter for the logs in the receipt. This operation + /// may be expensive. + fn bloom(&self) -> Bloom; + + /// Returns the bloom filter for the logs in the receipt, if it is cheap to + /// compute. + fn bloom_cheap(&self) -> Option { + None + } + + /// Returns the cumulative gas used in the block after this transaction was executed. + fn cumulative_gas_used(&self) -> u64; + + /// Returns the logs emitted by this transaction. + fn logs(&self) -> &[Log]; +} + #[cfg(test)] mod tests { use super::*; use alloy_eips::eip2718::Encodable2718; - use alloy_primitives::{address, b256, bytes, hex, Bytes, Log, LogData}; + use alloy_primitives::{address, b256, bytes, hex, Bytes, LogData}; use alloy_rlp::{Decodable, Encodable}; // Test vector from: https://eips.ethereum.org/EIPS/eip-2481 diff --git a/crates/consensus/src/receipt/receipts.rs b/crates/consensus/src/receipt/receipts.rs index 5e9022cb9cc..ed7e5b909f2 100644 --- a/crates/consensus/src/receipt/receipts.rs +++ b/crates/consensus/src/receipt/receipts.rs @@ -1,3 +1,4 @@ +use super::TxReceipt; use alloy_primitives::{Bloom, Log}; use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable}; @@ -28,7 +29,7 @@ impl Receipt { } } -impl alloy_network::Receipt for Receipt { +impl TxReceipt for Receipt { fn success(&self) -> bool { self.success } @@ -51,7 +52,7 @@ impl alloy_network::Receipt for Receipt { /// This convenience type allows us to lazily calculate the bloom filter for a /// receipt, similar to [`Sealed`]. /// -/// [`Sealed`]: ::alloy_network::Sealed +/// [`Sealed`]: crate::sealed::Sealed #[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct ReceiptWithBloom { /// The receipt. @@ -60,6 +61,28 @@ pub struct ReceiptWithBloom { pub bloom: Bloom, } +impl TxReceipt for ReceiptWithBloom { + fn success(&self) -> bool { + self.receipt.success + } + + fn bloom(&self) -> Bloom { + self.bloom + } + + fn bloom_cheap(&self) -> Option { + Some(self.bloom) + } + + fn cumulative_gas_used(&self) -> u64 { + self.receipt.cumulative_gas_used + } + + fn logs(&self) -> &[Log] { + &self.receipt.logs + } +} + impl From for ReceiptWithBloom { fn from(receipt: Receipt) -> Self { let bloom = receipt.bloom_slow(); @@ -135,28 +158,6 @@ impl ReceiptWithBloom { } } -impl alloy_network::Receipt for ReceiptWithBloom { - fn success(&self) -> bool { - self.receipt.success - } - - fn bloom(&self) -> Bloom { - self.bloom - } - - fn bloom_cheap(&self) -> Option { - Some(self.bloom) - } - - fn cumulative_gas_used(&self) -> u64 { - self.receipt.cumulative_gas_used - } - - fn logs(&self) -> &[Log] { - &self.receipt.logs - } -} - impl alloy_rlp::Encodable for ReceiptWithBloom { fn encode(&self, out: &mut dyn BufMut) { self.encode_fields(out); diff --git a/crates/network/src/sealed.rs b/crates/consensus/src/sealed.rs similarity index 100% rename from crates/network/src/sealed.rs rename to crates/consensus/src/sealed.rs diff --git a/crates/network/src/transaction/signed.rs b/crates/consensus/src/signed.rs similarity index 80% rename from crates/network/src/transaction/signed.rs rename to crates/consensus/src/signed.rs index 92cecd43150..390d14a3a50 100644 --- a/crates/network/src/transaction/signed.rs +++ b/crates/consensus/src/signed.rs @@ -1,4 +1,4 @@ -use crate::Transaction; +use crate::transaction::SignableTransaction; use alloy_primitives::{Signature, B256}; use alloy_rlp::BufMut; @@ -35,9 +35,9 @@ impl Signed { } } -impl Signed { +impl, Sig> Signed { /// Instantiate from a transaction and signature. Does not verify the signature. - pub const fn new_unchecked(tx: T, signature: Signature, hash: B256) -> Self { + pub const fn new_unchecked(tx: T, signature: Sig, hash: B256) -> Self { Self { tx, signature, hash } } @@ -59,7 +59,7 @@ impl Signed { } } -impl alloy_rlp::Encodable for Signed { +impl, Sig> alloy_rlp::Encodable for Signed { fn encode(&self, out: &mut dyn BufMut) { self.tx.encode_signed(&self.signature, out) } @@ -67,14 +67,14 @@ impl alloy_rlp::Encodable for Signed { // TODO: impl length } -impl alloy_rlp::Decodable for Signed { +impl, Sig> alloy_rlp::Decodable for Signed { fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { - T::decode_signed(buf) + >::decode_signed(buf) } } #[cfg(feature = "k256")] -impl Signed { +impl> Signed { /// Recover the signer of the transaction pub fn recover_signer( &self, diff --git a/crates/consensus/src/transaction/eip1559.rs b/crates/consensus/src/transaction/eip1559.rs index ea2ae383ac2..9aae947b249 100644 --- a/crates/consensus/src/transaction/eip1559.rs +++ b/crates/consensus/src/transaction/eip1559.rs @@ -1,7 +1,6 @@ -use crate::{TxKind, TxType}; +use crate::{SignableTransaction, Signed, Transaction, TxType}; use alloy_eips::eip2930::AccessList; -use alloy_network::{Signed, Transaction}; -use alloy_primitives::{keccak256, Bytes, ChainId, Signature, U256}; +use alloy_primitives::{keccak256, Bytes, ChainId, Signature, TxKind, U256}; use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header}; use std::mem; @@ -182,33 +181,40 @@ impl TxEip1559 { } } -impl Encodable for TxEip1559 { - fn encode(&self, out: &mut dyn BufMut) { - Header { list: true, payload_length: self.fields_len() }.encode(out); - self.encode_fields(out); +impl Transaction for TxEip1559 { + fn input(&self) -> &[u8] { + &self.input } - fn length(&self) -> usize { - let payload_length = self.fields_len(); - length_of_length(payload_length) + payload_length + fn to(&self) -> TxKind { + self.to } -} -impl Decodable for TxEip1559 { - fn decode(data: &mut &[u8]) -> alloy_rlp::Result { - let header = Header::decode(data)?; - let remaining_len = data.len(); + fn value(&self) -> U256 { + self.value + } - if header.payload_length > remaining_len { - return Err(alloy_rlp::Error::InputTooShort); - } + fn chain_id(&self) -> Option { + Some(self.chain_id) + } - Self::decode_inner(data) + fn nonce(&self) -> u64 { + self.nonce + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn gas_price(&self) -> Option { + None } } -impl Transaction for TxEip1559 { - type Signature = Signature; +impl SignableTransaction for TxEip1559 { + fn set_chain_id(&mut self, chain_id: ChainId) { + self.chain_id = chain_id; + } fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { out.put_u8(self.tx_type() as u8); @@ -239,7 +245,7 @@ impl Transaction for TxEip1559 { TxEip1559::encode_with_signature(self, signature, out) } - fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { let header = Header::decode(buf)?; if !header.list { return Err(alloy_rlp::Error::UnexpectedString); @@ -250,75 +256,39 @@ impl Transaction for TxEip1559 { Ok(tx.into_signed(signature)) } +} - fn input(&self) -> &[u8] { - &self.input - } - - fn input_mut(&mut self) -> &mut Bytes { - &mut self.input - } - - fn set_input(&mut self, input: Bytes) { - self.input = input; - } - - fn to(&self) -> TxKind { - self.to - } - - fn set_to(&mut self, to: TxKind) { - self.to = to; - } - - fn value(&self) -> U256 { - self.value - } - - fn set_value(&mut self, value: U256) { - self.value = value; - } - - fn chain_id(&self) -> Option { - Some(self.chain_id) - } - - fn set_chain_id(&mut self, chain_id: ChainId) { - self.chain_id = chain_id; - } - - fn nonce(&self) -> u64 { - self.nonce - } - - fn set_nonce(&mut self, nonce: u64) { - self.nonce = nonce; +impl Encodable for TxEip1559 { + fn encode(&self, out: &mut dyn BufMut) { + Header { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); } - fn gas_limit(&self) -> u64 { - self.gas_limit + fn length(&self) -> usize { + let payload_length = self.fields_len(); + length_of_length(payload_length) + payload_length } +} - fn set_gas_limit(&mut self, limit: u64) { - self.gas_limit = limit; - } +impl Decodable for TxEip1559 { + fn decode(data: &mut &[u8]) -> alloy_rlp::Result { + let header = Header::decode(data)?; + let remaining_len = data.len(); - fn gas_price(&self) -> Option { - None - } + if header.payload_length > remaining_len { + return Err(alloy_rlp::Error::InputTooShort); + } - fn set_gas_price(&mut self, price: U256) { - let _ = price; + Self::decode_inner(data) } } #[cfg(all(test, feature = "k256"))] mod tests { use super::TxEip1559; - use crate::TxKind; + use crate::SignableTransaction; use alloy_eips::eip2930::AccessList; - use alloy_network::Transaction; - use alloy_primitives::{address, b256, hex, Address, Signature, B256, U256}; + use alloy_primitives::{address, b256, hex, Address, Signature, TxKind, B256, U256}; use alloy_rlp::Encodable; #[test] diff --git a/crates/consensus/src/transaction/eip2930.rs b/crates/consensus/src/transaction/eip2930.rs index 410b187af5a..92a4675596a 100644 --- a/crates/consensus/src/transaction/eip2930.rs +++ b/crates/consensus/src/transaction/eip2930.rs @@ -1,7 +1,6 @@ -use crate::{TxKind, TxType}; +use crate::{SignableTransaction, Signed, Transaction, TxType}; use alloy_eips::eip2930::AccessList; -use alloy_network::{Signed, Transaction}; -use alloy_primitives::{keccak256, Bytes, ChainId, Signature, U256}; +use alloy_primitives::{keccak256, Bytes, ChainId, Signature, TxKind, U256}; use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header}; use std::mem; @@ -143,34 +142,40 @@ impl TxEip2930 { } } -impl Encodable for TxEip2930 { - fn encode(&self, out: &mut dyn BufMut) { - Header { list: true, payload_length: self.fields_len() }.encode(out); - self.encode_fields(out); +impl Transaction for TxEip2930 { + fn input(&self) -> &[u8] { + &self.input } - fn length(&self) -> usize { - let payload_length = self.fields_len(); - length_of_length(payload_length) + payload_length + fn to(&self) -> TxKind { + self.to } -} -impl Decodable for TxEip2930 { - fn decode(data: &mut &[u8]) -> alloy_rlp::Result { - let header = Header::decode(data)?; - let remaining_len = data.len(); + fn value(&self) -> U256 { + self.value + } - if header.payload_length > remaining_len { - return Err(alloy_rlp::Error::InputTooShort); - } + fn chain_id(&self) -> Option { + Some(self.chain_id) + } - Self::decode_inner(data) + fn nonce(&self) -> u64 { + self.nonce + } + + fn gas_limit(&self) -> u64 { + self.gas_limit + } + + fn gas_price(&self) -> Option { + Some(U256::from(self.gas_price)) } } -impl Transaction for TxEip2930 { - type Signature = Signature; - // type Receipt = ReceiptWithBloom; +impl SignableTransaction for TxEip2930 { + fn set_chain_id(&mut self, chain_id: ChainId) { + self.chain_id = chain_id; + } fn encode_for_signing(&self, out: &mut dyn BufMut) { out.put_u8(self.tx_type() as u8); @@ -201,7 +206,7 @@ impl Transaction for TxEip2930 { self.encode_with_signature(signature, out) } - fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { let header = Header::decode(buf)?; if !header.list { return Err(alloy_rlp::Error::UnexpectedString); @@ -212,76 +217,38 @@ impl Transaction for TxEip2930 { Ok(tx.into_signed(signature)) } +} - fn input(&self) -> &[u8] { - &self.input - } - - fn input_mut(&mut self) -> &mut Bytes { - &mut self.input - } - - fn set_input(&mut self, input: Bytes) { - self.input = input; - } - - fn to(&self) -> TxKind { - self.to - } - - fn set_to(&mut self, to: TxKind) { - self.to = to; - } - - fn value(&self) -> U256 { - self.value - } - - fn set_value(&mut self, value: U256) { - self.value = value; - } - - fn chain_id(&self) -> Option { - Some(self.chain_id) - } - - fn set_chain_id(&mut self, chain_id: ChainId) { - self.chain_id = chain_id; - } - - fn nonce(&self) -> u64 { - self.nonce - } - - fn set_nonce(&mut self, nonce: u64) { - self.nonce = nonce; - } - - fn gas_limit(&self) -> u64 { - self.gas_limit +impl Encodable for TxEip2930 { + fn encode(&self, out: &mut dyn BufMut) { + Header { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); } - fn set_gas_limit(&mut self, limit: u64) { - self.gas_limit = limit; + fn length(&self) -> usize { + let payload_length = self.fields_len(); + length_of_length(payload_length) + payload_length } +} - fn gas_price(&self) -> Option { - Some(U256::from(self.gas_price)) - } +impl Decodable for TxEip2930 { + fn decode(data: &mut &[u8]) -> alloy_rlp::Result { + let header = Header::decode(data)?; + let remaining_len = data.len(); - fn set_gas_price(&mut self, price: U256) { - if let Ok(price) = price.try_into() { - self.gas_price = price; + if header.payload_length > remaining_len { + return Err(alloy_rlp::Error::InputTooShort); } + + Self::decode_inner(data) } } #[cfg(test)] mod tests { use super::TxEip2930; - use crate::{TxEnvelope, TxKind}; - use alloy_network::{Signed, Transaction}; - use alloy_primitives::{Address, Bytes, Signature, U256}; + use crate::{SignableTransaction, Signed, TxEnvelope}; + use alloy_primitives::{Address, Bytes, Signature, TxKind, U256}; use alloy_rlp::{Decodable, Encodable}; #[test] diff --git a/crates/consensus/src/transaction/eip4844.rs b/crates/consensus/src/transaction/eip4844.rs index 93ffef3c118..ad2de87819c 100644 --- a/crates/consensus/src/transaction/eip4844.rs +++ b/crates/consensus/src/transaction/eip4844.rs @@ -1,10 +1,9 @@ -use crate::{TxKind, TxType}; +use crate::{SignableTransaction, Signed, Transaction, TxType}; use alloy_eips::{ eip2930::AccessList, eip4844::{BYTES_PER_BLOB, BYTES_PER_COMMITMENT, BYTES_PER_PROOF, DATA_GAS_PER_BLOB}, }; -use alloy_network::{Signed, Transaction}; -use alloy_primitives::{keccak256, Bytes, ChainId, Signature, B256, U256}; +use alloy_primitives::{keccak256, Bytes, ChainId, Signature, TxKind, B256, U256}; use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header}; use std::mem; @@ -153,7 +152,64 @@ impl TxEip4844Variant { } impl Transaction for TxEip4844Variant { - type Signature = Signature; + fn chain_id(&self) -> Option { + match self { + TxEip4844Variant::TxEip4844(tx) => Some(tx.chain_id), + TxEip4844Variant::TxEip4844WithSidecar(tx) => Some(tx.tx().chain_id), + } + } + + fn gas_limit(&self) -> u64 { + match self { + TxEip4844Variant::TxEip4844(tx) => tx.gas_limit, + TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx().gas_limit, + } + } + + fn gas_price(&self) -> Option { + None + } + + fn input(&self) -> &[u8] { + match self { + TxEip4844Variant::TxEip4844(tx) => tx.input.as_ref(), + TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx().input.as_ref(), + } + } + + fn nonce(&self) -> u64 { + match self { + TxEip4844Variant::TxEip4844(tx) => tx.nonce, + TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx().nonce, + } + } + + fn to(&self) -> TxKind { + match self { + TxEip4844Variant::TxEip4844(tx) => tx.to, + TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx.to, + } + } + + fn value(&self) -> U256 { + match self { + TxEip4844Variant::TxEip4844(tx) => tx.value, + TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx.value, + } + } +} + +impl SignableTransaction for TxEip4844Variant { + fn set_chain_id(&mut self, chain_id: ChainId) { + match self { + TxEip4844Variant::TxEip4844(ref mut inner) => { + inner.chain_id = chain_id; + } + TxEip4844Variant::TxEip4844WithSidecar(ref mut inner) => { + inner.tx.chain_id = chain_id; + } + } + } fn payload_len_for_signature(&self) -> usize { let payload_length = self.fields_len(); @@ -213,105 +269,6 @@ impl Transaction for TxEip4844Variant { fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) { Self::encode_with_signature(self, signature, out, true); } - - fn chain_id(&self) -> Option { - match self { - TxEip4844Variant::TxEip4844(tx) => Some(tx.chain_id), - TxEip4844Variant::TxEip4844WithSidecar(tx) => Some(tx.tx().chain_id), - } - } - - fn gas_limit(&self) -> u64 { - match self { - TxEip4844Variant::TxEip4844(tx) => tx.gas_limit, - TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx().gas_limit, - } - } - - fn gas_price(&self) -> Option { - None - } - - fn set_chain_id(&mut self, chain_id: ChainId) { - match self { - TxEip4844Variant::TxEip4844(tx) => tx.chain_id = chain_id, - TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx.chain_id = chain_id, - } - } - - fn input(&self) -> &[u8] { - match self { - TxEip4844Variant::TxEip4844(tx) => tx.input.as_ref(), - TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx().input.as_ref(), - } - } - - fn input_mut(&mut self) -> &mut Bytes { - match self { - TxEip4844Variant::TxEip4844(tx) => &mut tx.input, - TxEip4844Variant::TxEip4844WithSidecar(tx) => &mut tx.tx.input, - } - } - - fn nonce(&self) -> u64 { - match self { - TxEip4844Variant::TxEip4844(tx) => tx.nonce, - TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx().nonce, - } - } - - fn set_gas_limit(&mut self, limit: u64) { - match self { - TxEip4844Variant::TxEip4844(tx) => tx.gas_limit = limit, - TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx.gas_limit = limit, - } - } - - fn set_gas_price(&mut self, price: U256) { - let _ = price; - } - - fn set_input(&mut self, data: Bytes) { - match self { - TxEip4844Variant::TxEip4844(tx) => tx.input = data, - TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx.input = data, - } - } - - fn set_nonce(&mut self, nonce: u64) { - match self { - TxEip4844Variant::TxEip4844(tx) => tx.nonce = nonce, - TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx.nonce = nonce, - } - } - - fn set_to(&mut self, to: TxKind) { - match self { - TxEip4844Variant::TxEip4844(tx) => tx.to = to, - TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx.to = to, - } - } - - fn set_value(&mut self, value: U256) { - match self { - TxEip4844Variant::TxEip4844(tx) => tx.value = value, - TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx.value = value, - } - } - - fn to(&self) -> TxKind { - match self { - TxEip4844Variant::TxEip4844(tx) => tx.to, - TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx.to, - } - } - - fn value(&self) -> U256 { - match self { - TxEip4844Variant::TxEip4844(tx) => tx.value, - TxEip4844Variant::TxEip4844WithSidecar(tx) => tx.tx.value, - } - } } /// [EIP-4844 Blob Transaction](https://eips.ethereum.org/EIPS/eip-4844#blob-transaction) @@ -546,29 +503,6 @@ impl TxEip4844 { mem::size_of::() // max_fee_per_data_gas } - /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating - /// hash that for eip2718 does not require rlp header - pub(crate) fn encode_with_signature( - &self, - signature: &Signature, - out: &mut dyn BufMut, - with_header: bool, - ) { - let payload_length = self.fields_len() + signature.rlp_vrs_len(); - if with_header { - Header { - list: false, - payload_length: 1 + length_of_length(payload_length) + payload_length, - } - .encode(out); - } - out.put_u8(self.tx_type() as u8); - let header = Header { list: true, payload_length }; - header.encode(out); - self.encode_fields(out); - signature.encode(out); - } - /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. pub fn payload_len_with_signature(&self, signature: &Signature) -> usize { let len = self.payload_len_with_signature_without_header(signature); @@ -587,7 +521,7 @@ impl TxEip4844 { TxType::Eip4844 } - /// Encodes the 4844 transaction in RLP for signing. + /// Encodes the EIP-4844 transaction in RLP for signing. /// /// This encodes the transaction as: /// `tx_type || rlp(chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, @@ -609,105 +543,57 @@ impl TxEip4844 { } impl Transaction for TxEip4844 { - type Signature = Signature; - - fn chain_id(&self) -> Option { - Some(self.chain_id) - } - - fn payload_len_for_signature(&self) -> usize { - let payload_length = self.fields_len(); - // 'transaction type byte length' + 'header length' + 'payload length' - 1 + length_of_length(payload_length) + payload_length - } - - fn into_signed(self, signature: Signature) -> Signed { - let payload_length = 1 + self.fields_len() + signature.rlp_vrs_len(); - let mut buf = Vec::with_capacity(payload_length); - buf.put_u8(TxType::Eip4844 as u8); - self.encode_signed(&signature, &mut buf); - let hash = keccak256(&buf); - - // Drop any v chain id value to ensure the signature format is correct at the time of - // combination for an EIP-4844 transaction. V should indicate the y-parity of the - // signature. - Signed::new_unchecked(self, signature.with_parity_bool(), hash) - } - - fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { - let header = Header::decode(buf)?; - if !header.list { - return Err(alloy_rlp::Error::UnexpectedString); - } - - let tx = Self::decode_inner(buf)?; - let signature = Signature::decode_rlp_vrs(buf)?; - - Ok(tx.into_signed(signature)) - } - - fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { - self.encode_for_signing(out); - } - - fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) { - TxEip4844::encode_with_signature(self, signature, out, true); - } - fn input(&self) -> &[u8] { &self.input } - fn input_mut(&mut self) -> &mut Bytes { - &mut self.input - } - - fn set_input(&mut self, input: Bytes) { - self.input = input; - } - fn to(&self) -> TxKind { self.to } - fn set_to(&mut self, to: TxKind) { - self.to = to; - } - fn value(&self) -> U256 { self.value } - fn set_value(&mut self, value: U256) { - self.value = value; - } - - fn set_chain_id(&mut self, chain_id: ChainId) { - self.chain_id = chain_id; + fn chain_id(&self) -> Option { + Some(self.chain_id) } fn nonce(&self) -> u64 { self.nonce } - fn set_nonce(&mut self, nonce: u64) { - self.nonce = nonce; - } - fn gas_limit(&self) -> u64 { self.gas_limit } - fn set_gas_limit(&mut self, limit: u64) { - self.gas_limit = limit; - } - fn gas_price(&self) -> Option { None } +} + +impl Encodable for TxEip4844 { + fn encode(&self, out: &mut dyn BufMut) { + Header { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); + } + + fn length(&self) -> usize { + let payload_length = self.fields_len(); + length_of_length(payload_length) + payload_length + } +} + +impl Decodable for TxEip4844 { + fn decode(data: &mut &[u8]) -> alloy_rlp::Result { + let header = Header::decode(data)?; + let remaining_len = data.len(); - fn set_gas_price(&mut self, price: U256) { - let _ = price; + if header.payload_length > remaining_len { + return Err(alloy_rlp::Error::InputTooShort); + } + + Self::decode_inner(data) } } @@ -775,83 +661,9 @@ impl TxEip4844WithSidecar { pub fn into_parts(self) -> (TxEip4844, BlobTransactionSidecar) { (self.tx, self.sidecar) } - - /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating - /// hash that for eip2718 does not require rlp header - pub(crate) fn encode_with_signature( - &self, - signature: &Signature, - out: &mut dyn BufMut, - with_header: bool, - ) { - let payload_length = self.tx.fields_len() + signature.rlp_vrs_len(); - if with_header { - Header { - list: false, - payload_length: 1 + length_of_length(payload_length) + payload_length, - } - .encode(out); - } - out.put_u8(self.tx.tx_type() as u8); - let header = Header { list: true, payload_length }; - header.encode(out); - self.tx.encode_fields(out); - signature.encode(out); - self.sidecar.encode_inner(out); - } } impl Transaction for TxEip4844WithSidecar { - type Signature = Signature; - - fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> { - let header = Header::decode(buf)?; - if !header.list { - return Err(alloy_rlp::Error::UnexpectedString); - } - - let tx = TxEip4844::decode_inner(buf)?; - let signature = Signature::decode_rlp_vrs(buf)?; - let sidecar = BlobTransactionSidecar::decode_inner(buf).unwrap_or_default(); - - Ok(Self::from_tx_and_sidecar(tx, sidecar).into_signed(signature)) - } - - fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { - // A signature for a [TxEip4844WithSidecar] is a signature over the [TxEip4844] EIP-2718 - // payload fields: - // (BLOB_TX_TYPE || - // rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to, value, - // data, access_list, max_fee_per_blob_gas, blob_versioned_hashes])) - self.tx.encode_for_signing(out); - } - - fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut) { - self.encode_with_signature(signature, out, true) - } - - fn into_signed(self, signature: Signature) -> Signed { - let payload_length = 1 + self.tx.fields_len() + signature.rlp_vrs_len(); - let mut buf = Vec::with_capacity(payload_length); - // The sidecar is NOT included in the signed payload, only the transaction fields and the - // type byte. Include the type byte. - buf.put_u8(TxType::Eip4844 as u8); - // Include the transaction fields. - self.tx.encode_signed(&signature, &mut buf); - let hash = keccak256(&buf); - - // Drop any v chain id value to ensure the signature format is correct at the time of - // combination for an EIP-4844 transaction. V should indicate the y-parity of the - // signature. - Signed::new_unchecked(self, signature.with_parity_bool(), hash) - } - - fn payload_len_for_signature(&self) -> usize { - // The payload length is the length of the `transaction_payload_body` list. - // The sidecar is NOT included. - self.tx.payload_len_for_signature() - } - fn chain_id(&self) -> Option { self.tx.chain_id() } @@ -868,42 +680,10 @@ impl Transaction for TxEip4844WithSidecar { self.tx.nonce() } - fn set_chain_id(&mut self, chain_id: ChainId) { - self.tx.set_chain_id(chain_id); - } - - fn set_gas_limit(&mut self, limit: u64) { - self.tx.set_gas_limit(limit); - } - - fn set_gas_price(&mut self, price: U256) { - self.tx.set_gas_price(price); - } - - fn set_to(&mut self, to: TxKind) { - self.tx.set_to(to); - } - - fn set_input(&mut self, data: Bytes) { - self.tx.set_input(data); - } - - fn set_nonce(&mut self, nonce: u64) { - self.tx.set_nonce(nonce); - } - - fn set_value(&mut self, value: U256) { - self.tx.set_value(value); - } - fn to(&self) -> TxKind { self.tx.to() } - fn signature_hash(&self) -> B256 { - self.tx.signature_hash() - } - fn value(&self) -> U256 { self.tx.value() } @@ -911,10 +691,6 @@ impl Transaction for TxEip4844WithSidecar { fn input(&self) -> &[u8] { self.tx.input() } - - fn input_mut(&mut self) -> &mut Bytes { - self.tx.input_mut() - } } /// This represents a set of blobs, and its corresponding commitments and proofs. diff --git a/crates/consensus/src/transaction/envelope.rs b/crates/consensus/src/transaction/envelope.rs index 97bf3efdae7..40702a78d11 100644 --- a/crates/consensus/src/transaction/envelope.rs +++ b/crates/consensus/src/transaction/envelope.rs @@ -1,6 +1,5 @@ -use crate::{TxEip1559, TxEip2930, TxEip4844Variant, TxLegacy}; +use crate::{Signed, TxEip1559, TxEip2930, TxEip4844Variant, TxLegacy}; use alloy_eips::eip2718::{Decodable2718, Eip2718Error, Encodable2718}; -use alloy_network::Signed; use alloy_rlp::{length_of_length, Decodable, Encodable}; /// Ethereum `TransactionType` flags as specified in EIPs [2718], [1559], and @@ -77,6 +76,12 @@ pub enum TxEnvelope { Eip4844(Signed), } +impl From> for TxEnvelope { + fn from(v: Signed) -> Self { + Self::Legacy(v) + } +} + impl From> for TxEnvelope { fn from(v: Signed) -> Self { Self::Eip2930(v) @@ -89,6 +94,12 @@ impl From> for TxEnvelope { } } +impl From> for TxEnvelope { + fn from(v: Signed) -> Self { + Self::Eip4844(v) + } +} + impl TxEnvelope { /// Return the [`TxType`] of the inner txn. pub const fn tx_type(&self) -> TxType { @@ -202,9 +213,9 @@ impl Encodable2718 for TxEnvelope { #[cfg(test)] mod tests { use super::*; + use crate::transaction::SignableTransaction; use alloy_eips::eip2930::{AccessList, AccessListItem}; - use alloy_network::{Transaction, TxKind}; - use alloy_primitives::{Address, Bytes, Signature, B256, U256}; + use alloy_primitives::{Address, Bytes, Signature, TxKind, B256, U256}; #[test] #[cfg(feature = "k256")] @@ -252,6 +263,7 @@ mod tests { // Test vector from https://sepolia.etherscan.io/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0 // Blobscan: https://sepolia.blobscan.com/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0 fn test_decode_live_4844_tx() { + use crate::Transaction; use alloy_primitives::{address, b256}; // https://sepolia.etherscan.io/getRawTx?tx=0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0 @@ -287,9 +299,9 @@ mod tests { assert_eq!(from, address!("A83C816D4f9b2783761a22BA6FADB0eB0606D7B2")); } - fn test_encode_decode_roundtrip(tx: T) + fn test_encode_decode_roundtrip>(tx: T) where - Signed: Into, + Signed: Into, { let signature = Signature::test_signature(); let tx_signed = tx.into_signed(signature); diff --git a/crates/consensus/src/transaction/legacy.rs b/crates/consensus/src/transaction/legacy.rs index 01f143860ce..cf44418aebe 100644 --- a/crates/consensus/src/transaction/legacy.rs +++ b/crates/consensus/src/transaction/legacy.rs @@ -1,6 +1,5 @@ -use crate::TxKind; -use alloy_network::{Signed, Transaction}; -use alloy_primitives::{keccak256, Bytes, ChainId, Signature, U256}; +use crate::{SignableTransaction, Signed, Transaction}; +use alloy_primitives::{keccak256, Bytes, ChainId, Signature, TxKind, U256}; use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header, Result}; use std::mem; @@ -140,50 +139,40 @@ impl TxLegacy { } } -impl Encodable for TxLegacy { - fn encode(&self, out: &mut dyn BufMut) { - self.encode_for_signing(out) +impl Transaction for TxLegacy { + fn input(&self) -> &[u8] { + &self.input } - fn length(&self) -> usize { - let payload_length = self.fields_len() + self.eip155_fields_len(); - // 'header length' + 'payload length' - length_of_length(payload_length) + payload_length + fn to(&self) -> TxKind { + self.to } -} - -impl Decodable for TxLegacy { - fn decode(data: &mut &[u8]) -> Result { - let header = Header::decode(data)?; - let remaining_len = data.len(); - - let transaction_payload_len = header.payload_length; - if transaction_payload_len > remaining_len { - return Err(alloy_rlp::Error::InputTooShort); - } + fn value(&self) -> U256 { + self.value + } - let mut transaction = Self::decode_fields(data)?; + fn chain_id(&self) -> Option { + self.chain_id + } - // If we still have data, it should be an eip-155 encoded chain_id - if !data.is_empty() { - transaction.chain_id = Some(Decodable::decode(data)?); - let _: U256 = Decodable::decode(data)?; // r - let _: U256 = Decodable::decode(data)?; // s - } + fn nonce(&self) -> u64 { + self.nonce + } - let decoded = remaining_len - data.len(); - if decoded != transaction_payload_len { - return Err(alloy_rlp::Error::UnexpectedLength); - } + fn gas_limit(&self) -> u64 { + self.gas_limit + } - Ok(transaction) + fn gas_price(&self) -> Option { + Some(U256::from(self.gas_price)) } } -impl Transaction for TxLegacy { - type Signature = Signature; - // type Receipt = ReceiptWithBloom; +impl SignableTransaction for TxLegacy { + fn set_chain_id(&mut self, chain_id: ChainId) { + self.chain_id = Some(chain_id); + } fn encode_for_signing(&self, out: &mut dyn BufMut) { Header { list: true, payload_length: self.fields_len() + self.eip155_fields_len() } @@ -225,67 +214,46 @@ impl Transaction for TxLegacy { Ok(tx.into_signed(signature)) } +} - fn input(&self) -> &[u8] { - &self.input - } - - fn input_mut(&mut self) -> &mut Bytes { - &mut self.input - } - - fn set_input(&mut self, data: Bytes) { - self.input = data; - } - - fn to(&self) -> TxKind { - self.to - } - - fn set_to(&mut self, to: TxKind) { - self.to = to; - } - - fn value(&self) -> U256 { - self.value - } - - fn set_value(&mut self, value: U256) { - self.value = value; - } - - fn chain_id(&self) -> Option { - self.chain_id +impl Encodable for TxLegacy { + fn encode(&self, out: &mut dyn BufMut) { + self.encode_for_signing(out) } - fn set_chain_id(&mut self, chain_id: ChainId) { - self.chain_id = Some(chain_id); + fn length(&self) -> usize { + let payload_length = self.fields_len() + self.eip155_fields_len(); + // 'header length' + 'payload length' + length_of_length(payload_length) + payload_length } +} - fn nonce(&self) -> u64 { - self.nonce - } +impl Decodable for TxLegacy { + fn decode(data: &mut &[u8]) -> Result { + let header = Header::decode(data)?; + let remaining_len = data.len(); - fn set_nonce(&mut self, nonce: u64) { - self.nonce = nonce; - } + let transaction_payload_len = header.payload_length; - fn gas_limit(&self) -> u64 { - self.gas_limit - } + if transaction_payload_len > remaining_len { + return Err(alloy_rlp::Error::InputTooShort); + } - fn set_gas_limit(&mut self, gas_limit: u64) { - self.gas_limit = gas_limit; - } + let mut transaction = Self::decode_fields(data)?; - fn gas_price(&self) -> Option { - Some(U256::from(self.gas_price)) - } + // If we still have data, it should be an eip-155 encoded chain_id + if !data.is_empty() { + transaction.chain_id = Some(Decodable::decode(data)?); + let _: U256 = Decodable::decode(data)?; // r + let _: U256 = Decodable::decode(data)?; // s + } - fn set_gas_price(&mut self, price: U256) { - if let Ok(price) = price.try_into() { - self.gas_price = price; + let decoded = remaining_len - data.len(); + if decoded != transaction_payload_len { + return Err(alloy_rlp::Error::UnexpectedLength); } + + Ok(transaction) } } @@ -294,13 +262,11 @@ mod tests { #[test] #[cfg(feature = "k256")] fn recover_signer_legacy() { - use crate::{TxKind, TxLegacy}; - use alloy_network::Transaction; - use alloy_primitives::{b256, hex, Address, Signature, B256, U256}; + use crate::{SignableTransaction, TxLegacy}; + use alloy_primitives::{address, b256, hex, Signature, TxKind, U256}; - let signer: Address = hex!("398137383b3d25c92898c656696e41950e47316b").into(); - let hash: B256 = - hex!("bb3a336e3f823ec18197f1e13ee875700f08f03e2cab75f0d0b118dabb44cba0").into(); + let signer = address!("398137383b3d25c92898c656696e41950e47316b"); + let hash = b256!("bb3a336e3f823ec18197f1e13ee875700f08f03e2cab75f0d0b118dabb44cba0"); let tx = TxLegacy { chain_id: Some(1), @@ -329,8 +295,7 @@ mod tests { #[cfg(feature = "k256")] // Test vector from https://github.com/alloy-rs/alloy/issues/125 fn decode_legacy_and_recover_signer() { - use crate::TxLegacy; - use alloy_network::Signed; + use crate::{Signed, TxLegacy}; use alloy_primitives::address; use alloy_rlp::Decodable; diff --git a/crates/consensus/src/transaction/mod.rs b/crates/consensus/src/transaction/mod.rs index ef7c2012b11..bcc36fddbbb 100644 --- a/crates/consensus/src/transaction/mod.rs +++ b/crates/consensus/src/transaction/mod.rs @@ -1,12 +1,13 @@ +use crate::Signed; +use alloy_primitives::{keccak256, ChainId, TxKind, B256, U256}; +use alloy_rlp::BufMut; + mod eip1559; pub use eip1559::TxEip1559; mod eip2930; pub use eip2930::TxEip2930; -mod legacy; -pub use legacy::TxLegacy; - mod eip4844; #[cfg(feature = "kzg")] pub use eip4844::BlobTransactionValidationError; @@ -14,3 +15,113 @@ pub use eip4844::{BlobTransactionSidecar, TxEip4844, TxEip4844Variant, TxEip4844 mod envelope; pub use envelope::{TxEnvelope, TxType}; + +mod legacy; +pub use legacy::TxLegacy; + +mod typed; +pub use typed::TypedTransaction; + +/// Represents a minimal EVM transaction. +pub trait Transaction: std::any::Any + Send + Sync + 'static { + /// Get `data`. + fn input(&self) -> &[u8]; + + /// Get `to`. + fn to(&self) -> TxKind; + + /// Get `value`. + fn value(&self) -> U256; + + /// Get `chain_id`. + fn chain_id(&self) -> Option; + + /// Get `nonce`. + fn nonce(&self) -> u64; + + /// Get `gas_limit`. + fn gas_limit(&self) -> u64; + + /// Get `gas_price`. + fn gas_price(&self) -> Option; +} + +/// A signable transaction. +/// +/// A transaction can have multiple signature types. This is usually +/// [`alloy_primitives::Signature`], however, it may be different for future EIP-2718 transaction +/// types, or in other networks. For example, in Optimism, the deposit transaction signature is the +/// unit type `()`. +pub trait SignableTransaction: Transaction { + /// Sets `chain_id`. + /// + /// Prefer [`set_chain_id_checked`](Self::set_chain_id_checked). + fn set_chain_id(&mut self, chain_id: ChainId); + + /// Set `chain_id` if it is not already set. Checks that the provided `chain_id` matches the + /// existing `chain_id` if it is already set, returning `false` if they do not match. + fn set_chain_id_checked(&mut self, chain_id: ChainId) -> bool { + match self.chain_id() { + Some(tx_chain_id) => { + if tx_chain_id != chain_id { + return false; + } + self.set_chain_id(chain_id); + } + None => { + self.set_chain_id(chain_id); + } + } + true + } + + /// RLP-encodes the transaction for signing. + fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut); + + /// Outputs the length of the signature RLP encoding for the transaction. + fn payload_len_for_signature(&self) -> usize; + + /// RLP-encodes the transaction for signing it. Used to calculate `signature_hash`. + /// + /// See [`SignableTransaction::encode_for_signing`]. + fn encoded_for_signing(&self) -> Vec { + let mut buf = Vec::with_capacity(self.payload_len_for_signature()); + self.encode_for_signing(&mut buf); + buf + } + + /// Calculate the signing hash for the transaction. + fn signature_hash(&self) -> B256 { + keccak256(self.encoded_for_signing()) + } + + /// Convert to a signed transaction by adding a signature and computing the + /// hash. + fn into_signed(self, signature: Signature) -> Signed + where + Self: Sized; + + /// Encode with a signature. This encoding is usually RLP, but may be + /// different for future EIP-2718 transaction types. + fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut); + + /// Decode a signed transaction. This decoding is usually RLP, but may be + /// different for future EIP-2718 transaction types. + /// + /// This MUST be the inverse of [`SignableTransaction::encode_signed`]. + fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> + where + Self: Sized; +} + +// TODO: Remove in favor of dyn trait upcasting (TBD, see https://github.com/rust-lang/rust/issues/65991#issuecomment-1903120162) +#[doc(hidden)] +impl dyn SignableTransaction { + pub fn __downcast_ref(&self) -> Option<&T> { + if std::any::Any::type_id(self) == std::any::TypeId::of::() { + unsafe { Some(&*(self as *const _ as *const T)) } + } else { + None + } + } +} diff --git a/crates/consensus/src/transaction/typed.rs b/crates/consensus/src/transaction/typed.rs new file mode 100644 index 00000000000..adf3f5d10b3 --- /dev/null +++ b/crates/consensus/src/transaction/typed.rs @@ -0,0 +1,146 @@ +use crate::{Transaction, TxEip1559, TxEip2930, TxEip4844Variant, TxLegacy, TxType}; +use alloy_primitives::TxKind; + +/// The TypedTransaction enum represents all Ethereum transaction request types. +/// +/// Its variants correspond to specific allowed transactions: +/// 1. Legacy (pre-EIP2718) [`TxLegacy`] +/// 2. EIP2930 (state access lists) [`TxEip2930`] +/// 3. EIP1559 [`TxEip1559`] +/// 4. EIP4844 [`TxEip4844Variant`] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TypedTransaction { + /// Legacy transaction + Legacy(TxLegacy), + /// EIP-2930 transaction + Eip2930(TxEip2930), + /// EIP-1559 transaction + Eip1559(TxEip1559), + /// EIP-4844 transaction + Eip4844(TxEip4844Variant), +} + +impl From for TypedTransaction { + fn from(tx: TxLegacy) -> Self { + Self::Legacy(tx) + } +} + +impl From for TypedTransaction { + fn from(tx: TxEip2930) -> Self { + Self::Eip2930(tx) + } +} + +impl From for TypedTransaction { + fn from(tx: TxEip1559) -> Self { + Self::Eip1559(tx) + } +} + +impl From for TypedTransaction { + fn from(tx: TxEip4844Variant) -> Self { + Self::Eip4844(tx) + } +} + +impl TypedTransaction { + /// Return the [`TxType`] of the inner txn. + pub const fn tx_type(&self) -> TxType { + match self { + Self::Legacy(_) => TxType::Legacy, + Self::Eip2930(_) => TxType::Eip2930, + Self::Eip1559(_) => TxType::Eip1559, + Self::Eip4844(_) => TxType::Eip4844, + } + } + + /// Return the inner legacy transaction if it exists. + pub const fn legacy(&self) -> Option<&TxLegacy> { + match self { + Self::Legacy(tx) => Some(tx), + _ => None, + } + } + + /// Return the inner EIP-2930 transaction if it exists. + pub const fn eip2930(&self) -> Option<&TxEip2930> { + match self { + Self::Eip2930(tx) => Some(tx), + _ => None, + } + } + + /// Return the inner EIP-1559 transaction if it exists. + pub const fn eip1559(&self) -> Option<&TxEip1559> { + match self { + Self::Eip1559(tx) => Some(tx), + _ => None, + } + } +} + +impl Transaction for TypedTransaction { + fn chain_id(&self) -> Option { + match self { + Self::Legacy(tx) => tx.chain_id(), + Self::Eip2930(tx) => tx.chain_id(), + Self::Eip1559(tx) => tx.chain_id(), + Self::Eip4844(tx) => tx.chain_id(), + } + } + + fn gas_limit(&self) -> u64 { + match self { + Self::Legacy(tx) => tx.gas_limit(), + Self::Eip2930(tx) => tx.gas_limit(), + Self::Eip1559(tx) => tx.gas_limit(), + Self::Eip4844(tx) => tx.gas_limit(), + } + } + + fn gas_price(&self) -> Option { + match self { + Self::Legacy(tx) => tx.gas_price(), + Self::Eip2930(tx) => tx.gas_price(), + Self::Eip1559(tx) => tx.gas_price(), + Self::Eip4844(tx) => tx.gas_price(), + } + } + + fn input(&self) -> &[u8] { + match self { + Self::Legacy(tx) => tx.input(), + Self::Eip2930(tx) => tx.input(), + Self::Eip1559(tx) => tx.input(), + Self::Eip4844(tx) => tx.input(), + } + } + + fn nonce(&self) -> u64 { + match self { + Self::Legacy(tx) => tx.nonce(), + Self::Eip2930(tx) => tx.nonce(), + Self::Eip1559(tx) => tx.nonce(), + Self::Eip4844(tx) => tx.nonce(), + } + } + + fn to(&self) -> TxKind { + match self { + Self::Legacy(tx) => tx.to(), + Self::Eip2930(tx) => tx.to(), + Self::Eip1559(tx) => tx.to(), + Self::Eip4844(tx) => tx.to(), + } + } + + fn value(&self) -> alloy_primitives::U256 { + match self { + Self::Legacy(tx) => tx.value(), + Self::Eip2930(tx) => tx.value(), + Self::Eip1559(tx) => tx.value(), + Self::Eip4844(tx) => tx.value(), + } + } +} diff --git a/crates/contract/Cargo.toml b/crates/contract/Cargo.toml index 23bb2f17f2c..f8963a965e6 100644 --- a/crates/contract/Cargo.toml +++ b/crates/contract/Cargo.toml @@ -12,6 +12,7 @@ repository.workspace = true exclude.workspace = true [dependencies] +alloy-network.workspace = true alloy-providers.workspace = true alloy-rpc-types.workspace = true alloy-transport.workspace = true @@ -21,8 +22,13 @@ alloy-json-abi.workspace = true alloy-primitives.workspace = true alloy-sol-types.workspace = true +futures-util.workspace = true + thiserror.workspace = true [dev-dependencies] +alloy-rpc-client.workspace = true +alloy-transport-http.workspace = true alloy-node-bindings.workspace = true +reqwest.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/contract/src/call.rs b/crates/contract/src/call.rs index ef5e1b6ac0b..65979fb3595 100644 --- a/crates/contract/src/call.rs +++ b/crates/contract/src/call.rs @@ -1,14 +1,13 @@ use crate::{Error, Result}; use alloy_dyn_abi::{DynSolValue, FunctionExt, JsonAbiExt}; use alloy_json_abi::Function; +use alloy_network::{Network, ReceiptResponse, TransactionBuilder}; use alloy_primitives::{Address, Bytes, U256, U64}; -use alloy_providers::tmp::TempProvider; -use alloy_rpc_types::{ - request::{TransactionInput, TransactionRequest}, - state::StateOverride, - BlockId, TransactionReceipt, -}; +use alloy_providers::Provider; +use alloy_rpc_types::{state::StateOverride, BlockId}; use alloy_sol_types::SolCall; +use alloy_transport::Transport; +use futures_util::TryFutureExt; use std::{ future::{Future, IntoFuture}, marker::PhantomData, @@ -17,13 +16,13 @@ use std::{ /// [`CallBuilder`] using a [`SolCall`] type as the call decoder. // NOTE: please avoid changing this type due to its use in the `sol!` macro. -pub type SolCallBuilder = CallBuilder>; +pub type SolCallBuilder = CallBuilder>; /// [`CallBuilder`] using a [`Function`] as the call decoder. -pub type DynCallBuilder

= CallBuilder; +pub type DynCallBuilder = CallBuilder; /// [`CallBuilder`] that does not have a call decoder. -pub type RawCallBuilder

= CallBuilder; +pub type RawCallBuilder = CallBuilder; mod private { pub trait Sealed {} @@ -120,7 +119,7 @@ impl CallDecoder for () { /// Using [`sol!`][sol]: /// /// ```no_run -/// # async fn test(provider: P) -> Result<(), Box> { +/// # async fn test>(provider: P) -> Result<(), Box> { /// use alloy_contract::SolCallBuilder; /// use alloy_primitives::{Address, U256}; /// use alloy_sol_types::sol; @@ -141,13 +140,13 @@ impl CallDecoder for () { /// // Through `contract.(args...)` /// let a = U256::ZERO; /// let b = true; -/// let builder: SolCallBuilder<_, MyContract::doStuffCall> = contract.doStuff(a, b); +/// let builder: SolCallBuilder<_, _, _, MyContract::doStuffCall> = contract.doStuff(a, b); /// let MyContract::doStuffReturn { c: _, d: _ } = builder.call().await?; /// /// // Through `contract.call_builder(&)`: /// // (note that this is discouraged because it's inherently less type-safe) /// let call = MyContract::doStuffCall { a, b }; -/// let builder: SolCallBuilder<_, MyContract::doStuffCall> = contract.call_builder(&call); +/// let builder: SolCallBuilder<_, _, _, MyContract::doStuffCall> = contract.call_builder(&call); /// let MyContract::doStuffReturn { c: _, d: _ } = builder.call().await?; /// # Ok(()) /// # } @@ -156,7 +155,7 @@ impl CallDecoder for () { /// Using [`ContractInstance`](crate::ContractInstance): /// /// ```no_run -/// # async fn test(provider: P, dynamic_abi: alloy_json_abi::JsonAbi) -> Result<(), Box> { +/// # async fn test>(provider: P, dynamic_abi: alloy_json_abi::JsonAbi) -> Result<(), Box> { /// use alloy_primitives::{Address, Bytes, U256}; /// use alloy_dyn_abi::DynSolValue; /// use alloy_contract::{CallBuilder, ContractInstance, DynCallBuilder, Interface, RawCallBuilder}; @@ -170,16 +169,16 @@ impl CallDecoder for () { /// let provider = ...; /// # ); /// let address = Address::ZERO; -/// let contract: ContractInstance<_> = interface.connect(address, &provider); +/// let contract: ContractInstance<_, _, _> = interface.connect(address, &provider); /// /// // Build and call the function: -/// let call_builder: DynCallBuilder<_> = contract.function("doStuff", &[U256::ZERO.into(), true.into()])?; +/// let call_builder: DynCallBuilder<_, _, _> = contract.function("doStuff", &[U256::ZERO.into(), true.into()])?; /// let result: Vec = call_builder.call().await?; /// /// // You can also decode the output manually. Get the raw bytes: /// let raw_result: Bytes = call_builder.call_raw().await?; /// // Or, equivalently: -/// let raw_builder: RawCallBuilder<_> = call_builder.clone().clear_decoder(); +/// let raw_builder: RawCallBuilder<_, _, _> = call_builder.clone().clear_decoder(); /// let raw_result: Bytes = raw_builder.call().await?; /// // Decode the raw bytes: /// let decoded_result: Vec = call_builder.decode_output(raw_result, false)?; @@ -190,36 +189,36 @@ impl CallDecoder for () { /// [sol]: alloy_sol_types::sol #[derive(Clone)] #[must_use = "call builders do nothing unless you `.call`, `.send`, or `.await` them"] -pub struct CallBuilder { - // TODO: this will not work with `send_transaction` and does not differentiate between EIP-1559 - // and legacy tx - request: TransactionRequest, +pub struct CallBuilder { + request: N::TransactionRequest, block: Option, state: Option, provider: P, decoder: D, + transport: PhantomData, } // See [`ContractInstance`]. -impl DynCallBuilder

{ +impl> DynCallBuilder { pub(crate) fn new_dyn(provider: P, function: &Function, args: &[DynSolValue]) -> Result { Ok(Self::new_inner(provider, function.abi_encode_input(args)?.into(), function.clone())) } /// Clears the decoder, returning a raw call builder. #[inline] - pub fn clear_decoder(self) -> RawCallBuilder

{ + pub fn clear_decoder(self) -> RawCallBuilder { RawCallBuilder { request: self.request, block: self.block, state: self.state, provider: self.provider, decoder: (), + transport: PhantomData, } } } -impl SolCallBuilder { +impl, C: SolCall> SolCallBuilder { // `sol!` macro constructor, see `#[sol(rpc)]`. Not public API. // NOTE: please avoid changing this function due to its use in the `sol!` macro. #[doc(hidden)] @@ -229,18 +228,19 @@ impl SolCallBuilder { /// Clears the decoder, returning a raw call builder. #[inline] - pub fn clear_decoder(self) -> RawCallBuilder

{ + pub fn clear_decoder(self) -> RawCallBuilder { RawCallBuilder { request: self.request, block: self.block, state: self.state, provider: self.provider, decoder: (), + transport: PhantomData, } } } -impl RawCallBuilder

{ +impl> RawCallBuilder { /// Creates a new call builder with the provided provider and ABI encoded input. /// /// Will not decode the output of the call, meaning that [`call`](Self::call) will behave the @@ -251,22 +251,27 @@ impl RawCallBuilder

{ } } -impl CallBuilder { +impl, D: CallDecoder> CallBuilder { fn new_inner(provider: P, input: Bytes, decoder: D) -> Self { - let request = - TransactionRequest { input: TransactionInput::new(input), ..Default::default() }; - Self { request, decoder, provider, block: None, state: None } + Self { + request: ::default().with_input(input), + decoder, + provider, + block: None, + state: None, + transport: PhantomData, + } } /// Sets the `from` field in the transaction to the provided value. Defaults to [Address::ZERO]. pub fn from(mut self, from: Address) -> Self { - self.request = self.request.from(from); + self.request.set_from(from); self } /// Sets the `to` field in the transaction to the provided address. pub fn to(mut self, to: Option

) -> Self { - self.request = self.request.to(to); + self.request.set_to(to.into()); self } @@ -277,7 +282,7 @@ impl CallBuilder { /// Sets the `gas` field in the transaction to the provided value pub fn gas(mut self, gas: U256) -> Self { - self.request = self.request.gas_limit(gas); + self.request.set_gas_limit(gas); self } @@ -285,20 +290,28 @@ impl CallBuilder { /// If the internal transaction is an EIP-1559 one, then it sets both /// `max_fee_per_gas` and `max_priority_fee_per_gas` to the same value pub fn gas_price(mut self, gas_price: U256) -> Self { - self.request = self.request.max_fee_per_gas(gas_price); - self.request = self.request.max_priority_fee_per_gas(gas_price); + self.request.set_gas_price(gas_price); self } /// Sets the `value` field in the transaction to the provided value pub fn value(mut self, value: U256) -> Self { - self.request = self.request.value(value); + self.request.set_value(value); self } /// Sets the `nonce` field in the transaction to the provided value pub fn nonce(mut self, nonce: U64) -> Self { - self.request = self.request.nonce(nonce); + self.request.set_nonce(nonce); + self + } + + /// Applies a function to the internal transaction request. + pub fn map(mut self, f: F) -> Self + where + F: FnOnce(N::TransactionRequest) -> N::TransactionRequest, + { + self.request = f(self.request); self } @@ -320,12 +333,12 @@ impl CallBuilder { /// Returns the underlying transaction's ABI-encoded data. pub fn calldata(&self) -> &Bytes { - self.request.input.input().expect("set in the constructor") + self.request.input().expect("set in the constructor") } /// Returns the estimated gas cost for the underlying transaction to be executed pub async fn estimate_gas(&self) -> Result { - self.provider.estimate_gas(self.request.clone(), self.block).await.map_err(Into::into) + self.provider.estimate_gas(&self.request, self.block).await.map_err(Into::into) } /// Queries the blockchain via an `eth_call` without submitting a transaction to the network. @@ -344,9 +357,9 @@ impl CallBuilder { /// See [`call`](Self::call) for more information. pub async fn call_raw(&self) -> Result { if let Some(state) = &self.state { - self.provider.call_with_overrides(self.request.clone(), self.block, state.clone()).await + self.provider.call_with_overrides(&self.request, self.block, state.clone()).await } else { - self.provider.call(self.request.clone(), self.block).await + self.provider.call(&self.request, self.block).await } .map_err(Into::into) } @@ -368,30 +381,25 @@ impl CallBuilder { /// Note that the deployment address can be pre-calculated if the `from` address and `nonce` are /// known using [`calculate_create_address`](Self::calculate_create_address). pub async fn deploy(&self) -> Result
{ - if self.request.to.is_some() { + if !self.request.to().is_some_and(|to| to.is_create()) { return Err(Error::NotADeploymentTransaction); } let pending_tx = self.send().await?; let receipt = pending_tx.await?; - receipt.contract_address.ok_or(Error::ContractNotDeployed) + receipt + .ok_or(Error::ContractNotDeployed)? + .contract_address() + .ok_or(Error::ContractNotDeployed) } /// Broadcasts the underlying transaction to the network. // TODO: more docs referring to customizing PendingTransaction - pub async fn send(&self) -> Result>> { - // TODO: send_transaction, PendingTransaction - // NOTE: This struct is needed to have a concrete type for the `Future` trait. - struct Tmp(PhantomData); - impl Future for Tmp { - type Output = T; - fn poll( - self: Pin<&mut Self>, - _cx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - todo!() - } - } - Ok(Tmp(PhantomData)) + pub async fn send( + &self, + ) -> Result>> + '_> { + let pending = self.provider.send_transaction(self.request.clone()).await?; + + Ok(pending.and_then(|hash| self.provider.get_transaction_receipt(hash)).map_err(Into::into)) } /// Calculates the address that will be created by the transaction, if any. @@ -403,15 +411,16 @@ impl CallBuilder { } } -impl CallBuilder<&P, D> { +impl CallBuilder { /// Clones the provider and returns a new builder with the cloned provider. - pub fn with_cloned_provider(self) -> CallBuilder { + pub fn with_cloned_provider(self) -> CallBuilder { CallBuilder { request: self.request, block: self.block, state: self.state, provider: self.provider.clone(), decoder: self.decoder, + transport: PhantomData, } } } @@ -419,9 +428,11 @@ impl CallBuilder<&P, D> { /// [`CallBuilder`] can be turned into a [`Future`] automatically with `.await`. /// /// Defaults to calling [`CallBuilder::call`]. -impl IntoFuture for CallBuilder +impl IntoFuture for CallBuilder where - P: TempProvider, + N: Network, + T: Transport + Clone, + P: Provider, D: CallDecoder + Send + Sync, Self: 'static, { @@ -438,7 +449,7 @@ where } } -impl std::fmt::Debug for CallBuilder { +impl std::fmt::Debug for CallBuilder { #[inline] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("CallBuilder") @@ -454,10 +465,21 @@ impl std::fmt::Debug for CallBuilder { #[allow(unused_imports)] mod tests { use super::*; + use alloy_network::Ethereum; use alloy_node_bindings::{Anvil, AnvilInstance}; use alloy_primitives::{address, b256, bytes, hex}; - use alloy_providers::tmp::{HttpProvider, Provider}; + use alloy_providers::{HttpProvider, Provider, RootProvider}; + use alloy_rpc_client::RpcClient; use alloy_sol_types::sol; + use alloy_transport_http::Http; + use reqwest::Client; + + fn spawn_anvil() -> (HttpProvider, AnvilInstance) { + let anvil = Anvil::new().spawn(); + let url = anvil.endpoint().parse().unwrap(); + let http = Http::::new(url); + (RootProvider::::new(RpcClient::new(http, true)), anvil) + } #[test] fn empty_constructor() { @@ -468,7 +490,7 @@ mod tests { } } - let provider = Provider::try_from("http://localhost:8545").unwrap(); + let (provider, _anvil) = spawn_anvil(); let call_builder = EmptyConstructor::deploy_builder(&provider); assert_eq!(*call_builder.calldata(), bytes!("6942")); } @@ -492,7 +514,7 @@ mod tests { #[test] fn call_encoding() { - let provider = Provider::try_from("http://localhost:8545").unwrap(); + let (provider, _anvil) = spawn_anvil(); let contract = MyContract::new(Address::ZERO, &&provider).with_cloned_provider(); let call_builder = contract.doStuff(U256::ZERO, true).with_cloned_provider(); assert_eq!( @@ -510,7 +532,7 @@ mod tests { #[test] fn deploy_encoding() { - let provider = Provider::try_from("http://localhost:8545").unwrap(); + let (provider, _anvil) = spawn_anvil(); let bytecode = &MyContract::BYTECODE[..]; let call_builder = MyContract::deploy_builder(&provider, false); assert_eq!( @@ -534,7 +556,6 @@ mod tests { // TODO: send_transaction, PendingTransaction #[tokio::test(flavor = "multi_thread")] - #[ignore = "TODO"] async fn deploy_and_call() { let (provider, anvil) = spawn_anvil(); @@ -559,10 +580,4 @@ mod tests { b256!("0000000000000000000000000000000000000000000000000000000000000001"), ); } - - fn spawn_anvil() -> (HttpProvider, AnvilInstance) { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(anvil.endpoint()).unwrap(); - (provider, anvil) - } } diff --git a/crates/contract/src/instance.rs b/crates/contract/src/instance.rs index e94ee9d9cae..0382a9f9371 100644 --- a/crates/contract/src/instance.rs +++ b/crates/contract/src/instance.rs @@ -1,25 +1,30 @@ use crate::{CallBuilder, Interface, Result}; use alloy_dyn_abi::DynSolValue; use alloy_json_abi::{Function, JsonAbi}; +use alloy_network::Network; use alloy_primitives::{Address, Selector}; -use alloy_providers::tmp::TempProvider; +use alloy_providers::Provider; +use alloy_transport::Transport; +use std::marker::PhantomData; /// A handle to an Ethereum contract at a specific address. /// /// A contract is an abstraction of an executable program on Ethereum. Every deployed contract has /// an address, which is used to connect to it so that it may receive messages (transactions). #[derive(Clone)] -pub struct ContractInstance

{ +pub struct ContractInstance { address: Address, provider: P, interface: Interface, + transport: PhantomData, + network: PhantomData, } -impl

ContractInstance

{ +impl ContractInstance { /// Creates a new contract from the provided address, provider, and interface. #[inline] pub const fn new(address: Address, provider: P, interface: Interface) -> Self { - Self { address, provider, interface } + Self { address, provider, interface, transport: PhantomData, network: PhantomData } } /// Returns a reference to the contract's address. @@ -36,7 +41,7 @@ impl

ContractInstance

{ /// Returns a new contract instance at `address`. #[inline] - pub fn at(mut self, address: Address) -> ContractInstance

{ + pub fn at(mut self, address: Address) -> ContractInstance { self.set_address(address); self } @@ -54,25 +59,31 @@ impl

ContractInstance

{ } } -impl ContractInstance<&P> { +impl ContractInstance { /// Clones the provider and returns a new contract instance with the cloned provider. #[inline] - pub fn with_cloned_provider(self) -> ContractInstance

{ + pub fn with_cloned_provider(self) -> ContractInstance { ContractInstance { address: self.address, provider: self.provider.clone(), interface: self.interface, + transport: PhantomData, + network: PhantomData, } } } -impl ContractInstance

{ +impl> ContractInstance { /// Returns a transaction builder for the provided function name. /// /// If there are multiple functions with the same name due to overloading, consider using /// the [`ContractInstance::function_from_selector`] method instead, since this will use the /// first match. - pub fn function(&self, name: &str, args: &[DynSolValue]) -> Result> { + pub fn function( + &self, + name: &str, + args: &[DynSolValue], + ) -> Result> { let function = self.interface.get_from_name(name)?; CallBuilder::new_dyn(&self.provider, function, args) } @@ -82,13 +93,13 @@ impl ContractInstance

{ &self, selector: &Selector, args: &[DynSolValue], - ) -> Result> { + ) -> Result> { let function = self.interface.get_from_selector(selector)?; CallBuilder::new_dyn(&self.provider, function, args) } } -impl

std::ops::Deref for ContractInstance

{ +impl std::ops::Deref for ContractInstance { type Target = Interface; fn deref(&self) -> &Self::Target { @@ -96,7 +107,7 @@ impl

std::ops::Deref for ContractInstance

{ } } -impl

std::fmt::Debug for ContractInstance

{ +impl std::fmt::Debug for ContractInstance { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ContractInstance").field("address", &self.address).finish() } diff --git a/crates/contract/src/interface.rs b/crates/contract/src/interface.rs index cf719d0795c..836b7317cff 100644 --- a/crates/contract/src/interface.rs +++ b/crates/contract/src/interface.rs @@ -116,7 +116,11 @@ impl Interface { } /// Create a [`ContractInstance`] from this ABI for a contract at the given address. - pub const fn connect

(self, address: Address, provider: P) -> ContractInstance

{ + pub const fn connect( + self, + address: Address, + provider: P, + ) -> ContractInstance { ContractInstance::new(address, provider, self) } } diff --git a/crates/contract/src/lib.rs b/crates/contract/src/lib.rs index 64561fdce9b..a7c77710b46 100644 --- a/crates/contract/src/lib.rs +++ b/crates/contract/src/lib.rs @@ -34,5 +34,7 @@ pub use call::*; // NOTE: please avoid changing the API of this module due to its use in the `sol!` macro. #[doc(hidden)] pub mod private { - pub use alloy_providers::tmp::TempProvider as Provider; + pub use alloy_network::Network; + pub use alloy_providers::Provider; + pub use alloy_transport::Transport; } diff --git a/crates/eips/src/eip1559/helpers.rs b/crates/eips/src/eip1559/helpers.rs index a7d622face5..98980e2371a 100644 --- a/crates/eips/src/eip1559/helpers.rs +++ b/crates/eips/src/eip1559/helpers.rs @@ -63,9 +63,8 @@ pub fn calc_next_block_base_fee( #[cfg(test)] mod tests { - use crate::eip1559::constants::{MIN_PROTOCOL_BASE_FEE, MIN_PROTOCOL_BASE_FEE_U256}; - use super::*; + use crate::eip1559::constants::{MIN_PROTOCOL_BASE_FEE, MIN_PROTOCOL_BASE_FEE_U256}; #[test] fn min_protocol_sanity() { diff --git a/crates/network/Cargo.toml b/crates/network/Cargo.toml index 51ecf79cbc8..d9142a496ae 100644 --- a/crates/network/Cargo.toml +++ b/crates/network/Cargo.toml @@ -12,11 +12,18 @@ repository.workspace = true exclude.workspace = true [dependencies] +alloy-consensus.workspace = true alloy-eips = { workspace = true, features = ["serde"] } alloy-json-rpc.workspace = true alloy-primitives.workspace = true -alloy-rlp.workspace = true +alloy-rpc-types.workspace = true +alloy-signer.workspace = true +async-trait.workspace = true serde = { workspace = true, features = ["derive"] } +thiserror.workspace = true + +[dev-dependencies] +tokio.workspace = true [features] -k256 = ["alloy-primitives/k256"] +k256 = ["alloy-primitives/k256", "alloy-consensus/k256"] diff --git a/crates/network/src/ethereum/builder.rs b/crates/network/src/ethereum/builder.rs new file mode 100644 index 00000000000..b8a2e23fb7b --- /dev/null +++ b/crates/network/src/ethereum/builder.rs @@ -0,0 +1,242 @@ +use crate::{ + BuilderResult, Ethereum, Network, NetworkSigner, TransactionBuilder, TransactionBuilderError, +}; +use alloy_consensus::{TxEip1559, TxEip2930, TxEip4844, TxEip4844Variant, TxLegacy}; +use alloy_primitives::{Address, TxKind, U256, U64}; +use alloy_rpc_types::request::TransactionRequest; +use async_trait::async_trait; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TransactionBuilder for alloy_rpc_types::TransactionRequest { + fn chain_id(&self) -> Option { + self.chain_id + } + + fn set_chain_id(&mut self, chain_id: alloy_primitives::ChainId) { + self.chain_id = Some(chain_id); + } + + fn nonce(&self) -> Option { + self.nonce + } + + fn set_nonce(&mut self, nonce: U64) { + self.nonce = Some(nonce); + } + + fn input(&self) -> Option<&alloy_primitives::Bytes> { + self.input.input() + } + + fn set_input(&mut self, input: alloy_primitives::Bytes) { + self.input.input = Some(input); + } + + fn to(&self) -> Option { + self.to.map(TxKind::Call).or(Some(TxKind::Create)) + } + + fn from(&self) -> Option

{ + self.from + } + + fn set_from(&mut self, from: Address) { + self.from = Some(from); + } + + fn set_to(&mut self, to: alloy_primitives::TxKind) { + match to { + TxKind::Create => self.to = None, + TxKind::Call(to) => self.to = Some(to), + } + } + + fn value(&self) -> Option { + self.value + } + + fn set_value(&mut self, value: alloy_primitives::U256) { + self.value = Some(value) + } + + fn gas_price(&self) -> Option { + self.gas_price + } + + fn set_gas_price(&mut self, gas_price: U256) { + self.gas_price = Some(gas_price); + } + + fn max_fee_per_gas(&self) -> Option { + self.max_fee_per_gas + } + + fn set_max_fee_per_gas(&mut self, max_fee_per_gas: U256) { + self.max_fee_per_gas = Some(max_fee_per_gas); + } + + fn max_priority_fee_per_gas(&self) -> Option { + self.max_priority_fee_per_gas + } + + fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: U256) { + self.max_priority_fee_per_gas = Some(max_priority_fee_per_gas); + } + + fn max_fee_per_blob_gas(&self) -> Option { + self.max_fee_per_blob_gas + } + + fn set_max_fee_per_blob_gas(&mut self, max_fee_per_blob_gas: U256) { + self.max_fee_per_blob_gas = Some(max_fee_per_blob_gas) + } + + fn gas_limit(&self) -> Option { + self.gas + } + + fn set_gas_limit(&mut self, gas_limit: U256) { + self.gas = Some(gas_limit); + } + + fn build_unsigned(self) -> BuilderResult<::UnsignedTx> { + match ( + self.gas_price.as_ref(), + self.max_fee_per_gas.as_ref(), + self.access_list.as_ref(), + self.max_fee_per_blob_gas.as_ref(), + self.blob_versioned_hashes.as_ref(), + self.sidecar.as_ref(), + ) { + // Legacy transaction + (Some(_), None, None, None, None, None) => build_legacy(self).map(Into::into), + // EIP-2930 + // If only accesslist is set, and there are no EIP-1559 fees + (_, None, Some(_), None, None, None) => build_2930(self).map(Into::into), + // EIP-1559 + // If EIP-4844 fields are missing + (None, _, _, None, None, None) => build_1559(self).map(Into::into), + // EIP-4844 + // All blob fields required + (None, _, _, Some(_), Some(_), Some(_)) => { + build_4844(self).map(TxEip4844Variant::from).map(Into::into) + } + _ => build_legacy(self).map(Into::into), + } + } + + async fn build>( + self, + signer: &S, + ) -> BuilderResult<::TxEnvelope> { + Ok(signer.sign_transaction(self.build_unsigned()?).await?) + } +} + +/// Build a legacy transaction. +fn build_legacy(request: TransactionRequest) -> Result { + Ok(TxLegacy { + chain_id: request.chain_id, + nonce: request.nonce.ok_or_else(|| TransactionBuilderError::MissingKey("nonce"))?.to(), + gas_price: request + .gas_price + .ok_or_else(|| TransactionBuilderError::MissingKey("gas_price"))? + .to(), + gas_limit: request + .gas + .ok_or_else(|| TransactionBuilderError::MissingKey("gas_limit"))? + .to(), + to: request.to.into(), + value: request.value.unwrap_or_default(), + input: request.input.into_input().unwrap_or_default(), + }) +} + +/// Build an EIP-1559 transaction. +fn build_1559(request: TransactionRequest) -> Result { + Ok(TxEip1559 { + chain_id: request.chain_id.unwrap_or(1), + nonce: request.nonce.ok_or_else(|| TransactionBuilderError::MissingKey("nonce"))?.to(), + max_priority_fee_per_gas: request + .max_priority_fee_per_gas + .ok_or_else(|| TransactionBuilderError::MissingKey("max_priority_fee_per_gas"))? + .to(), + max_fee_per_gas: request + .max_fee_per_gas + .ok_or_else(|| TransactionBuilderError::MissingKey("max_fee_per_gas"))? + .to(), + gas_limit: request + .gas + .ok_or_else(|| TransactionBuilderError::MissingKey("gas_limit"))? + .to(), + to: request.to.into(), + value: request.value.unwrap_or_default(), + input: request.input.into_input().unwrap_or_default(), + access_list: convert_access_list(request.access_list.unwrap_or_default()), + }) +} + +/// Build an EIP-2930 transaction. +fn build_2930(request: TransactionRequest) -> Result { + Ok(TxEip2930 { + chain_id: request.chain_id.unwrap_or(1), + nonce: request.nonce.ok_or_else(|| TransactionBuilderError::MissingKey("nonce"))?.to(), + gas_price: request + .gas_price + .ok_or_else(|| TransactionBuilderError::MissingKey("gas_price"))? + .to(), + gas_limit: request + .gas + .ok_or_else(|| TransactionBuilderError::MissingKey("gas_limit"))? + .to(), + to: request.to.into(), + value: request.value.unwrap_or_default(), + input: request.input.into_input().unwrap_or_default(), + access_list: convert_access_list(request.access_list.unwrap_or_default()), + }) +} + +/// Build an EIP-4844 transaction. +fn build_4844(request: TransactionRequest) -> Result { + Ok(TxEip4844 { + chain_id: request.chain_id.unwrap_or(1), + nonce: request.nonce.ok_or_else(|| TransactionBuilderError::MissingKey("nonce"))?.to(), + gas_limit: request + .gas + .ok_or_else(|| TransactionBuilderError::MissingKey("gas_limit"))? + .to(), + max_fee_per_gas: request + .max_fee_per_gas + .ok_or_else(|| TransactionBuilderError::MissingKey("max_fee_per_gas"))? + .to(), + max_priority_fee_per_gas: request + .max_priority_fee_per_gas + .ok_or_else(|| TransactionBuilderError::MissingKey("max_priority_fee_per_gas"))? + .to(), + to: request.to.into(), + value: request.value.unwrap_or_default(), + access_list: convert_access_list(request.access_list.unwrap_or_default()), + blob_versioned_hashes: request + .blob_versioned_hashes + .ok_or_else(|| TransactionBuilderError::MissingKey("blob_versioned_hashes"))?, + max_fee_per_blob_gas: request + .max_fee_per_blob_gas + .ok_or_else(|| TransactionBuilderError::MissingKey("max_fee_per_blob_gas"))? + .to(), + input: request.input.into_input().unwrap_or_default(), + }) +} + +// todo: these types are almost 1:1, minus rlp decoding and ser/de, should dedupe +fn convert_access_list(list: alloy_rpc_types::AccessList) -> alloy_eips::eip2930::AccessList { + alloy_eips::eip2930::AccessList( + list.0 + .into_iter() + .map(|item| alloy_eips::eip2930::AccessListItem { + address: item.address, + storage_keys: item.storage_keys, + }) + .collect(), + ) +} diff --git a/crates/network/src/ethereum/mod.rs b/crates/network/src/ethereum/mod.rs new file mode 100644 index 00000000000..2479ef19cca --- /dev/null +++ b/crates/network/src/ethereum/mod.rs @@ -0,0 +1,36 @@ +use crate::{Network, ReceiptResponse}; + +mod builder; + +mod signer; +pub use signer::EthereumSigner; + +/// Types for a mainnet-like Ethereum network. +#[derive(Debug, Clone, Copy)] +pub struct Ethereum { + _private: (), +} + +impl Network for Ethereum { + type TxEnvelope = alloy_consensus::TxEnvelope; + + type UnsignedTx = alloy_consensus::TypedTransaction; + + type ReceiptEnvelope = alloy_consensus::ReceiptEnvelope; + + type Header = alloy_consensus::Header; + + type TransactionRequest = alloy_rpc_types::transaction::TransactionRequest; + + type TransactionResponse = alloy_rpc_types::Transaction; + + type ReceiptResponse = alloy_rpc_types::TransactionReceipt; + + type HeaderResponse = alloy_rpc_types::Header; +} + +impl ReceiptResponse for alloy_rpc_types::TransactionReceipt { + fn contract_address(&self) -> Option { + self.contract_address + } +} diff --git a/crates/network/src/ethereum/signer.rs b/crates/network/src/ethereum/signer.rs new file mode 100644 index 00000000000..352caf30271 --- /dev/null +++ b/crates/network/src/ethereum/signer.rs @@ -0,0 +1,152 @@ +use super::Ethereum; +use crate::{NetworkSigner, TxSigner}; +use alloy_consensus::{SignableTransaction, TxEnvelope, TypedTransaction}; +use alloy_signer::Signature; +use async_trait::async_trait; +use std::sync::Arc; + +/// A signer capable of signing any transaction for the Ethereum network. +#[derive(Clone)] +pub struct EthereumSigner(Arc + Send + Sync>); + +impl std::fmt::Debug for EthereumSigner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("EthereumSigner").finish() + } +} + +impl From for EthereumSigner +where + S: TxSigner + Send + Sync + 'static, +{ + fn from(signer: S) -> Self { + Self::new(signer) + } +} + +impl EthereumSigner { + /// Create a new Ethereum signer. + pub fn new(signer: S) -> Self + where + S: TxSigner + Send + Sync + 'static, + { + Self(Arc::new(signer)) + } + + async fn sign_transaction( + &self, + tx: &mut dyn SignableTransaction, + ) -> alloy_signer::Result { + self.0.sign_transaction(tx).await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl NetworkSigner for EthereumSigner { + async fn sign_transaction(&self, tx: TypedTransaction) -> alloy_signer::Result { + match tx { + TypedTransaction::Legacy(mut t) => { + let sig = self.sign_transaction(&mut t).await?; + Ok(t.into_signed(sig).into()) + } + TypedTransaction::Eip2930(mut t) => { + let sig = self.sign_transaction(&mut t).await?; + Ok(t.into_signed(sig).into()) + } + TypedTransaction::Eip1559(mut t) => { + let sig = self.sign_transaction(&mut t).await?; + Ok(t.into_signed(sig).into()) + } + TypedTransaction::Eip4844(mut t) => { + let sig = self.sign_transaction(&mut t).await?; + Ok(t.into_signed(sig).into()) + } + } + } +} + +#[cfg(test)] +mod test { + use crate::{TxSigner, TxSignerSync}; + use alloy_consensus::{SignableTransaction, TxLegacy}; + use alloy_primitives::{address, ChainId, Signature, U256}; + use alloy_signer::{LocalWallet, Result, Signer}; + + #[tokio::test] + async fn signs_tx() { + async fn sign_tx_test(tx: &mut TxLegacy, chain_id: Option) -> Result { + let mut before = tx.clone(); + let sig = sign_dyn_tx_test(tx, chain_id).await?; + if let Some(chain_id) = chain_id { + assert_eq!(tx.chain_id, Some(chain_id), "chain ID was not set"); + before.chain_id = Some(chain_id); + } + assert_eq!(*tx, before); + Ok(sig) + } + + async fn sign_dyn_tx_test( + tx: &mut dyn SignableTransaction, + chain_id: Option, + ) -> Result { + let mut wallet: LocalWallet = + "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap(); + wallet.set_chain_id(chain_id); + + let sig = wallet.sign_transaction_sync(tx)?; + let sighash = tx.signature_hash(); + assert_eq!(sig.recover_address_from_prehash(&sighash).unwrap(), wallet.address()); + + let sig_async = wallet.sign_transaction(tx).await.unwrap(); + assert_eq!(sig_async, sig); + + Ok(sig) + } + + // retrieved test vector from: + // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction + let mut tx = TxLegacy { + to: alloy_primitives::TxKind::Call(address!( + "F0109fC8DF283027b6285cc889F5aA624EaC1F55" + )), + value: U256::from(1_000_000_000), + gas_limit: 2_000_000, + nonce: 0, + gas_price: 21_000_000_000, + input: Default::default(), + chain_id: None, + }; + let sig_none = sign_tx_test(&mut tx, None).await.unwrap(); + + tx.chain_id = Some(1); + let sig_1 = sign_tx_test(&mut tx, None).await.unwrap(); + let expected = "c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa6825".parse().unwrap(); + assert_eq!(sig_1, expected); + assert_ne!(sig_1, sig_none); + + tx.chain_id = Some(2); + let sig_2 = sign_tx_test(&mut tx, None).await.unwrap(); + assert_ne!(sig_2, sig_1); + assert_ne!(sig_2, sig_none); + + // Sets chain ID. + tx.chain_id = None; + let sig_none_none = sign_tx_test(&mut tx, None).await.unwrap(); + assert_eq!(sig_none_none, sig_none); + + tx.chain_id = None; + let sig_none_1 = sign_tx_test(&mut tx, Some(1)).await.unwrap(); + assert_eq!(sig_none_1, sig_1); + + tx.chain_id = None; + let sig_none_2 = sign_tx_test(&mut tx, Some(2)).await.unwrap(); + assert_eq!(sig_none_2, sig_2); + + // Errors on mismatch. + tx.chain_id = Some(2); + let error = sign_tx_test(&mut tx, Some(1)).await.unwrap_err(); + let expected_error = alloy_signer::Error::TransactionChainIdMismatch { signer: 1, tx: 2 }; + assert_eq!(error.to_string(), expected_error.to_string()); + } +} diff --git a/crates/network/src/lib.rs b/crates/network/src/lib.rs index c7042b550d0..6255e840973 100644 --- a/crates/network/src/lib.rs +++ b/crates/network/src/lib.rs @@ -17,19 +17,19 @@ use alloy_eips::eip2718::Eip2718Envelope; use alloy_json_rpc::RpcObject; -use alloy_primitives::B256; - -mod sealed; -pub use sealed::{Sealable, Sealed}; +use alloy_primitives::{Address, B256}; mod transaction; -pub use transaction::{Eip1559Transaction, Signed, Transaction, TxKind}; - -mod receipt; -pub use receipt::Receipt; +pub use transaction::{ + BuilderResult, NetworkSigner, TransactionBuilder, TransactionBuilderError, TxSigner, + TxSignerSync, +}; pub use alloy_eips::eip2718; +mod ethereum; +pub use ethereum::{Ethereum, EthereumSigner}; + /// A list of transactions, either hydrated or hashes. #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[serde(untagged)] @@ -50,18 +50,30 @@ pub struct BlockResponse { transactions: TransactionList, } -/// Captures type info for network-specific RPC requests/responses. -pub trait Network: Sized + Send + Sync + 'static { - #[doc(hidden)] - /// Asserts that this trait can only be implemented on a ZST. - const __ASSERT_ZST: () = { - assert!(std::mem::size_of::() == 0, "Network must be a ZST"); - }; +/// A receipt response. +/// +/// This is distinct from [`TxReceipt`], since this is for JSON-RPC receipts. +/// +/// [`TxReceipt`]: alloy_consensus::TxReceipt +pub trait ReceiptResponse { + /// Address of the created contract, or `None` if the transaction was not a deployment. + fn contract_address(&self) -> Option
; +} +/// Captures type info for network-specific RPC requests/responses. +/// +/// Networks are only containers for types, so it is recommended to use ZSTs for their definition. +// todo: block responses are ethereum only, so we need to include this in here too, or make `Block` +// generic over tx/header type +pub trait Network: Clone + Copy + Sized + Send + Sync + 'static { // -- Consensus types -- /// The network transaction envelope type. type TxEnvelope: Eip2718Envelope; + + /// An enum over the various transaction types. + type UnsignedTx; + /// The network receipt envelope type. type ReceiptEnvelope: Eip2718Envelope; /// The network header type. @@ -70,11 +82,11 @@ pub trait Network: Sized + Send + Sync + 'static { // -- JSON RPC types -- /// The JSON body of a transaction request. - type TransactionRequest: RpcObject + Transaction; // + TransactionBuilder + type TransactionRequest: RpcObject + TransactionBuilder + std::fmt::Debug; /// The JSON body of a transaction response. type TransactionResponse: RpcObject; /// The JSON body of a transaction receipt. - type ReceiptResponse: RpcObject; + type ReceiptResponse: RpcObject + ReceiptResponse; /// The JSON body of a header response, as flattened into /// [`BlockResponse`]. type HeaderResponse: RpcObject; diff --git a/crates/network/src/receipt.rs b/crates/network/src/receipt.rs deleted file mode 100644 index 2648cce6623..00000000000 --- a/crates/network/src/receipt.rs +++ /dev/null @@ -1,23 +0,0 @@ -use alloy_primitives::{Bloom, Log}; - -/// Receipt is the result of a transaction execution. -pub trait Receipt { - /// Returns true if the transaction was successful. - fn success(&self) -> bool; - - /// Returns the bloom filter for the logs in the receipt. This operation - /// may be expensive. - fn bloom(&self) -> Bloom; - - /// Returns the bloom filter for the logs in the receipt, if it is cheap to - /// compute. - fn bloom_cheap(&self) -> Option { - None - } - - /// Returns the cumulative gas used in the block after this transaction was executed. - fn cumulative_gas_used(&self) -> u64; - - /// Returns the logs emitted by this transaction. - fn logs(&self) -> &[Log]; -} diff --git a/crates/network/src/transaction/builder.rs b/crates/network/src/transaction/builder.rs new file mode 100644 index 00000000000..14d1eac6d0f --- /dev/null +++ b/crates/network/src/transaction/builder.rs @@ -0,0 +1,200 @@ +use super::signer::NetworkSigner; +use crate::Network; +use alloy_primitives::{Address, Bytes, ChainId, TxKind, U256, U64}; +use async_trait::async_trait; + +/// Error type for transaction builders. +#[derive(Debug, thiserror::Error)] +pub enum TransactionBuilderError { + /// A required key is missing. + #[error("A required key is missing: {0}")] + MissingKey(&'static str), + + /// Signer cannot produce signature type required for transaction. + #[error("Signer cannot produce signature type required for transaction")] + UnsupportedSignatureType, + + /// Signer error. + #[error(transparent)] + Signer(#[from] alloy_signer::Error), + + /// A custom error. + #[error("{0}")] + Custom(#[source] Box), +} + +impl TransactionBuilderError { + /// Instantiate a custom error. + pub fn custom(e: E) -> Self + where + E: std::error::Error + Send + Sync + 'static, + { + Self::Custom(Box::new(e)) + } +} + +/// [`TransactionBuilder`] result type. +pub type BuilderResult = std::result::Result; + +/// A Transaction builder for a network. +/// +/// Transaction builders are primarily used to construct typed transactions that can be signed with +/// [`TransactionBuilder::build`], or unsigned typed transactions with +/// [`TransactionBuilder::build_unsigned`]. +/// +/// Transaction builders should be able to construct all available transaction types on a given +/// network. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait TransactionBuilder: Default + Sized + Send + Sync + 'static { + /// Get the chain ID for the transaction. + fn chain_id(&self) -> Option; + + /// Set the chain ID for the transaction. + fn set_chain_id(&mut self, chain_id: ChainId); + + /// Builder-pattern method for setting the chain ID. + fn with_chain_id(mut self, chain_id: alloy_primitives::ChainId) -> Self { + self.set_chain_id(chain_id); + self + } + + /// Get the nonce for the transaction. + fn nonce(&self) -> Option; + + /// Set the nonce for the transaction. + fn set_nonce(&mut self, nonce: U64); + + /// Builder-pattern method for setting the nonce. + fn with_nonce(mut self, nonce: U64) -> Self { + self.set_nonce(nonce); + self + } + + /// Get the input data for the transaction. + fn input(&self) -> Option<&Bytes>; + + /// Set the input data for the transaction. + fn set_input(&mut self, input: Bytes); + + /// Builder-pattern method for setting the input data. + fn with_input(mut self, input: Bytes) -> Self { + self.set_input(input); + self + } + + /// Get the sender for the transaction. + fn from(&self) -> Option
; + + /// Set the sender for the transaction. + fn set_from(&mut self, from: Address); + + /// Builder-pattern method for setting the sender. + fn with_from(mut self, from: Address) -> Self { + self.set_from(from); + self + } + + /// Get the recipient for the transaction. + fn to(&self) -> Option; + + /// Set the recipient for the transaction. + fn set_to(&mut self, to: TxKind); + + /// Builder-pattern method for setting the recipient. + fn with_to(mut self, to: TxKind) -> Self { + self.set_to(to); + self + } + + /// Calculates the address that will be created by the transaction, if any. + /// + /// Returns `None` if the transaction is not a contract creation (the `to` field is set), or if + /// the `from` or `nonce` fields are not set. + fn calculate_create_address(&self) -> Option
{ + if !self.to().is_some_and(|to| to.is_create()) { + return None; + } + let from = self.from()?; + let nonce = self.nonce()?; + Some(from.create(nonce.to())) + } + + /// Get the value for the transaction. + fn value(&self) -> Option; + + /// Set the value for the transaction. + fn set_value(&mut self, value: U256); + + /// Builder-pattern method for setting the value. + fn with_value(mut self, value: U256) -> Self { + self.set_value(value); + self + } + + /// Get the legacy gas price for the transaction. + fn gas_price(&self) -> Option; + + /// Set the legacy gas price for the transaction. + fn set_gas_price(&mut self, gas_price: U256); + + /// Builder-pattern method for setting the legacy gas price. + fn with_gas_price(mut self, gas_price: U256) -> Self { + self.set_gas_price(gas_price); + self + } + + /// Get the max fee per gas for the transaction. + fn max_fee_per_gas(&self) -> Option; + + /// Set the max fee per gas for the transaction. + fn set_max_fee_per_gas(&mut self, max_fee_per_gas: U256); + + /// Builder-pattern method for setting max fee per gas . + fn with_max_fee_per_gas(mut self, max_fee_per_gas: U256) -> Self { + self.set_max_fee_per_gas(max_fee_per_gas); + self + } + + /// Get the max priority fee per gas for the transaction. + fn max_priority_fee_per_gas(&self) -> Option; + + /// Set the max priority fee per gas for the transaction. + fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: U256); + + /// Builder-pattern method for setting max priority fee per gas. + fn with_max_priority_fee_per_gas(mut self, max_priority_fee_per_gas: U256) -> Self { + self.set_max_priority_fee_per_gas(max_priority_fee_per_gas); + self + } + + /// Get the max fee per blob gas for the transaction. + fn max_fee_per_blob_gas(&self) -> Option; + + /// Set the max fee per blob gas for the transaction. + fn set_max_fee_per_blob_gas(&mut self, max_fee_per_blob_gas: U256); + + /// Builder-pattern method for setting max fee per blob gas . + fn with_max_fee_per_blob_gas(mut self, max_fee_per_blob_gas: U256) -> Self { + self.set_max_fee_per_blob_gas(max_fee_per_blob_gas); + self + } + + /// Get the gas limit for the transaction. + fn gas_limit(&self) -> Option; + + /// Set the gas limit for the transaction. + fn set_gas_limit(&mut self, gas_limit: U256); + + /// Builder-pattern method for setting the gas limit. + fn with_gas_limit(mut self, gas_limit: U256) -> Self { + self.set_gas_limit(gas_limit); + self + } + + /// Build an unsigned, but typed, transaction. + fn build_unsigned(self) -> BuilderResult; + + /// Build a signed transaction. + async fn build>(self, signer: &S) -> BuilderResult; +} diff --git a/crates/network/src/transaction/common.rs b/crates/network/src/transaction/common.rs deleted file mode 100644 index 45d637a655f..00000000000 --- a/crates/network/src/transaction/common.rs +++ /dev/null @@ -1,91 +0,0 @@ -use alloy_primitives::Address; -use alloy_rlp::{Buf, BufMut, Decodable, Encodable, EMPTY_STRING_CODE}; - -/// The `to` field of a transaction. Either a target address, or empty for a -/// contract creation. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] -pub enum TxKind { - /// A transaction that creates a contract. - #[default] - Create, - /// A transaction that calls a contract or transfer. - Call(Address), -} - -impl From> for TxKind { - /// Creates a `TxKind::Call` with the `Some` address, `None` otherwise. - #[inline] - fn from(value: Option
) -> Self { - match value { - None => TxKind::Create, - Some(addr) => TxKind::Call(addr), - } - } -} - -impl From
for TxKind { - /// Creates a `TxKind::Call` with the given address. - #[inline] - fn from(value: Address) -> Self { - TxKind::Call(value) - } -} - -impl TxKind { - /// Returns the address of the contract that will be called or will receive the transfer. - pub const fn to(self) -> Option
{ - match self { - TxKind::Create => None, - TxKind::Call(to) => Some(to), - } - } - - /// Returns true if the transaction is a contract creation. - #[inline] - pub const fn is_create(self) -> bool { - matches!(self, TxKind::Create) - } - - /// Returns true if the transaction is a contract call. - #[inline] - pub const fn is_call(self) -> bool { - matches!(self, TxKind::Call(_)) - } - - /// Calculates a heuristic for the in-memory size of this object. - #[inline] - pub const fn size(self) -> usize { - std::mem::size_of::() - } -} - -impl Encodable for TxKind { - fn encode(&self, out: &mut dyn BufMut) { - match self { - TxKind::Call(to) => to.encode(out), - TxKind::Create => out.put_u8(EMPTY_STRING_CODE), - } - } - fn length(&self) -> usize { - match self { - TxKind::Call(to) => to.length(), - TxKind::Create => 1, // EMPTY_STRING_CODE is a single byte - } - } -} - -impl Decodable for TxKind { - fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { - if let Some(&first) = buf.first() { - if first == EMPTY_STRING_CODE { - buf.advance(1); - Ok(TxKind::Create) - } else { - let addr =
::decode(buf)?; - Ok(TxKind::Call(addr)) - } - } else { - Err(alloy_rlp::Error::InputTooShort) - } - } -} diff --git a/crates/network/src/transaction/mod.rs b/crates/network/src/transaction/mod.rs index 109b238cba5..b0e3860945d 100644 --- a/crates/network/src/transaction/mod.rs +++ b/crates/network/src/transaction/mod.rs @@ -1,120 +1,5 @@ -use alloy_primitives::{keccak256, Bytes, ChainId, Signature, B256, U256}; -use alloy_rlp::BufMut; +mod builder; +pub use builder::{BuilderResult, TransactionBuilder, TransactionBuilderError}; -mod common; -pub use common::TxKind; - -mod signed; -pub use signed::Signed; - -/// Represents a minimal EVM transaction. -pub trait Transaction: std::any::Any + Send + Sync + 'static { - /// The signature type for this transaction. - /// - /// This is usually [`alloy_primitives::Signature`], however, it may be different for future - /// EIP-2718 transaction types, or in other networks. For example, in Optimism, the deposit - /// transaction signature is the unit type `()`. - type Signature; - - /// RLP-encodes the transaction for signing. - fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut); - - /// Outputs the length of the signature RLP encoding for the transaction. - fn payload_len_for_signature(&self) -> usize; - - /// RLP-encodes the transaction for signing it. Used to calculate `signature_hash`. - /// - /// See [`Transaction::encode_for_signing`]. - fn encoded_for_signing(&self) -> Vec { - let mut buf = Vec::with_capacity(self.payload_len_for_signature()); - self.encode_for_signing(&mut buf); - buf - } - - /// Calculate the signing hash for the transaction. - fn signature_hash(&self) -> B256 { - keccak256(self.encoded_for_signing()) - } - - /// Convert to a signed transaction by adding a signature and computing the - /// hash. - fn into_signed(self, signature: Signature) -> Signed - where - Self: Sized; - - /// Encode with a signature. This encoding is usually RLP, but may be - /// different for future EIP-2718 transaction types. - fn encode_signed(&self, signature: &Signature, out: &mut dyn BufMut); - - /// Decode a signed transaction. This decoding is usually RLP, but may be - /// different for future EIP-2718 transaction types. - /// - /// This MUST be the inverse of [`Transaction::encode_signed`]. - fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> - where - Self: Sized; - - /// Get `data`. - fn input(&self) -> &[u8]; - /// Get `data`. - fn input_mut(&mut self) -> &mut Bytes; - /// Set `data`. - fn set_input(&mut self, data: Bytes); - - /// Get `to`. - fn to(&self) -> TxKind; - /// Set `to`. - fn set_to(&mut self, to: TxKind); - - /// Get `value`. - fn value(&self) -> U256; - /// Set `value`. - fn set_value(&mut self, value: U256); - - /// Get `chain_id`. - fn chain_id(&self) -> Option; - /// Set `chain_id`. - fn set_chain_id(&mut self, chain_id: ChainId); - - /// Get `nonce`. - fn nonce(&self) -> u64; - /// Set `nonce`. - fn set_nonce(&mut self, nonce: u64); - - /// Get `gas_limit`. - fn gas_limit(&self) -> u64; - /// Set `gas_limit`. - fn set_gas_limit(&mut self, limit: u64); - - /// Get `gas_price`. - fn gas_price(&self) -> Option; - /// Set `gas_price`. - fn set_gas_price(&mut self, price: U256); -} - -// TODO: Remove in favor of dyn trait upcasting (TBD, see https://github.com/rust-lang/rust/issues/65991#issuecomment-1903120162) -#[doc(hidden)] -impl dyn Transaction { - pub fn __downcast_ref(&self) -> Option<&T> { - if std::any::Any::type_id(self) == std::any::TypeId::of::() { - unsafe { Some(&*(self as *const _ as *const T)) } - } else { - None - } - } -} - -/// Captures getters and setters common across EIP-1559 transactions across all networks -pub trait Eip1559Transaction: Transaction { - /// Get `max_priority_fee_per_gas`. - #[doc(alias = "max_tip")] - fn max_priority_fee_per_gas(&self) -> U256; - /// Set `max_priority_fee_per_gas`. - #[doc(alias = "set_max_tip")] - fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: U256); - - /// Get `max_fee_per_gas`. - fn max_fee_per_gas(&self) -> U256; - /// Set `max_fee_per_gas`. - fn set_max_fee_per_gas(&mut self, max_fee_per_gas: U256); -} +mod signer; +pub use signer::{NetworkSigner, TxSigner, TxSignerSync}; diff --git a/crates/network/src/transaction/signer.rs b/crates/network/src/transaction/signer.rs new file mode 100644 index 00000000000..8bb06e83496 --- /dev/null +++ b/crates/network/src/transaction/signer.rs @@ -0,0 +1,94 @@ +use crate::Network; +use alloy_consensus::SignableTransaction; +use alloy_signer::{ + k256::ecdsa::{self, signature::hazmat::PrehashSigner, RecoveryId}, + sign_transaction_with_chain_id, Signature, SignerSync, Wallet, +}; +use async_trait::async_trait; + +/// A signer capable of signing any transaction for the given network. +/// +/// Network crate authors should implement this trait on a type capable of signing any transaction +/// (regardless of signature type) on a given network. Signer crate authors should instead implement +/// [`TxSigner`] to signify signing capability for specific signature types. +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait NetworkSigner: Send + Sync { + /// Asynchronously sign an unsigned transaction. + async fn sign_transaction(&self, tx: N::UnsignedTx) -> alloy_signer::Result; +} + +/// Asynchronous transaction signer, capable of signing any [`SignableTransaction`] for the given +/// `Signature` type. +/// +/// A signer should hold an optional [`ChainId`] value, which is used for [EIP-155] replay +/// protection. +/// +/// If `chain_id` is Some, [EIP-155] should be applied to the input transaction in +/// [`sign_transaction`](Self::sign_transaction), and to the resulting signature in all the methods. +/// If `chain_id` is None, [EIP-155] should not be applied. +/// +/// Synchronous signers should implement both this trait and [`TxSignerSync`]. +/// +/// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 +/// [`ChainId`]: alloy_primitives::ChainId +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait TxSigner { + /// Asynchronously sign an unsigned transaction. + async fn sign_transaction( + &self, + tx: &mut dyn SignableTransaction, + ) -> alloy_signer::Result; +} + +/// Synchronous transaction signer, capable of signing any [`SignableTransaction`] for the given +/// `Signature` type. +/// +/// A signer should hold an optional [`ChainId`] value, which is used for [EIP-155] replay +/// protection. +/// +/// If `chain_id` is Some, [EIP-155] should be applied to the input transaction in +/// [`sign_transaction_sync`](Self::sign_transaction_sync), and to the resulting signature in all +/// the methods. If `chain_id` is None, [EIP-155] should not be applied. +/// +/// Synchronous signers should also implement [`TxSigner`], as they are always able to by delegating +/// the asynchronous methods to the synchronous ones. +/// +/// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 +/// [`ChainId`]: alloy_primitives::ChainId +pub trait TxSignerSync { + /// Synchronously sign an unsigned transaction. + fn sign_transaction_sync( + &self, + tx: &mut dyn SignableTransaction, + ) -> alloy_signer::Result; +} + +// todo: these are implemented here because of a potential circular dep +// we should move wallet/yubi etc. into its own crate +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl TxSigner for Wallet +where + D: PrehashSigner<(ecdsa::Signature, RecoveryId)> + Send + Sync, +{ + async fn sign_transaction( + &self, + tx: &mut dyn SignableTransaction, + ) -> alloy_signer::Result { + sign_transaction_with_chain_id!(self, tx, self.sign_hash_sync(&tx.signature_hash())) + } +} + +impl TxSignerSync for Wallet +where + D: PrehashSigner<(ecdsa::Signature, RecoveryId)>, +{ + fn sign_transaction_sync( + &self, + tx: &mut dyn SignableTransaction, + ) -> alloy_signer::Result { + sign_transaction_with_chain_id!(self, tx, self.sign_hash_sync(&tx.signature_hash())) + } +} diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index b97c0958f9b..73405c4bd23 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -28,7 +28,6 @@ futures.workspace = true lru = "0.12" reqwest.workspace = true serde.workspace = true -thiserror.workspace = true tokio = { workspace = true, features = ["sync", "macros"] } tracing.workspace = true @@ -36,8 +35,6 @@ tracing.workspace = true alloy-consensus.workspace = true alloy-node-bindings.workspace = true alloy-rlp.workspace = true +alloy-signer.workspace = true tokio = { workspace = true, features = ["macros"] } tracing-subscriber = { workspace = true, features = ["fmt"] } - -[features] -anvil = [] diff --git a/crates/providers/src/builder.rs b/crates/providers/src/builder.rs index 6765cf2efd8..b86d99a6cb8 100644 --- a/crates/providers/src/builder.rs +++ b/crates/providers/src/builder.rs @@ -1,4 +1,7 @@ -use crate::new::{Provider, RootProvider}; +use crate::{ + new::{Provider, RootProvider}, + SignerLayer, +}; use alloy_network::Network; use alloy_rpc_client::RpcClient; use alloy_transport::Transport; @@ -13,6 +16,22 @@ pub trait ProviderLayer, N: Network, T: Transport + Clone> { fn layer(&self, inner: P) -> Self::Provider; } +/// An identity layer that does nothing. +pub struct Identity; + +impl ProviderLayer for Identity +where + T: Transport + Clone, + N: Network, + P: Provider, +{ + type Provider = P; + + fn layer(&self, inner: P) -> Self::Provider { + inner + } +} + pub struct Stack { inner: Inner, outer: Outer, @@ -54,6 +73,18 @@ pub struct ProviderBuilder { network: PhantomData, } +impl ProviderBuilder { + pub fn new() -> Self { + ProviderBuilder { layer: Identity, network: PhantomData } + } +} + +impl Default for ProviderBuilder { + fn default() -> Self { + Self::new() + } +} + impl ProviderBuilder { /// Add a layer to the stack being built. This is similar to /// [`tower::ServiceBuilder::layer`]. @@ -67,11 +98,20 @@ impl ProviderBuilder { /// /// [`tower::ServiceBuilder::layer`]: https://docs.rs/tower/latest/tower/struct.ServiceBuilder.html#method.layer /// [`tower::ServiceBuilder`]: https://docs.rs/tower/latest/tower/struct.ServiceBuilder.html - pub fn layer(self, layer: Inner) -> ProviderBuilder> { ProviderBuilder { layer: Stack::new(layer, self.layer), network: PhantomData } } + /// Add a signer layer to the stack being built. + /// + /// See [`SignerLayer`]. + pub fn signer(self, signer: S) -> ProviderBuilder, L>> { + ProviderBuilder { + layer: Stack::new(SignerLayer::new(signer), self.layer), + network: PhantomData, + } + } + /// Change the network. /// /// By default, the network is invalid, and contains the unit type `()`. diff --git a/crates/providers/src/chain.rs b/crates/providers/src/chain.rs index a233b2a9894..427260ec359 100644 --- a/crates/providers/src/chain.rs +++ b/crates/providers/src/chain.rs @@ -92,17 +92,26 @@ impl ChainStreamPoller { let mut retries = MAX_RETRIES; for number in self.next_yield..=block_number { debug!(number, "fetching block"); - let block = match provider.get_block_by_number(number, false).await { - Ok(block) => block, + let block = match provider.get_block_by_number(number.into(), false).await { + Ok(Some(block)) => block, Err(RpcError::Transport(err)) if retries > 0 && err.recoverable() => { debug!(number, %err, "failed to fetch block, retrying"); retries -= 1; continue; } + Ok(None) if retries > 0 => { + debug!(number, "failed to fetch block (doesn't exist), retrying"); + retries -= 1; + continue; + } Err(err) => { error!(number, %err, "failed to fetch block"); break 'task; } + Ok(None) => { + error!(number, "failed to fetch block (doesn't exist)"); + break 'task; + } }; self.known_blocks.put(number, block); } diff --git a/crates/providers/src/lib.rs b/crates/providers/src/lib.rs index dab33965640..cdc448d0872 100644 --- a/crates/providers/src/lib.rs +++ b/crates/providers/src/lib.rs @@ -16,11 +16,20 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +use alloy_transport_http::Http; +use reqwest::Client as ReqwestClient; + +/// Type alias for a [`RootProvider`] using the [`Http`] transport. +pub type HttpProvider = RootProvider>; + #[macro_use] extern crate tracing; mod builder; -pub use builder::{ProviderBuilder, ProviderLayer, Stack}; +pub use builder::{Identity, ProviderBuilder, ProviderLayer, Stack}; + +mod signer; +pub use signer::{SignerLayer, SignerProvider}; mod chain; @@ -28,9 +37,8 @@ mod heart; pub use heart::{PendingTransaction, WatchConfig}; pub mod new; -pub use new::{Provider, ProviderRef, RootProvider, WeakProvider}; -pub mod utils; +#[doc(inline)] +pub use new::{AnvilProvider, Provider, ProviderRef, RawProvider, RootProvider, WeakProvider}; -// TODO: remove -pub mod tmp; +pub mod utils; diff --git a/crates/providers/src/new.rs b/crates/providers/src/new.rs index 71b37233e6d..5a572f2aa34 100644 --- a/crates/providers/src/new.rs +++ b/crates/providers/src/new.rs @@ -1,12 +1,24 @@ use crate::{ chain::ChainStreamPoller, heart::{Heartbeat, HeartbeatHandle, PendingTransaction, WatchConfig}, + utils, + utils::EstimatorFunction, +}; +use alloy_network::Network; +use alloy_primitives::{ + hex, Address, BlockHash, BlockNumber, Bytes, StorageKey, StorageValue, TxHash, B256, U256, U64, }; -use alloy_network::{Network, Transaction}; -use alloy_primitives::{hex, Address, BlockNumber, B256, U256, U64}; use alloy_rpc_client::{ClientRef, RpcClient, WeakClient}; -use alloy_rpc_types::Block; +use alloy_rpc_trace_types::{ + geth::{GethDebugTracingOptions, GethTrace}, + parity::LocalizedTransactionTrace, +}; +use alloy_rpc_types::{ + state::StateOverride, AccessListWithGasUsed, Block, BlockId, BlockNumberOrTag, + EIP1186AccountProofResponse, FeeHistory, Filter, Log, SyncStatus, +}; use alloy_transport::{BoxTransport, Transport, TransportErrorKind, TransportResult}; +use serde::{de::DeserializeOwned, Serialize}; use std::{ marker::PhantomData, sync::{Arc, OnceLock, Weak}, @@ -26,7 +38,7 @@ pub struct RootProvider { } impl RootProvider { - pub(crate) fn new(client: RpcClient) -> Self { + pub fn new(client: RpcClient) -> Self { Self { inner: Arc::new(RootProviderInner::new(client)) } } } @@ -70,6 +82,8 @@ impl RootProviderInner { } } +// todo: adjust docs +// todo: reorder /// Provider is parameterized with a network and a transport. The default /// transport is type-erased, but you can do `Provider`. #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] @@ -84,21 +98,18 @@ pub trait Provider: Send + Sync async fn new_pending_transaction(&self, tx_hash: B256) -> TransportResult; - async fn estimate_gas(&self, tx: &N::TransactionRequest) -> TransportResult { - self.client().prepare("eth_estimateGas", (tx,)).await - } - /// Get the last block number available. async fn get_block_number(&self) -> TransportResult { self.client().prepare("eth_blockNumber", ()).await.map(|num: U64| num.to::()) } - /// Get the transaction count for an address. Used for finding the - /// appropriate nonce. - /// - /// TODO: block number/hash/tag - async fn get_transaction_count(&self, address: Address) -> TransportResult { - self.client().prepare("eth_getTransactionCount", (address, "latest")).await + /// Gets the transaction count of the corresponding address. + async fn get_transaction_count( + &self, + address: Address, + tag: Option, + ) -> TransportResult { + self.client().prepare("eth_getTransactionCount", (address, tag.unwrap_or_default())).await } /// Get a block by its number. @@ -106,26 +117,44 @@ pub trait Provider: Send + Sync /// TODO: Network associate async fn get_block_by_number( &self, - number: BlockNumber, + number: BlockNumberOrTag, hydrate: bool, - ) -> TransportResult { + ) -> TransportResult> { self.client().prepare("eth_getBlockByNumber", (number, hydrate)).await } - /// Populate the gas limit for a transaction. - async fn populate_gas(&self, tx: &mut N::TransactionRequest) -> TransportResult<()> { - let gas = self.estimate_gas(&*tx).await?; - if let Ok(gas) = gas.try_into() { - tx.set_gas_limit(gas); - } - Ok(()) + /// Populates the legacy gas price field of the given transaction request. + async fn populate_gas( + &self, + tx: &mut N::TransactionRequest, + block: Option, + ) -> TransportResult<()> { + use alloy_network::TransactionBuilder; + let gas = self.estimate_gas(&*tx, block).await; + + gas.map(|gas| tx.set_gas_limit(gas)) + } + + /// Populates the EIP-1559 gas price fields of the given transaction request. + async fn populate_gas_eip1559( + &self, + tx: &mut N::TransactionRequest, + estimator: Option, + ) -> TransportResult<()> { + use alloy_network::TransactionBuilder; + let gas = self.estimate_eip1559_fees(estimator).await; + + gas.map(|(max_fee_per_gas, max_priority_fee_per_gas)| { + tx.set_max_fee_per_gas(max_fee_per_gas); + tx.set_max_priority_fee_per_gas(max_priority_fee_per_gas); + }) } /// Broadcasts a transaction, returning a [`PendingTransaction`] that resolves once the /// transaction has been confirmed. async fn send_transaction( &self, - tx: &N::TransactionRequest, + tx: N::TransactionRequest, ) -> TransportResult { let tx_hash = self.client().prepare("eth_sendTransaction", (tx,)).await?; self.new_pending_transaction(tx_hash).await @@ -138,8 +167,288 @@ pub trait Provider: Send + Sync let tx_hash = self.client().prepare("eth_sendRawTransaction", (rlp_hex,)).await?; self.new_pending_transaction(tx_hash).await } + + /// Gets the balance of the account at the specified tag, which defaults to latest. + async fn get_balance(&self, address: Address, tag: Option) -> TransportResult { + self.client() + .prepare( + "eth_getBalance", + (address, tag.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))), + ) + .await + } + + /// Gets a block by either its hash, tag, or number, with full transactions or only hashes. + async fn get_block(&self, id: BlockId, full: bool) -> TransportResult> { + match id { + BlockId::Hash(hash) => self.get_block_by_hash(hash.into(), full).await, + BlockId::Number(number) => self.get_block_by_number(number, full).await, + } + } + + /// Gets a block by its [BlockHash], with full transactions or only hashes. + async fn get_block_by_hash( + &self, + hash: BlockHash, + full: bool, + ) -> TransportResult> { + self.client().prepare("eth_getBlockByHash", (hash, full)).await + } + + /// Gets the client version of the chain client(). + async fn get_client_version(&self) -> TransportResult { + self.client().prepare("web3_clientVersion", ()).await + } + + /// Gets the chain ID. + async fn get_chain_id(&self) -> TransportResult { + self.client().prepare("eth_chainId", ()).await + } + + /// Gets the network ID. Same as `eth_chainId`. + async fn get_net_version(&self) -> TransportResult { + self.client().prepare("net_version", ()).await + } + + /// Gets the specified storage value from [Address]. + async fn get_storage_at( + &self, + address: Address, + key: U256, + tag: Option, + ) -> TransportResult { + self.client().prepare("eth_getStorageAt", (address, key, tag.unwrap_or_default())).await + } + + /// Gets the bytecode located at the corresponding [Address]. + async fn get_code_at(&self, address: Address, tag: BlockId) -> TransportResult { + self.client().prepare("eth_getCode", (address, tag)).await + } + + /// Gets a transaction by its [TxHash]. + async fn get_transaction_by_hash( + &self, + hash: TxHash, + ) -> TransportResult { + self.client().prepare("eth_getTransactionByHash", (hash,)).await + } + + /// Retrieves a [`Vec`] with the given [Filter]. + async fn get_logs(&self, filter: Filter) -> TransportResult> { + self.client().prepare("eth_getLogs", (filter,)).await + } + + /// Gets the accounts in the remote node. This is usually empty unless you're using a local + /// node. + async fn get_accounts(&self) -> TransportResult> { + self.client().prepare("eth_accounts", ()).await + } + + /// Gets the current gas price. + async fn get_gas_price(&self) -> TransportResult { + self.client().prepare("eth_gasPrice", ()).await + } + + /// Gets a transaction receipt if it exists, by its [TxHash]. + async fn get_transaction_receipt( + &self, + hash: TxHash, + ) -> TransportResult> { + self.client().prepare("eth_getTransactionReceipt", (hash,)).await + } + + /// Returns a collection of historical gas information [FeeHistory] which + /// can be used to calculate the EIP1559 fields `maxFeePerGas` and `maxPriorityFeePerGas`. + async fn get_fee_history( + &self, + block_count: U256, + last_block: BlockNumberOrTag, + reward_percentiles: &[f64], + ) -> TransportResult { + self.client().prepare("eth_feeHistory", (block_count, last_block, reward_percentiles)).await + } + + /// Gets the selected block [BlockNumberOrTag] receipts. + async fn get_block_receipts( + &self, + block: BlockNumberOrTag, + ) -> TransportResult>> { + self.client().prepare("eth_getBlockReceipts", (block,)).await + } + + /// Gets an uncle block through the tag [BlockId] and index [U64]. + async fn get_uncle(&self, tag: BlockId, idx: U64) -> TransportResult> { + match tag { + BlockId::Hash(hash) => { + self.client().prepare("eth_getUncleByBlockHashAndIndex", (hash, idx)).await + } + BlockId::Number(number) => { + self.client().prepare("eth_getUncleByBlockNumberAndIndex", (number, idx)).await + } + } + } + + /// Gets syncing info. + async fn syncing(&self) -> TransportResult { + self.client().prepare("eth_syncing", ()).await + } + + /// Execute a smart contract call with a transaction request, without publishing a transaction. + async fn call( + &self, + tx: &N::TransactionRequest, + block: Option, + ) -> TransportResult { + self.client().prepare("eth_call", (tx, block.unwrap_or_default())).await + } + + /// Execute a smart contract call with a transaction request and state overrides, without + /// publishing a transaction. + /// + /// # Note + /// + /// Not all client implementations support state overrides. + async fn call_with_overrides( + &self, + tx: &N::TransactionRequest, + block: Option, + state: StateOverride, + ) -> TransportResult { + self.client().prepare("eth_call", (tx, block.unwrap_or_default(), state)).await + } + + /// Estimate the gas needed for a transaction. + async fn estimate_gas( + &self, + tx: &N::TransactionRequest, + block: Option, + ) -> TransportResult { + if let Some(block_id) = block { + self.client().prepare("eth_estimateGas", (tx, block_id)).await + } else { + self.client().prepare("eth_estimateGas", (tx,)).await + } + } + + /// Estimates the EIP1559 `maxFeePerGas` and `maxPriorityFeePerGas` fields. + /// + /// Receives an optional [EstimatorFunction] that can be used to modify + /// how to estimate these fees. + async fn estimate_eip1559_fees( + &self, + estimator: Option, + ) -> TransportResult<(U256, U256)> { + let base_fee_per_gas = match self.get_block_by_number(BlockNumberOrTag::Latest, false).await + { + Ok(Some(block)) => match block.header.base_fee_per_gas { + Some(base_fee_per_gas) => base_fee_per_gas, + None => return Err(TransportErrorKind::custom_str("EIP-1559 not activated")), + }, + + Ok(None) => return Err(TransportErrorKind::custom_str("Latest block not found")), + + Err(err) => return Err(err), + }; + + let fee_history = match self + .get_fee_history( + U256::from(utils::EIP1559_FEE_ESTIMATION_PAST_BLOCKS), + BlockNumberOrTag::Latest, + &[utils::EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE], + ) + .await + { + Ok(fee_history) => fee_history, + Err(err) => return Err(err), + }; + + // use the provided fee estimator function, or fallback to the default implementation. + let (max_fee_per_gas, max_priority_fee_per_gas) = if let Some(es) = estimator { + es(base_fee_per_gas, fee_history.reward.unwrap_or_default()) + } else { + utils::eip1559_default_estimator( + base_fee_per_gas, + fee_history.reward.unwrap_or_default(), + ) + }; + + Ok((max_fee_per_gas, max_priority_fee_per_gas)) + } + + async fn get_proof( + &self, + address: Address, + keys: Vec, + block: Option, + ) -> TransportResult { + self.client().prepare("eth_getProof", (address, keys, block.unwrap_or_default())).await + } + + async fn create_access_list( + &self, + request: &N::TransactionRequest, + block: Option, + ) -> TransportResult { + self.client().prepare("eth_createAccessList", (request, block.unwrap_or_default())).await + } + + // todo: move to extension trait + /// Parity trace transaction. + async fn trace_transaction( + &self, + hash: TxHash, + ) -> TransportResult> { + self.client().prepare("trace_transaction", (hash,)).await + } + + // todo: move to extension trait + async fn debug_trace_transaction( + &self, + hash: TxHash, + trace_options: GethDebugTracingOptions, + ) -> TransportResult { + self.client().prepare("debug_traceTransaction", (hash, trace_options)).await + } + + // todo: move to extension trait + async fn trace_block( + &self, + block: BlockNumberOrTag, + ) -> TransportResult> { + self.client().prepare("trace_block", (block,)).await + } +} + +/// Extension trait for Anvil specific JSON-RPC methods. +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +pub trait AnvilProvider: Provider { + /// Set the bytecode of a given account. + async fn set_code(&self, address: Address, code: &'static str) -> TransportResult<()> { + self.client().prepare("anvil_setCode", (address, code)).await + } +} + +impl AnvilProvider for P where P: Provider {} + +/// Extension trait for raw RPC requests. +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +pub trait RawProvider: Provider { + /// Sends a raw JSON-RPC request. + async fn raw_request(&self, method: &'static str, params: P) -> TransportResult + where + P: Serialize + Send + Sync + Clone, + R: Serialize + DeserializeOwned + Send + Sync + Unpin + 'static, + Self: Sync, + { + let res: R = self.client().prepare(method, ¶ms).await?; + Ok(res) + } } +impl RawProvider for P where P: Provider {} + #[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] impl Provider for RootProvider { @@ -182,182 +491,300 @@ impl Provider for RootProviderInner(dyn Provider); - #[derive(Clone)] - struct TxLegacy(alloy_consensus::TxLegacy); - impl serde::Serialize for TxLegacy { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let tx = &self.0; - TransactionRequest { - from: None, - to: tx.to().to(), - gas_price: tx.gas_price(), - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - max_fee_per_blob_gas: None, - gas: Some(U256::from(tx.gas_limit())), - value: Some(tx.value()), - input: TransactionInput::new(tx.input().to_vec().into()), - nonce: Some(U64::from(tx.nonce())), - chain_id: tx.chain_id().map(U64::from), - access_list: None, - transaction_type: None, - blob_versioned_hashes: None, - sidecar: None, - other: Default::default(), - } - .serialize(serializer) - } + fn init_tracing() { + let _ = tracing_subscriber::fmt::try_init(); } - impl<'de> serde::Deserialize<'de> for TxLegacy { - fn deserialize(_deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - unimplemented!() - } + + fn spawn_anvil() -> (HttpProvider, AnvilInstance) { + let anvil = Anvil::new().spawn(); + let url = anvil.endpoint().parse().unwrap(); + let http = Http::::new(url); + (RootProvider::::new(RpcClient::new(http, true)), anvil) } - #[allow(unused)] - impl alloy_network::Transaction for TxLegacy { - type Signature = (); - fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { - todo!() - } + #[tokio::test] + async fn test_send_tx() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); - fn payload_len_for_signature(&self) -> usize { - todo!() - } + let tx = TransactionRequest { + value: Some(U256::from(100)), + to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), + gas_price: Some(U256::from(20e9)), + gas: Some(U256::from(21000)), + ..Default::default() + }; + let pending_tx = provider.send_transaction(tx).await.expect("failed to send tx"); + let hash1 = pending_tx.tx_hash; + let hash2 = pending_tx.await.expect("failed to await pending tx"); + assert_eq!(hash1, hash2); + } - fn into_signed( - self, - signature: alloy_primitives::Signature, - ) -> alloy_network::Signed - where - Self: Sized, - { - todo!() - } + #[tokio::test] + async fn gets_block_number() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); - fn encode_signed( - &self, - signature: &alloy_primitives::Signature, - out: &mut dyn alloy_primitives::bytes::BufMut, - ) { - todo!() - } + let num = provider.get_block_number().await.unwrap(); + assert_eq!(0, num) + } - fn decode_signed(buf: &mut &[u8]) -> alloy_rlp::Result> - where - Self: Sized, - { - todo!() - } + #[tokio::test] + async fn gets_block_number_with_raw_req() { + use super::RawProvider; - fn input(&self) -> &[u8] { - todo!() - } + init_tracing(); + let (provider, _anvil) = spawn_anvil(); - fn input_mut(&mut self) -> &mut alloy_primitives::Bytes { - todo!() - } + let num: U64 = provider.raw_request("eth_blockNumber", ()).await.unwrap(); + assert_eq!(0, num.to::()) + } - fn set_input(&mut self, data: alloy_primitives::Bytes) { - todo!() - } + #[tokio::test] + async fn gets_transaction_count() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); + + let count = provider + .get_transaction_count( + address!("328375e18E7db8F1CA9d9bA8bF3E9C94ee34136A"), + Some(BlockNumberOrTag::Latest.into()), + ) + .await + .unwrap(); + assert_eq!(count, U256::from(0)); + } - fn to(&self) -> alloy_network::TxKind { - todo!() - } + #[tokio::test] + async fn gets_block_by_hash() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); + + let num = 0; + let tag: BlockNumberOrTag = num.into(); + let block = provider.get_block_by_number(tag, true).await.unwrap().unwrap(); + let hash = block.header.hash.unwrap(); + let block = provider.get_block_by_hash(hash, true).await.unwrap().unwrap(); + assert_eq!(block.header.hash.unwrap(), hash); + } - fn set_to(&mut self, to: alloy_network::TxKind) { - todo!() - } + #[tokio::test] + async fn gets_block_by_hash_with_raw_req() { + use super::RawProvider; - fn value(&self) -> U256 { - todo!() - } + init_tracing(); + let (provider, _anvil) = spawn_anvil(); + + let num = 0; + let tag: BlockNumberOrTag = num.into(); + let block = provider.get_block_by_number(tag, true).await.unwrap().unwrap(); + let hash = block.header.hash.unwrap(); + let block: Block = provider + .raw_request::<(alloy_primitives::FixedBytes<32>, bool), Block>( + "eth_getBlockByHash", + (hash, true), + ) + .await + .unwrap(); + assert_eq!(block.header.hash.unwrap(), hash); + } - fn set_value(&mut self, value: U256) { - todo!() - } + #[tokio::test] + async fn gets_block_by_number_full() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); - fn chain_id(&self) -> Option { - todo!() - } + let num = 0; + let tag: BlockNumberOrTag = num.into(); + let block = provider.get_block_by_number(tag, true).await.unwrap().unwrap(); + assert_eq!(block.header.number.unwrap(), U256::from(num)); + } - fn set_chain_id(&mut self, chain_id: alloy_primitives::ChainId) { - todo!() - } + #[tokio::test] + async fn gets_block_by_number() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); - fn nonce(&self) -> u64 { - todo!() - } + let num = 0; + let tag: BlockNumberOrTag = num.into(); + let block = provider.get_block_by_number(tag, true).await.unwrap().unwrap(); + assert_eq!(block.header.number.unwrap(), U256::from(num)); + } - fn set_nonce(&mut self, nonce: u64) { - todo!() - } + #[tokio::test] + async fn gets_client_version() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); - fn gas_limit(&self) -> u64 { - todo!() - } + let version = provider.get_client_version().await.unwrap(); + assert!(version.contains("anvil")); + } - fn set_gas_limit(&mut self, limit: u64) { - todo!() - } + #[tokio::test] + async fn gets_chain_id() { + let chain_id: u64 = 13371337; + let anvil = Anvil::new().args(["--chain-id", chain_id.to_string().as_str()]).spawn(); + let url = anvil.endpoint().parse().unwrap(); + let http = Http::::new(url); + let provider = RootProvider::::new(RpcClient::new(http, true)); - fn gas_price(&self) -> Option { - todo!() - } + let chain_id = provider.get_chain_id().await.unwrap(); + assert_eq!(chain_id, U64::from(chain_id)); + } - fn set_gas_price(&mut self, price: U256) { - todo!() - } + #[tokio::test] + async fn gets_network_id() { + let chain_id: u64 = 13371337; + let anvil = Anvil::new().args(["--chain-id", chain_id.to_string().as_str()]).spawn(); + let url = anvil.endpoint().parse().unwrap(); + let http = Http::::new(url); + let provider = RootProvider::::new(RpcClient::new(http, true)); + + let chain_id = provider.get_net_version().await.unwrap(); + assert_eq!(chain_id, U64::from(chain_id)); } - struct TmpNetwork; - impl Network for TmpNetwork { - type TxEnvelope = alloy_consensus::TxEnvelope; - type ReceiptEnvelope = alloy_consensus::ReceiptEnvelope; - type Header = (); - type TransactionRequest = TxLegacy; - type TransactionResponse = (); - type ReceiptResponse = (); - type HeaderResponse = (); + #[tokio::test] + #[cfg(feature = "anvil")] + async fn gets_code_at() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); + + // Set the code + let addr = alloy_primitives::Address::with_last_byte(16); + provider.set_code(addr, "0xbeef").await.unwrap(); + let _code = provider + .get_code_at(addr, BlockId::Number(alloy_rpc_types::BlockNumberOrTag::Latest)) + .await + .unwrap(); } - fn init_tracing() { - let _ = tracing_subscriber::fmt::try_init(); + #[tokio::test] + async fn gets_storage_at() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); + + let addr = alloy_primitives::Address::with_last_byte(16); + let storage = provider.get_storage_at(addr, U256::ZERO, None).await.unwrap(); + assert_eq!(storage, U256::ZERO); } #[tokio::test] - async fn test_send_tx() { + #[ignore] + async fn gets_transaction_by_hash() { init_tracing(); + let (provider, _anvil) = spawn_anvil(); + + let tx = provider + .get_transaction_by_hash(b256!( + "5c03fab9114ceb98994b43892ade87ddfd9ae7e8f293935c3bd29d435dc9fd95" + )) + .await + .unwrap(); + assert_eq!( + tx.block_hash.unwrap(), + b256!("b20e6f35d4b46b3c4cd72152faec7143da851a0dc281d390bdd50f58bfbdb5d3") + ); + assert_eq!(tx.block_number.unwrap(), U256::from(4571819)); + } - let anvil = alloy_node_bindings::Anvil::new().spawn(); - let url = anvil.endpoint().parse().unwrap(); - let http = Http::::new(url); - let provider = RootProvider::::new(RpcClient::new(http, true)); + #[tokio::test] + #[ignore] + async fn gets_logs() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); + + let filter = Filter::new() + .at_block_hash(b256!( + "b20e6f35d4b46b3c4cd72152faec7143da851a0dc281d390bdd50f58bfbdb5d3" + )) + .event_signature(b256!( + "e1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c" + )); + let logs = provider.get_logs(filter).await.unwrap(); + assert_eq!(logs.len(), 1); + } - let tx = alloy_consensus::TxLegacy { - value: U256::from(100), - to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), - gas_price: 20e9 as u128, - gas_limit: 21000, - ..Default::default() - }; - let pending_tx = provider.send_transaction(&TxLegacy(tx)).await.expect("failed to send tx"); - let hash1 = pending_tx.tx_hash; - let hash2 = pending_tx.await.expect("failed to await pending tx"); - assert_eq!(hash1, hash2); + #[tokio::test] + #[ignore] + async fn gets_tx_receipt() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); + + let receipt = provider + .get_transaction_receipt(b256!( + "5c03fab9114ceb98994b43892ade87ddfd9ae7e8f293935c3bd29d435dc9fd95" + )) + .await + .unwrap(); + assert!(receipt.is_some()); + let receipt = receipt.unwrap(); + assert_eq!( + receipt.transaction_hash.unwrap(), + b256!("5c03fab9114ceb98994b43892ade87ddfd9ae7e8f293935c3bd29d435dc9fd95") + ); + } + + #[tokio::test] + async fn gets_fee_history() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); + + let block_number = provider.get_block_number().await.unwrap(); + let fee_history = provider + .get_fee_history( + U256::from(utils::EIP1559_FEE_ESTIMATION_PAST_BLOCKS), + BlockNumberOrTag::Number(block_number), + &[utils::EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE], + ) + .await + .unwrap(); + assert_eq!(fee_history.oldest_block, U256::ZERO); + } + + #[tokio::test] + #[ignore] // Anvil has yet to implement the `eth_getBlockReceipts` method. + async fn gets_block_receipts() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); + + let receipts = provider.get_block_receipts(BlockNumberOrTag::Latest).await.unwrap(); + assert!(receipts.is_some()); + } + + #[tokio::test] + async fn gets_block_traces() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); + + let traces = provider.trace_block(BlockNumberOrTag::Latest).await.unwrap(); + assert_eq!(traces.len(), 0); + } + + #[tokio::test] + async fn sends_raw_transaction() { + init_tracing(); + let (provider, _anvil) = spawn_anvil(); + + let pending = provider + .send_raw_transaction( + // Transfer 1 ETH from default EOA address to the Genesis address. + bytes!("f865808477359400825208940000000000000000000000000000000000000000018082f4f5a00505e227c1c636c76fac55795db1a40a4d24840d81b40d2fe0cc85767f6bd202a01e91b437099a8a90234ac5af3cb7ca4fb1432e133f75f9a91678eaf5f487c74b").as_ref() + ) + .await.unwrap(); + assert_eq!( + pending.tx_hash().to_string(), + "0x9dae5cf33694a02e8a7d5de3fe31e9d05ca0ba6e9180efac4ab20a06c9e598a3" + ); } } diff --git a/crates/providers/src/signer.rs b/crates/providers/src/signer.rs new file mode 100644 index 00000000000..cb2ff1d4e4e --- /dev/null +++ b/crates/providers/src/signer.rs @@ -0,0 +1,151 @@ +use crate::{PendingTransaction, Provider, ProviderLayer}; +use alloy_network::{eip2718::Encodable2718, Network, NetworkSigner, TransactionBuilder}; +use alloy_primitives::B256; +use alloy_rpc_client::{ClientRef, WeakClient}; +use alloy_transport::{Transport, TransportErrorKind, TransportResult}; +use async_trait::async_trait; +use std::marker::PhantomData; + +/// A layer that signs transactions locally. +/// +/// The layer uses a [`NetworkSigner`] to sign transactions sent using +/// [`Provider::send_transaction`] locally before passing them to the node with +/// [`Provider::send_raw_transaction`]. +/// +/// If you have other layers that depend on [`Provider::send_transaction`] being invoked, add those +/// first. +/// +/// # Example +/// +/// ```rs +/// # async fn test>(transport: T, signer: S) { +/// let provider = ProviderBuilder::<_, Ethereum>::new() +/// .layer(SignerLayer::new(EthereumSigner::from(signer))) +/// .network::() +/// .provider(RootProvider::new(transport)); +/// +/// provider.send_transaction(TransactionRequest::default()).await; +/// # } +/// ``` +pub struct SignerLayer { + signer: S, +} + +impl SignerLayer { + /// Creates a new signing layer with the given signer. + pub fn new(signer: S) -> Self { + Self { signer } + } +} + +impl ProviderLayer for SignerLayer +where + P: Provider, + N: Network, + T: Transport + Clone, + S: NetworkSigner + Clone, +{ + type Provider = SignerProvider; + + fn layer(&self, inner: P) -> Self::Provider { + SignerProvider { inner, signer: self.signer.clone(), _phantom: PhantomData } + } +} + +/// A locally-signing provider. +/// +/// Signs transactions locally using a [`NetworkSigner`] +/// +/// # Note +/// +/// You cannot construct this provider directly. Use [`ProviderBuilder`] with a [`SignerLayer`]. +/// +/// [`ProviderBuilder`]: crate::ProviderBuilder +pub struct SignerProvider +where + N: Network, + T: Transport + Clone, + P: Provider, +{ + inner: P, + signer: S, + _phantom: PhantomData<(N, T)>, +} + +#[async_trait] +impl Provider for SignerProvider +where + N: Network, + T: Transport + Clone, + P: Provider, + S: NetworkSigner, +{ + fn client(&self) -> ClientRef<'_, T> { + self.inner.client() + } + + fn weak_client(&self) -> WeakClient { + self.inner.weak_client() + } + + async fn new_pending_transaction(&self, tx_hash: B256) -> TransportResult { + self.inner.new_pending_transaction(tx_hash).await + } + + async fn send_transaction( + &self, + tx: N::TransactionRequest, + ) -> TransportResult { + let envelope = tx.build(&self.signer).await.map_err(TransportErrorKind::custom)?; + let rlp = envelope.encoded_2718(); + + self.inner.send_raw_transaction(&rlp).await + } +} + +#[cfg(test)] +mod tests { + use crate::{Provider, ProviderBuilder, RootProvider}; + use alloy_network::{Ethereum, EthereumSigner}; + use alloy_node_bindings::Anvil; + use alloy_primitives::{address, U256, U64}; + use alloy_rpc_client::RpcClient; + use alloy_rpc_types::TransactionRequest; + use alloy_transport_http::Http; + use reqwest::Client; + + #[tokio::test] + async fn poc() { + let anvil = Anvil::new().spawn(); + let url = anvil.endpoint().parse().unwrap(); + let http = Http::::new(url); + + let wallet = alloy_signer::Wallet::from(anvil.keys()[0].clone()); + + // can we somehow remove the need for <_, Ethereum>? we NEED to call .network + // note: we need to 1) add <_, Ethereum> 2) layer things, and then 3) call .network before + // we can call provider + let provider = ProviderBuilder::<_, Ethereum>::new() + .signer(EthereumSigner::from(wallet)) + .network::() + .provider(RootProvider::new(RpcClient::new(http, true))); + + let tx = TransactionRequest { + nonce: Some(U64::from(0)), + value: Some(U256::from(100)), + to: address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045").into(), + gas_price: Some(U256::from(20e9)), + gas: Some(U256::from(21000)), + ..Default::default() + }; + + let pending = provider.send_transaction(tx).await.unwrap(); + let local_hash = pending.tx_hash; + let node_hash = pending.await.unwrap(); + assert_eq!(local_hash, node_hash); + assert_eq!( + node_hash.to_string(), + "0xeb56033eab0279c6e9b685a5ec55ea0ff8d06056b62b7f36974898d4fbb57e64" + ); + } +} diff --git a/crates/providers/src/tmp.rs b/crates/providers/src/tmp.rs deleted file mode 100644 index 25c054e52dd..00000000000 --- a/crates/providers/src/tmp.rs +++ /dev/null @@ -1,790 +0,0 @@ -//! Alloy main Provider abstraction. - -use crate::utils::{self, EstimatorFunction}; -use alloy_primitives::{Address, BlockHash, Bytes, StorageKey, StorageValue, TxHash, U256, U64}; -use alloy_rpc_client::{ClientBuilder, RpcClient}; -use alloy_rpc_trace_types::{ - geth::{GethDebugTracingOptions, GethTrace}, - parity::LocalizedTransactionTrace, -}; -use alloy_rpc_types::{ - request::TransactionRequest, state::StateOverride, AccessListWithGasUsed, Block, BlockId, - BlockNumberOrTag, EIP1186AccountProofResponse, FeeHistory, Filter, Log, SyncStatus, - Transaction, TransactionReceipt, -}; -use alloy_transport::{BoxTransport, Transport, TransportErrorKind, TransportResult}; -use alloy_transport_http::Http; -use auto_impl::auto_impl; -use reqwest::Client; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use thiserror::Error; - -#[derive(Debug, Error, Serialize, Deserialize)] -pub enum ClientError { - #[error("Could not parse URL")] - ParseError, - #[error("Unsupported Tag")] - UnsupportedBlockIdError, -} - -/// Type alias for a [`Provider`] using the [`Http`] transport. -pub type HttpProvider = Provider>; - -/// An abstract provider for interacting with the [Ethereum JSON RPC -/// API](https://github.com/ethereum/wiki/wiki/JSON-RPC). Must be instantiated -/// with a transport which implements the [Transport] trait. -#[derive(Debug, Clone)] -pub struct Provider { - inner: RpcClient, - from: Option
, -} - -/// Temporary Provider trait to be used until the new Provider trait with -/// the Network abstraction is stable. -/// Once the new Provider trait is stable, this trait will be removed. -#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] -#[auto_impl(&, &mut, Rc, Arc, Box)] -pub trait TempProvider: Send + Sync { - /// Gets the transaction count of the corresponding address. - async fn get_transaction_count( - &self, - address: Address, - tag: Option, - ) -> TransportResult; - - /// Gets the last block number available. - async fn get_block_number(&self) -> TransportResult; - - /// Gets the balance of the account at the specified tag, which defaults to latest. - async fn get_balance(&self, address: Address, tag: Option) -> TransportResult; - - /// Gets a block by either its hash, tag, or number, with full transactions or only hashes. - async fn get_block(&self, id: BlockId, full: bool) -> TransportResult> { - match id { - BlockId::Hash(hash) => self.get_block_by_hash(hash.into(), full).await, - BlockId::Number(number) => self.get_block_by_number(number, full).await, - } - } - - /// Gets a block by its [BlockHash], with full transactions or only hashes. - async fn get_block_by_hash( - &self, - hash: BlockHash, - full: bool, - ) -> TransportResult>; - - /// Gets a block by [BlockNumberOrTag], with full transactions or only hashes. - async fn get_block_by_number( - &self, - number: BlockNumberOrTag, - full: bool, - ) -> TransportResult>; - - /// Gets the client version of the chain client. - async fn get_client_version(&self) -> TransportResult; - - /// Gets the chain ID. - async fn get_chain_id(&self) -> TransportResult; - - /// Gets the network ID. Same as `eth_chainId`. - async fn get_net_version(&self) -> TransportResult; - - /// Gets the specified storage value from [Address]. - async fn get_storage_at( - &self, - address: Address, - key: U256, - tag: Option, - ) -> TransportResult; - - /// Gets the bytecode located at the corresponding [Address]. - async fn get_code_at(&self, address: Address, tag: Option) -> TransportResult; - - /// Gets a [Transaction] by its [TxHash]. - async fn get_transaction_by_hash(&self, hash: TxHash) -> TransportResult; - - /// Retrieves a [`Vec`] with the given [Filter]. - async fn get_logs(&self, filter: Filter) -> TransportResult>; - - /// Gets the accounts in the remote node. This is usually empty unless you're using a local - /// node. - async fn get_accounts(&self) -> TransportResult>; - - /// Gets the current gas price. - async fn get_gas_price(&self) -> TransportResult; - - /// Gets a [TransactionReceipt] if it exists, by its [TxHash]. - async fn get_transaction_receipt( - &self, - hash: TxHash, - ) -> TransportResult>; - - /// Returns a collection of historical gas information [FeeHistory] which - /// can be used to calculate the EIP1559 fields `maxFeePerGas` and `maxPriorityFeePerGas`. - async fn get_fee_history( - &self, - block_count: U256, - last_block: BlockNumberOrTag, - reward_percentiles: &[f64], - ) -> TransportResult; - - /// Gets the selected block [BlockNumberOrTag] receipts. - async fn get_block_receipts( - &self, - block: BlockNumberOrTag, - ) -> TransportResult>>; - - /// Gets an uncle block through the tag [BlockId] and index [U64]. - async fn get_uncle(&self, tag: BlockId, idx: U64) -> TransportResult>; - - /// Gets syncing info. - async fn syncing(&self) -> TransportResult; - - /// Execute a smart contract call with [TransactionRequest] without publishing a transaction. - async fn call(&self, tx: TransactionRequest, block: Option) -> TransportResult; - - /// Execute a smart contract call with [TransactionRequest] and state overrides, without - /// publishing a transaction. - /// - /// # Note - /// - /// Not all client implementations support state overrides. - async fn call_with_overrides( - &self, - tx: TransactionRequest, - block: Option, - state: StateOverride, - ) -> TransportResult; - - /// Estimate the gas needed for a transaction. - async fn estimate_gas( - &self, - tx: TransactionRequest, - block: Option, - ) -> TransportResult; - - /// Sends an already-signed transaction. - async fn send_raw_transaction(&self, tx: Bytes) -> TransportResult; - - /// Estimates the EIP1559 `maxFeePerGas` and `maxPriorityFeePerGas` fields. - /// Receives an optional [EstimatorFunction] that can be used to modify - /// how to estimate these fees. - async fn estimate_eip1559_fees( - &self, - estimator: Option, - ) -> TransportResult<(U256, U256)>; - - #[cfg(feature = "anvil")] - async fn set_code(&self, address: Address, code: &'static str) -> TransportResult<()>; - - async fn get_proof( - &self, - address: Address, - keys: Vec, - block: Option, - ) -> TransportResult; - - async fn create_access_list( - &self, - request: TransactionRequest, - block: Option, - ) -> TransportResult; - - /// Parity trace transaction. - async fn trace_transaction( - &self, - hash: TxHash, - ) -> TransportResult>; - - async fn debug_trace_transaction( - &self, - hash: TxHash, - trace_options: GethDebugTracingOptions, - ) -> TransportResult; - - async fn trace_block( - &self, - block: BlockNumberOrTag, - ) -> TransportResult>; - - async fn raw_request(&self, method: &'static str, params: P) -> TransportResult - where - P: Serialize + Send + Sync + Clone, - R: Serialize + DeserializeOwned + Send + Sync + Unpin + 'static, - Self: Sync; -} - -impl Provider { - pub fn new(transport: T) -> Self { - Self { - // todo(onbjerg): do we just default to false - inner: RpcClient::new(transport, false), - from: None, - } - } - - pub fn new_with_client(client: RpcClient) -> Self { - Self { inner: client, from: None } - } - - pub fn with_sender(mut self, from: Address) -> Self { - self.from = Some(from); - self - } - - pub fn inner(&self) -> &RpcClient { - &self.inner - } -} - -// todo: validate usage of BlockId vs BlockNumberOrTag vs Option etc. -// Simple JSON-RPC bindings. -// In the future, this will be replaced by a Provider trait, -// but as the interface is not stable yet, we define the bindings ourselves -// until we can use the trait and the client abstraction that will use it. -#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] -impl TempProvider for Provider { - /// Gets the transaction count of the corresponding address. - async fn get_transaction_count( - &self, - address: Address, - tag: Option, - ) -> TransportResult { - self.inner.prepare("eth_getTransactionCount", (address, tag.unwrap_or_default())).await - } - - /// Gets the last block number available. - /// Gets the last block number available. - async fn get_block_number(&self) -> TransportResult { - self.inner.prepare("eth_blockNumber", ()).await.map(|num: U64| num.to::()) - } - - /// Gets the balance of the account at the specified tag, which defaults to latest. - async fn get_balance(&self, address: Address, tag: Option) -> TransportResult { - self.inner - .prepare( - "eth_getBalance", - (address, tag.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest))), - ) - .await - } - - /// Gets a block by its [BlockHash], with full transactions or only hashes. - async fn get_block_by_hash( - &self, - hash: BlockHash, - full: bool, - ) -> TransportResult> { - self.inner.prepare("eth_getBlockByHash", (hash, full)).await - } - - /// Gets a block by [BlockNumberOrTag], with full transactions or only hashes. - async fn get_block_by_number( - &self, - number: BlockNumberOrTag, - full: bool, - ) -> TransportResult> { - self.inner.prepare("eth_getBlockByNumber", (number, full)).await - } - - /// Gets the client version of the chain client. - async fn get_client_version(&self) -> TransportResult { - self.inner.prepare("web3_clientVersion", ()).await - } - - /// Gets the chain ID. - async fn get_chain_id(&self) -> TransportResult { - self.inner.prepare("eth_chainId", ()).await - } - - async fn get_net_version(&self) -> TransportResult { - self.inner.prepare("net_version", ()).await - } - - /// Gets the specified storage value from [Address]. - async fn get_storage_at( - &self, - address: Address, - key: U256, - tag: Option, - ) -> TransportResult { - self.inner.prepare("eth_getStorageAt", (address, key, tag.unwrap_or_default())).await - } - - /// Gets the bytecode located at the corresponding [Address]. - async fn get_code_at(&self, address: Address, tag: Option) -> TransportResult { - self.inner.prepare("eth_getCode", (address, tag.unwrap_or_default())).await - } - - /// Gets a [Transaction] by its [TxHash]. - async fn get_transaction_by_hash(&self, hash: TxHash) -> TransportResult { - self.inner.prepare("eth_getTransactionByHash", (hash,)).await - } - - /// Retrieves a [`Vec`] with the given [Filter]. - async fn get_logs(&self, filter: Filter) -> TransportResult> { - self.inner.prepare("eth_getLogs", (filter,)).await - } - - /// Gets the accounts in the remote node. This is usually empty unless you're using a local - /// node. - async fn get_accounts(&self) -> TransportResult> { - self.inner.prepare("eth_accounts", ()).await - } - - /// Gets the current gas price. - async fn get_gas_price(&self) -> TransportResult { - self.inner.prepare("eth_gasPrice", ()).await - } - - /// Gets a [TransactionReceipt] if it exists, by its [TxHash]. - async fn get_transaction_receipt( - &self, - hash: TxHash, - ) -> TransportResult> { - self.inner.prepare("eth_getTransactionReceipt", (hash,)).await - } - - /// Returns a collection of historical gas information [FeeHistory] which - /// can be used to calculate the EIP1559 fields `maxFeePerGas` and `maxPriorityFeePerGas`. - async fn get_fee_history( - &self, - block_count: U256, - last_block: BlockNumberOrTag, - reward_percentiles: &[f64], - ) -> TransportResult { - self.inner.prepare("eth_feeHistory", (block_count, last_block, reward_percentiles)).await - } - - /// Gets the selected block [BlockNumberOrTag] receipts. - async fn get_block_receipts( - &self, - block: BlockNumberOrTag, - ) -> TransportResult>> { - self.inner.prepare("eth_getBlockReceipts", (block,)).await - } - - /// Gets an uncle block through the tag [BlockId] and index [U64]. - async fn get_uncle(&self, tag: BlockId, idx: U64) -> TransportResult> { - match tag { - BlockId::Hash(hash) => { - self.inner.prepare("eth_getUncleByBlockHashAndIndex", (hash, idx)).await - } - BlockId::Number(number) => { - self.inner.prepare("eth_getUncleByBlockNumberAndIndex", (number, idx)).await - } - } - } - - /// Gets syncing info. - async fn syncing(&self) -> TransportResult { - self.inner.prepare("eth_syncing", ()).await - } - - /// Execute a smart contract call with [TransactionRequest] without publishing a transaction. - async fn call(&self, tx: TransactionRequest, block: Option) -> TransportResult { - self.inner.prepare("eth_call", (tx, block.unwrap_or_default())).await - } - - /// Execute a smart contract call with [TransactionRequest] and state overrides, without - /// publishing a transaction. - /// - /// # Note - /// - /// Not all client implementations support state overrides. - async fn call_with_overrides( - &self, - tx: TransactionRequest, - block: Option, - state: StateOverride, - ) -> TransportResult { - self.inner.prepare("eth_call", (tx, block.unwrap_or_default(), state)).await - } - - /// Estimate the gas needed for a transaction. - async fn estimate_gas( - &self, - tx: TransactionRequest, - block: Option, - ) -> TransportResult { - if let Some(block_id) = block { - self.inner.prepare("eth_estimateGas", (tx, block_id)).await - } else { - self.inner.prepare("eth_estimateGas", (tx,)).await - } - } - - /// Sends an already-signed transaction. - async fn send_raw_transaction(&self, tx: Bytes) -> TransportResult { - self.inner.prepare("eth_sendRawTransaction", (tx,)).await - } - - /// Estimates the EIP1559 `maxFeePerGas` and `maxPriorityFeePerGas` fields. - /// Receives an optional [EstimatorFunction] that can be used to modify - /// how to estimate these fees. - async fn estimate_eip1559_fees( - &self, - estimator: Option, - ) -> TransportResult<(U256, U256)> { - let base_fee_per_gas = match self.get_block_by_number(BlockNumberOrTag::Latest, false).await - { - Ok(Some(block)) => match block.header.base_fee_per_gas { - Some(base_fee_per_gas) => base_fee_per_gas, - None => return Err(TransportErrorKind::custom_str("EIP-1559 not activated")), - }, - - Ok(None) => return Err(TransportErrorKind::custom_str("Latest block not found")), - - Err(err) => return Err(err), - }; - - let fee_history = match self - .get_fee_history( - U256::from(utils::EIP1559_FEE_ESTIMATION_PAST_BLOCKS), - BlockNumberOrTag::Latest, - &[utils::EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE], - ) - .await - { - Ok(fee_history) => fee_history, - Err(err) => return Err(err), - }; - - // use the provided fee estimator function, or fallback to the default implementation. - let (max_fee_per_gas, max_priority_fee_per_gas) = if let Some(es) = estimator { - es(base_fee_per_gas, fee_history.reward.unwrap_or_default()) - } else { - utils::eip1559_default_estimator( - base_fee_per_gas, - fee_history.reward.unwrap_or_default(), - ) - }; - - Ok((max_fee_per_gas, max_priority_fee_per_gas)) - } - - async fn get_proof( - &self, - address: Address, - keys: Vec, - block: Option, - ) -> TransportResult { - self.inner.prepare("eth_getProof", (address, keys, block.unwrap_or_default())).await - } - - async fn create_access_list( - &self, - request: TransactionRequest, - block: Option, - ) -> TransportResult { - self.inner.prepare("eth_createAccessList", (request, block.unwrap_or_default())).await - } - - /// Parity trace transaction. - async fn trace_transaction( - &self, - hash: TxHash, - ) -> TransportResult> { - self.inner.prepare("trace_transaction", (hash,)).await - } - - async fn debug_trace_transaction( - &self, - hash: TxHash, - trace_options: GethDebugTracingOptions, - ) -> TransportResult { - self.inner.prepare("debug_traceTransaction", (hash, trace_options)).await - } - - async fn trace_block( - &self, - block: BlockNumberOrTag, - ) -> TransportResult> { - self.inner.prepare("trace_block", (block,)).await - } - - /// Sends a raw request with the methods and params specified to the internal connection, - /// and returns the result. - async fn raw_request(&self, method: &'static str, params: P) -> TransportResult - where - P: Serialize + Send + Sync + Clone, - R: Serialize + DeserializeOwned + Send + Sync + Unpin + 'static, - { - let res: R = self.inner.prepare(method, ¶ms).await?; - Ok(res) - } - - #[cfg(feature = "anvil")] - async fn set_code(&self, address: Address, code: &'static str) -> TransportResult<()> { - self.inner.prepare("anvil_setCode", (address, code)).await - } -} - -impl TryFrom<&str> for Provider> { - type Error = ClientError; - - fn try_from(url: &str) -> Result { - let url = url.parse().map_err(|_e| ClientError::ParseError)?; - let inner = ClientBuilder::default().reqwest_http(url); - - Ok(Self { inner, from: None }) - } -} - -impl TryFrom for Provider> { - type Error = ClientError; - - fn try_from(value: String) -> Result { - Provider::try_from(value.as_str()) - } -} - -impl<'a> TryFrom<&'a String> for Provider> { - type Error = ClientError; - - fn try_from(value: &'a String) -> Result { - Provider::try_from(value.as_str()) - } -} - -#[cfg(test)] -mod tests { - use crate::{ - tmp::{Provider, TempProvider}, - utils, - }; - use alloy_node_bindings::Anvil; - use alloy_primitives::{address, b256, bytes, U256, U64}; - use alloy_rpc_types::{Block, BlockNumberOrTag, Filter}; - - #[tokio::test] - async fn gets_block_number() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let num = provider.get_block_number().await.unwrap(); - assert_eq!(0, num) - } - - #[tokio::test] - async fn gets_block_number_with_raw_req() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let num: U64 = provider.raw_request("eth_blockNumber", ()).await.unwrap(); - assert_eq!(0, num.to::()) - } - - #[tokio::test] - async fn gets_transaction_count() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let count = provider - .get_transaction_count( - address!("328375e18E7db8F1CA9d9bA8bF3E9C94ee34136A"), - Some(BlockNumberOrTag::Latest.into()), - ) - .await - .unwrap(); - assert_eq!(count, U256::from(0)); - } - - #[tokio::test] - async fn gets_block_by_hash() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let num = 0; - let tag: BlockNumberOrTag = num.into(); - let block = provider.get_block_by_number(tag, true).await.unwrap().unwrap(); - let hash = block.header.hash.unwrap(); - let block = provider.get_block_by_hash(hash, true).await.unwrap().unwrap(); - assert_eq!(block.header.hash.unwrap(), hash); - } - - #[tokio::test] - async fn gets_block_by_hash_with_raw_req() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let num = 0; - let tag: BlockNumberOrTag = num.into(); - let block = provider.get_block_by_number(tag, true).await.unwrap().unwrap(); - let hash = block.header.hash.unwrap(); - let block: Block = provider - .raw_request::<(alloy_primitives::FixedBytes<32>, bool), Block>( - "eth_getBlockByHash", - (hash, true), - ) - .await - .unwrap(); - assert_eq!(block.header.hash.unwrap(), hash); - } - - #[tokio::test] - async fn gets_block_by_number_full() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let num = 0; - let tag: BlockNumberOrTag = num.into(); - let block = provider.get_block_by_number(tag, true).await.unwrap().unwrap(); - assert_eq!(block.header.number.unwrap(), U256::from(num)); - } - - #[tokio::test] - async fn gets_block_by_number() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let num = 0; - let tag: BlockNumberOrTag = num.into(); - let block = provider.get_block_by_number(tag, true).await.unwrap().unwrap(); - assert_eq!(block.header.number.unwrap(), U256::from(num)); - } - - #[tokio::test] - async fn gets_client_version() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let version = provider.get_client_version().await.unwrap(); - assert!(version.contains("anvil")); - } - - #[tokio::test] - async fn gets_chain_id() { - let chain_id: u64 = 13371337; - let anvil = Anvil::new().args(["--chain-id", chain_id.to_string().as_str()]).spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let chain_id = provider.get_chain_id().await.unwrap(); - assert_eq!(chain_id, U64::from(chain_id)); - } - - #[tokio::test] - async fn gets_network_id() { - let chain_id: u64 = 13371337; - let anvil = Anvil::new().args(["--chain-id", chain_id.to_string().as_str()]).spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let chain_id = provider.get_net_version().await.unwrap(); - assert_eq!(chain_id, U64::from(chain_id)); - } - - #[tokio::test] - #[cfg(feature = "anvil")] - async fn gets_code_at() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - // Set the code - let addr = alloy_primitives::Address::with_last_byte(16); - provider.set_code(addr, "0xbeef").await.unwrap(); - let code = provider.get_code_at(addr, None).await.unwrap(); - assert_eq!(code, bytes!("beef")); - } - - #[tokio::test] - async fn gets_storage_at() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let addr = alloy_primitives::Address::with_last_byte(16); - let storage = provider.get_storage_at(addr, U256::ZERO, None).await.unwrap(); - assert_eq!(storage, U256::ZERO); - } - - #[tokio::test] - #[ignore] - async fn gets_transaction_by_hash() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let tx = provider - .get_transaction_by_hash(b256!( - "5c03fab9114ceb98994b43892ade87ddfd9ae7e8f293935c3bd29d435dc9fd95" - )) - .await - .unwrap(); - assert_eq!( - tx.block_hash.unwrap(), - b256!("b20e6f35d4b46b3c4cd72152faec7143da851a0dc281d390bdd50f58bfbdb5d3") - ); - assert_eq!(tx.block_number.unwrap(), U256::from(4571819)); - } - - #[tokio::test] - #[ignore] - async fn gets_logs() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let filter = Filter::new() - .at_block_hash(b256!( - "b20e6f35d4b46b3c4cd72152faec7143da851a0dc281d390bdd50f58bfbdb5d3" - )) - .event_signature(b256!( - "e1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c" - )); - let logs = provider.get_logs(filter).await.unwrap(); - assert_eq!(logs.len(), 1); - } - - #[tokio::test] - #[ignore] - async fn gets_tx_receipt() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let receipt = provider - .get_transaction_receipt(b256!( - "5c03fab9114ceb98994b43892ade87ddfd9ae7e8f293935c3bd29d435dc9fd95" - )) - .await - .unwrap(); - assert!(receipt.is_some()); - let receipt = receipt.unwrap(); - assert_eq!( - receipt.transaction_hash.unwrap(), - b256!("5c03fab9114ceb98994b43892ade87ddfd9ae7e8f293935c3bd29d435dc9fd95") - ); - } - - #[tokio::test] - async fn gets_fee_history() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let block_number = provider.get_block_number().await.unwrap(); - let fee_history = provider - .get_fee_history( - U256::from(utils::EIP1559_FEE_ESTIMATION_PAST_BLOCKS), - BlockNumberOrTag::Number(block_number), - &[utils::EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE], - ) - .await - .unwrap(); - assert_eq!(fee_history.oldest_block, U256::ZERO); - } - - #[tokio::test] - #[ignore] // Anvil has yet to implement the `eth_getBlockReceipts` method. - async fn gets_block_receipts() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let receipts = provider.get_block_receipts(BlockNumberOrTag::Latest).await.unwrap(); - assert!(receipts.is_some()); - } - - #[tokio::test] - async fn gets_block_traces() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let traces = provider.trace_block(BlockNumberOrTag::Latest).await.unwrap(); - assert_eq!(traces.len(), 0); - } - - #[tokio::test] - async fn sends_raw_transaction() { - let anvil = Anvil::new().spawn(); - let provider = Provider::try_from(&anvil.endpoint()).unwrap(); - let tx_hash = provider - .send_raw_transaction( - // Transfer 1 ETH from default EOA address to the Genesis address. - bytes!("f865808477359400825208940000000000000000000000000000000000000000018082f4f5a00505e227c1c636c76fac55795db1a40a4d24840d81b40d2fe0cc85767f6bd202a01e91b437099a8a90234ac5af3cb7ca4fb1432e133f75f9a91678eaf5f487c74b") - ) - .await.unwrap(); - assert_eq!( - tx_hash.to_string(), - "0x9dae5cf33694a02e8a7d5de3fe31e9d05ca0ba6e9180efac4ab20a06c9e598a3" - ); - } -} diff --git a/crates/rpc-types/Cargo.toml b/crates/rpc-types/Cargo.toml index 3422365bd9d..0a0a62026b7 100644 --- a/crates/rpc-types/Cargo.toml +++ b/crates/rpc-types/Cargo.toml @@ -33,12 +33,22 @@ proptest-derive = { version = "0.4", optional = true } jsonrpsee-types = { version = "0.20", optional = true } [features] -arbitrary = ["dep:arbitrary", "dep:proptest-derive", "dep:proptest", "alloy-primitives/arbitrary"] +arbitrary = [ + "dep:arbitrary", + "dep:proptest-derive", + "dep:proptest", + "alloy-primitives/arbitrary", +] jsonrpsee-types = ["dep:jsonrpsee-types"] -ssz = ["dep:ethereum_ssz" ,"dep:ethereum_ssz_derive", "alloy-primitives/ssz"] +ssz = ["dep:ethereum_ssz", "dep:ethereum_ssz_derive", "alloy-primitives/ssz"] [dev-dependencies] -alloy-primitives = { workspace = true, features = ["rand", "rlp", "serde", "arbitrary"] } +alloy-primitives = { workspace = true, features = [ + "rand", + "rlp", + "serde", + "arbitrary", +] } arbitrary = { workspace = true, features = ["derive"] } proptest.workspace = true diff --git a/crates/rpc-types/src/eth/transaction/mod.rs b/crates/rpc-types/src/eth/transaction/mod.rs index 52e6e47ae87..acef878b249 100644 --- a/crates/rpc-types/src/eth/transaction/mod.rs +++ b/crates/rpc-types/src/eth/transaction/mod.rs @@ -7,6 +7,7 @@ pub use blob::BlobTransactionSidecar; pub use common::TransactionInfo; pub use optimism::OptimismTransactionReceiptFields; pub use receipt::TransactionReceipt; +pub use request::{TransactionInput, TransactionRequest}; use serde::{Deserialize, Serialize}; pub use signature::{Parity, Signature}; diff --git a/crates/rpc-types/src/eth/transaction/optimism.rs b/crates/rpc-types/src/eth/transaction/optimism.rs index 075b0ef6a6d..cbc8b6594ad 100644 --- a/crates/rpc-types/src/eth/transaction/optimism.rs +++ b/crates/rpc-types/src/eth/transaction/optimism.rs @@ -1,8 +1,8 @@ -//! Misc Optimism-specific types -use alloy_primitives::{B256, U128, U256, U64}; -use serde::{Deserialize, Serialize}; +//! Misc Optimism-specific types. use crate::other::OtherFields; +use alloy_primitives::{B256, U128, U256, U64}; +use serde::{Deserialize, Serialize}; /// Optimism specific transaction fields #[derive(Debug, Copy, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -84,9 +84,8 @@ mod l1_fee_scalar_serde { #[cfg(test)] mod tests { - use serde_json::{json, Value}; - use super::*; + use serde_json::{json, Value}; #[test] fn serialize_empty_optimism_transaction_receipt_fields_struct() { diff --git a/crates/rpc-types/src/eth/transaction/request.rs b/crates/rpc-types/src/eth/transaction/request.rs index a15f125e548..01fd9679b5b 100644 --- a/crates/rpc-types/src/eth/transaction/request.rs +++ b/crates/rpc-types/src/eth/transaction/request.rs @@ -1,9 +1,9 @@ //! Alloy basic Transaction Request type. -use std::hash::Hash; use crate::{eth::transaction::AccessList, other::OtherFields, BlobTransactionSidecar}; -use alloy_primitives::{Address, Bytes, B256, U256, U64, U8}; +use alloy_primitives::{Address, Bytes, ChainId, B256, U256, U64, U8}; use serde::{Deserialize, Serialize}; +use std::hash::Hash; /// Represents _all_ transaction requests to/from RPC. #[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] @@ -35,7 +35,7 @@ pub struct TransactionRequest { /// The nonce of the transaction. pub nonce: Option, /// The chain ID for the transaction. - pub chain_id: Option, + pub chain_id: Option, /// An EIP-2930 access list, which lowers cost for accessing accounts and storages in the list. See [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930) for more information. #[serde(default)] pub access_list: Option, @@ -155,19 +155,6 @@ impl TransactionRequest { self.transaction_type = Some(U8::from(transaction_type)); self } - - /// Calculates the address that will be created by the transaction, if any. - /// - /// Returns `None` if the transaction is not a contract creation (the `to` field is set), or if - /// the `from` or `nonce` fields are not set. - pub fn calculate_create_address(&self) -> Option
{ - if self.to.is_some() { - return None; - } - let from = self.from.as_ref()?; - let nonce = self.nonce?; - Some(from.create(nonce.to())) - } } /// Helper type that supports both `data` and `input` fields that map to transaction input data. diff --git a/crates/signer-aws/Cargo.toml b/crates/signer-aws/Cargo.toml index c2e9b2e1083..df873e2a666 100644 --- a/crates/signer-aws/Cargo.toml +++ b/crates/signer-aws/Cargo.toml @@ -12,6 +12,8 @@ repository.workspace = true exclude.workspace = true [dependencies] +alloy-consensus.workspace = true +alloy-network.workspace = true alloy-primitives.workspace = true alloy-signer.workspace = true diff --git a/crates/signer-aws/src/signer.rs b/crates/signer-aws/src/signer.rs index 9cfc7154600..0441806cb89 100644 --- a/crates/signer-aws/src/signer.rs +++ b/crates/signer-aws/src/signer.rs @@ -1,5 +1,6 @@ +use alloy_consensus::SignableTransaction; use alloy_primitives::{hex, Address, ChainId, B256}; -use alloy_signer::{Result, Signature, Signer}; +use alloy_signer::{sign_transaction_with_chain_id, Result, Signature, Signer}; use async_trait::async_trait; use aws_sdk_kms::{ error::SdkError, @@ -92,12 +93,24 @@ pub enum AwsSignerError { PublicKeyNotFound, } +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl alloy_network::TxSigner for AwsSigner { + #[inline] + async fn sign_transaction( + &self, + tx: &mut dyn SignableTransaction, + ) -> Result { + sign_transaction_with_chain_id!(self, tx, self.sign_hash(&tx.signature_hash()).await) + } +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for AwsSigner { #[instrument(err)] - #[allow(clippy::blocks_in_conditions)] // instrument on async fn - async fn sign_hash(&self, hash: B256) -> Result { + #[allow(clippy::blocks_in_conditions)] // tracing::instrument on async fn + async fn sign_hash(&self, hash: &B256) -> Result { self.sign_digest_inner(hash).await.map_err(alloy_signer::Error::other) } @@ -148,19 +161,19 @@ impl AwsSigner { pub async fn sign_digest_with_key( &self, key_id: String, - digest: B256, + digest: &B256, ) -> Result { request_sign_digest(&self.kms, key_id, digest).await.and_then(decode_signature) } /// Sign a digest with this signer's key - pub async fn sign_digest(&self, digest: B256) -> Result { + pub async fn sign_digest(&self, digest: &B256) -> Result { self.sign_digest_with_key(self.key_id.clone(), digest).await } /// Sign a digest with this signer's key and applies EIP-155. #[instrument(err, skip(digest), fields(digest = %hex::encode(digest)))] - async fn sign_digest_inner(&self, digest: B256) -> Result { + async fn sign_digest_inner(&self, digest: &B256) -> Result { let sig = self.sign_digest(digest).await?; let mut sig = sig_from_digest_bytes_trial_recovery(sig, digest, &self.pubkey); if let Some(chain_id) = self.chain_id { @@ -182,7 +195,7 @@ async fn request_get_pubkey( async fn request_sign_digest( kms: &Client, key_id: String, - digest: B256, + digest: &B256, ) -> Result { kms.sign() .key_id(key_id) @@ -212,7 +225,7 @@ fn decode_signature(resp: SignOutput) -> Result Signature { let signature = Signature::from_signature_and_parity(sig, false).unwrap(); @@ -229,8 +242,8 @@ fn sig_from_digest_bytes_trial_recovery( } /// Makes a trial recovery to check whether an RSig corresponds to a known `VerifyingKey`. -fn check_candidate(signature: &Signature, hash: B256, pubkey: &VerifyingKey) -> bool { - signature.recover_from_prehash(&hash).map(|key| key == *pubkey).unwrap_or(false) +fn check_candidate(signature: &Signature, hash: &B256, pubkey: &VerifyingKey) -> bool { + signature.recover_from_prehash(hash).map(|key| key == *pubkey).unwrap_or(false) } #[cfg(test)] diff --git a/crates/signer-gcp/Cargo.toml b/crates/signer-gcp/Cargo.toml index c50534c3ead..890c305f3bd 100644 --- a/crates/signer-gcp/Cargo.toml +++ b/crates/signer-gcp/Cargo.toml @@ -12,6 +12,8 @@ repository.workspace = true exclude.workspace = true [dependencies] +alloy-consensus.workspace = true +alloy-network.workspace = true alloy-primitives.workspace = true alloy-signer.workspace = true diff --git a/crates/signer-gcp/src/signer.rs b/crates/signer-gcp/src/signer.rs index 0758c93a0bb..849c4b6db45 100644 --- a/crates/signer-gcp/src/signer.rs +++ b/crates/signer-gcp/src/signer.rs @@ -1,5 +1,6 @@ +use alloy_consensus::SignableTransaction; use alloy_primitives::{hex, Address, B256}; -use alloy_signer::{Result, Signature, Signer}; +use alloy_signer::{sign_transaction_with_chain_id, Result, Signature, Signer}; use async_trait::async_trait; use gcloud_sdk::{ google::cloud::kms::{ @@ -144,13 +145,25 @@ pub enum GcpSignerError { K256(#[from] ecdsa::Error), } +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl alloy_network::TxSigner for GcpSigner { + #[inline] + async fn sign_transaction( + &self, + tx: &mut dyn SignableTransaction, + ) -> Result { + sign_transaction_with_chain_id!(self, tx, self.sign_hash(&tx.signature_hash()).await) + } +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for GcpSigner { #[instrument(err)] #[allow(clippy::blocks_in_conditions)] - async fn sign_hash(&self, hash: B256) -> Result { - self.sign_digest_inner(&hash).await.map_err(alloy_signer::Error::other) + async fn sign_hash(&self, hash: &B256) -> Result { + self.sign_digest_inner(hash).await.map_err(alloy_signer::Error::other) } #[inline] diff --git a/crates/signer-ledger/Cargo.toml b/crates/signer-ledger/Cargo.toml index e83580716b1..c9727479b63 100644 --- a/crates/signer-ledger/Cargo.toml +++ b/crates/signer-ledger/Cargo.toml @@ -12,6 +12,7 @@ repository.workspace = true exclude.workspace = true [dependencies] +alloy-consensus.workspace = true alloy-primitives.workspace = true alloy-signer.workspace = true @@ -24,6 +25,7 @@ tracing.workspace = true # eip712 alloy-sol-types = { workspace = true, optional = true } +alloy-network.workspace = true [dev-dependencies] alloy-consensus.workspace = true diff --git a/crates/signer-ledger/src/signer.rs b/crates/signer-ledger/src/signer.rs index c31910adac4..b61a42a2545 100644 --- a/crates/signer-ledger/src/signer.rs +++ b/crates/signer-ledger/src/signer.rs @@ -1,8 +1,9 @@ //! Ledger Ethereum app wrapper. use crate::types::{DerivationType, LedgerError, INS, P1, P1_FIRST, P2}; +use alloy_consensus::SignableTransaction; use alloy_primitives::{hex, Address, ChainId, B256}; -use alloy_signer::{Result, SignableTx, Signature, Signer, TransactionExt}; +use alloy_signer::{sign_transaction_with_chain_id, Result, Signature, Signer}; use async_trait::async_trait; use coins_ledger::{ common::{APDUCommand, APDUData}, @@ -27,10 +28,22 @@ pub struct LedgerSigner { pub(crate) address: Address, } +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl alloy_network::TxSigner for LedgerSigner { + #[inline] + async fn sign_transaction( + &self, + tx: &mut dyn SignableTransaction, + ) -> Result { + sign_transaction_with_chain_id!(self, tx, self.sign_tx_rlp(&tx.encoded_for_signing()).await) + } +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for LedgerSigner { - async fn sign_hash(&self, _hash: B256) -> Result { + async fn sign_hash(&self, _hash: &B256) -> Result { Err(alloy_signer::Error::UnsupportedOperation( alloy_signer::UnsupportedSignerOperation::SignHash, )) @@ -47,20 +60,6 @@ impl Signer for LedgerSigner { .map_err(alloy_signer::Error::other) } - #[inline] - async fn sign_transaction(&self, tx: &mut SignableTx) -> Result { - let chain_id = self.chain_id(); - if let Some(chain_id) = chain_id { - tx.set_chain_id_checked(chain_id)?; - } - let rlp = tx.encoded_for_signing(); - let mut sig = self.sign_tx_rlp(&rlp).await.map_err(alloy_signer::Error::other)?; - if let Some(chain_id) = chain_id.or_else(|| tx.chain_id()) { - sig = sig.with_chain_id(chain_id); - } - Ok(sig) - } - #[cfg(feature = "eip712")] #[inline] async fn sign_typed_data( @@ -283,9 +282,9 @@ impl LedgerSigner { #[cfg(test)] mod tests { use super::*; + use alloy_network::TxSigner; use alloy_primitives::{address, bytes, U256}; use alloy_rlp::Decodable; - use alloy_signer::Transaction; use std::sync::OnceLock; const DTYPE: DerivationType = DerivationType::LedgerLive(0); @@ -334,7 +333,9 @@ mod tests { nonce: 5, gas_price: 400e9 as u128, gas_limit: 1000000, - to: alloy_consensus::TxKind::Call(address!("2ed7afa17473e17ac59908f088b4371d28585476")), + to: alloy_primitives::TxKind::Call(address!( + "2ed7afa17473e17ac59908f088b4371d28585476" + )), // TODO: this fails for some reason with 6a80 APDU_CODE_BAD_KEY_HANDLE // approve uni v2 router 0xff // input: bytes!("095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), @@ -376,7 +377,7 @@ mod tests { test_sign_tx_generic(&mut tx).await; } - async fn test_sign_tx_generic(tx: &mut SignableTx) { + async fn test_sign_tx_generic(tx: &mut dyn SignableTransaction) { let sighash = tx.signature_hash(); let ledger = init_ledger().await; let sig = match ledger.sign_transaction(tx).await { diff --git a/crates/signer-trezor/src/signer.rs b/crates/signer-trezor/src/signer.rs index 200071f40fa..d7d7231d92f 100644 --- a/crates/signer-trezor/src/signer.rs +++ b/crates/signer-trezor/src/signer.rs @@ -1,8 +1,7 @@ use super::types::{DerivationType, TrezorError}; -use alloy_consensus::TxEip1559; -use alloy_network::{Transaction, TxKind}; -use alloy_primitives::{hex, Address, ChainId, Parity, B256, U256}; -use alloy_signer::{Result, SignableTx, Signature, Signer, TransactionExt}; +use alloy_consensus::{SignableTransaction, TxEip1559}; +use alloy_primitives::{hex, Address, ChainId, Parity, TxKind, B256, U256}; +use alloy_signer::{sign_transaction_with_chain_id, Result, Signature, Signer}; use async_trait::async_trait; use std::fmt; use trezor_client::client::Trezor; @@ -38,7 +37,7 @@ impl fmt::Debug for TrezorSigner { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for TrezorSigner { #[inline] - async fn sign_hash(&self, _hash: B256) -> Result { + async fn sign_hash(&self, _hash: &B256) -> Result { Err(alloy_signer::Error::UnsupportedOperation( alloy_signer::UnsupportedSignerOperation::SignHash, )) @@ -49,18 +48,6 @@ impl Signer for TrezorSigner { self.sign_message_inner(message).await.map_err(alloy_signer::Error::other) } - #[inline] - async fn sign_transaction(&self, tx: &mut SignableTx) -> Result { - if let Some(chain_id) = self.chain_id { - tx.set_chain_id_checked(chain_id)?; - } - let mut sig = self.sign_tx_inner(tx).await.map_err(alloy_signer::Error::other)?; - if let Some(chain_id) = self.chain_id.or_else(|| tx.chain_id()) { - sig = sig.with_chain_id(chain_id); - } - Ok(sig) - } - #[inline] fn address(&self) -> Address { self.address @@ -77,6 +64,18 @@ impl Signer for TrezorSigner { } } +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl alloy_network::TxSigner for TrezorSigner { + #[inline] + async fn sign_transaction( + &self, + tx: &mut dyn SignableTransaction, + ) -> Result { + sign_transaction_with_chain_id!(self, tx, self.sign_tx_inner(tx).await) + } +} + impl TrezorSigner { /// Instantiates a new Trezor signer. #[instrument(ret)] @@ -157,7 +156,7 @@ impl TrezorSigner { /// Does not apply EIP-155. async fn sign_tx_inner( &self, - tx: &dyn Transaction, + tx: &dyn SignableTransaction, ) -> Result { let mut client = self.get_client()?; let path = Self::convert_path(&self.derivation); diff --git a/crates/signer-trezor/src/types.rs b/crates/signer-trezor/src/types.rs index 7ab3315b737..9b3eead0fe8 100644 --- a/crates/signer-trezor/src/types.rs +++ b/crates/signer-trezor/src/types.rs @@ -53,3 +53,9 @@ pub enum TrezorError { #[error("could not retrieve device features")] Features, } + +impl From for alloy_signer::Error { + fn from(error: TrezorError) -> Self { + alloy_signer::Error::other(error) + } +} diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 5e075a1b49b..464dccf97d2 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -12,7 +12,6 @@ repository.workspace = true exclude.workspace = true [dependencies] -alloy-network.workspace = true alloy-primitives = { workspace = true, features = ["k256"] } auto_impl.workspace = true @@ -33,10 +32,15 @@ coins-bip32 = { version = "0.8.7", default-features = false, optional = true } coins-bip39 = { version = "0.8.7", default-features = false, optional = true } # yubi -yubihsm = { version = "0.42", features = ["secp256k1", "http", "usb"], optional = true } +yubihsm = { version = "0.42", features = [ + "secp256k1", + "http", + "usb", +], optional = true } [dev-dependencies] alloy-consensus.workspace = true +alloy-network.workspace = true assert_matches.workspace = true serde_json.workspace = true tempfile.workspace = true @@ -44,7 +48,9 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } # need to enable features for tests yubihsm = { version = "0.42", features = ["mockhsm"] } -coins-bip39 = { version = "0.8.7", default-features = false, features = ["english"] } +coins-bip39 = { version = "0.8.7", default-features = false, features = [ + "english", +] } [features] eip712 = ["dep:alloy-sol-types"] diff --git a/crates/signer/README.md b/crates/signer/README.md index bb14922cb70..a482563501b 100644 --- a/crates/signer/README.md +++ b/crates/signer/README.md @@ -44,6 +44,7 @@ Sign a transaction: use alloy_consensus::TxLegacy; use alloy_primitives::{U256, address, bytes}; use alloy_signer::{LocalWallet, Signer, SignerSync}; +use alloy_network::{TxSignerSync}; // Instantiate the wallet. let wallet = "dcf2cbdd171a21c480aa7f53d77f31bb102282b3ff099c78e3118b37348c72f7" diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index 2f3c65151c7..4f7b81772bf 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -19,7 +19,7 @@ mod error; pub use error::{Error, Result, UnsupportedSignerOperation}; mod signer; -pub use signer::{SignableTx, Signer, SignerSync, Transaction, TransactionExt}; +pub use signer::{Signer, SignerSync}; mod wallet; #[cfg(feature = "mnemonic")] @@ -43,3 +43,33 @@ pub type LocalWallet = Wallet; /// A wallet instantiated with a YubiHSM #[cfg(feature = "yubihsm")] pub type YubiWallet = Wallet>; + +/// Utility to get and set the chain ID on a transaction and the resulting signature within a +/// signer's `sign_transaction`. +#[macro_export] +macro_rules! sign_transaction_with_chain_id { + // async ( + // signer: impl Signer, + // tx: &mut impl SignableTransaction, + // sign: lazy Signature, + // ) + ($signer:expr, $tx:expr, $sign:expr) => {{ + if let Some(chain_id) = $signer.chain_id() { + if !$tx.set_chain_id_checked(chain_id) { + return Err(alloy_signer::Error::TransactionChainIdMismatch { + signer: chain_id, + // we can only end up here if the tx has a chain id + tx: $tx.chain_id().unwrap(), + }); + } + } + + let mut sig = $sign.map_err(alloy_signer::Error::other)?; + + if let Some(chain_id) = $signer.chain_id().or_else(|| $tx.chain_id()) { + sig = sig.with_chain_id(chain_id); + } + + Ok(sig) + }}; +} diff --git a/crates/signer/src/signer.rs b/crates/signer/src/signer.rs index cf290bfbf6d..46dbd80a078 100644 --- a/crates/signer/src/signer.rs +++ b/crates/signer/src/signer.rs @@ -6,38 +6,6 @@ use auto_impl::auto_impl; #[cfg(feature = "eip712")] use alloy_sol_types::{Eip712Domain, SolStruct}; -pub use alloy_network::Transaction; - -/// A signable transaction. -pub type SignableTx = dyn Transaction; - -/// Extension trait for utilities for signable transactions. -/// -/// This trait is implemented for all types that implement [`Transaction`] with [`Signature`] as the -/// signature associated type. -pub trait TransactionExt: Transaction { - /// Set `chain_id` if it is not already set. Checks that the provided `chain_id` matches the - /// existing `chain_id` if it is already set. - fn set_chain_id_checked(&mut self, chain_id: ChainId) -> Result<()> { - match self.chain_id() { - Some(tx_chain_id) => { - if tx_chain_id != chain_id { - return Err(crate::Error::TransactionChainIdMismatch { - signer: chain_id, - tx: tx_chain_id, - }); - } - } - None => { - self.set_chain_id(chain_id); - } - } - Ok(()) - } -} - -impl> TransactionExt for T {} - /// Asynchronous Ethereum signer. /// /// All provided implementations rely on [`sign_hash`](Signer::sign_hash). A signer may not always @@ -45,43 +13,22 @@ impl> TransactionExt for T {} /// [`UnsupportedOperation`](crate::Error::UnsupportedOperation), and implement all the signing /// methods directly. /// -/// A signer should hold an optional [`ChainId`] value, which is used for [EIP-155] replay -/// protection. -/// -/// If `chain_id` is Some, [EIP-155] should be applied to the input transaction in -/// [`sign_transaction`](Self::sign_transaction), and to the resulting signature in all the methods. -/// If `chain_id` is None, [EIP-155] should not be applied. -/// /// Synchronous signers should implement both this trait and [`SignerSync`]. /// /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[auto_impl(&mut, Box)] -pub trait Signer: Send + Sync { +pub trait Signer: Send + Sync { /// Signs the given hash. - async fn sign_hash(&self, hash: B256) -> Result; + async fn sign_hash(&self, hash: &B256) -> Result; /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. /// /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 #[inline] - async fn sign_message(&self, message: &[u8]) -> Result { - self.sign_hash(eip191_hash_message(message)).await - } - - /// Signs the transaction. - #[inline] - async fn sign_transaction(&self, tx: &mut SignableTx) -> Result { - let chain_id = self.chain_id(); - if let Some(chain_id) = chain_id { - tx.set_chain_id_checked(chain_id)?; - } - let mut sig = self.sign_hash(tx.signature_hash()).await?; - if let Some(chain_id) = chain_id.or_else(|| tx.chain_id()) { - sig = sig.with_chain_id(chain_id); - } - Ok(sig) + async fn sign_message(&self, message: &[u8]) -> Result { + self.sign_hash(&eip191_hash_message(message)).await } /// Encodes and signs the typed data according to [EIP-712]. @@ -93,11 +40,11 @@ pub trait Signer: Send + Sync { &self, payload: &T, domain: &Eip712Domain, - ) -> Result + ) -> Result where Self: Sized, { - self.sign_hash(payload.eip712_signing_hash(domain)).await + self.sign_hash(&payload.eip712_signing_hash(domain)).await } /// Returns the signer's Ethereum Address. @@ -129,42 +76,21 @@ pub trait Signer: Send + Sync { /// [`UnsupportedOperation`](crate::Error::UnsupportedOperation), and implement all the signing /// methods directly. /// -/// A signer should hold an optional [`ChainId`] value, which is used for [EIP-155] replay -/// protection. -/// -/// If `chain_id` is Some, [EIP-155] should be applied to the input transaction in -/// [`sign_transaction_sync`](Self::sign_transaction_sync), and to the resulting signature in all -/// the methods. If `chain_id` is None, [EIP-155] should not be applied. -/// /// Synchronous signers should also implement [`Signer`], as they are always able to by delegating /// the asynchronous methods to the synchronous ones. /// /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 #[auto_impl(&, &mut, Box, Rc, Arc)] -pub trait SignerSync { +pub trait SignerSync { /// Signs the given hash. - fn sign_hash_sync(&self, hash: B256) -> Result; + fn sign_hash_sync(&self, hash: &B256) -> Result; /// Signs the hash of the provided message after prefixing it, as specified in [EIP-191]. /// /// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191 #[inline] - fn sign_message_sync(&self, message: &[u8]) -> Result { - self.sign_hash_sync(eip191_hash_message(message)) - } - - /// Signs the transaction. - #[inline] - fn sign_transaction_sync(&self, tx: &mut SignableTx) -> Result { - let chain_id = self.chain_id_sync(); - if let Some(chain_id) = chain_id { - tx.set_chain_id_checked(chain_id)?; - } - let mut sig = self.sign_hash_sync(tx.signature_hash())?; - if let Some(chain_id) = chain_id.or_else(|| tx.chain_id()) { - sig = sig.with_chain_id(chain_id); - } - Ok(sig) + fn sign_message_sync(&self, message: &[u8]) -> Result { + self.sign_hash_sync(&eip191_hash_message(message)) } /// Encodes and signs the typed data according to [EIP-712]. @@ -172,15 +98,11 @@ pub trait SignerSync { /// [EIP-712]: https://eips.ethereum.org/EIPS/eip-712 #[cfg(feature = "eip712")] #[inline] - fn sign_typed_data_sync( - &self, - payload: &T, - domain: &Eip712Domain, - ) -> Result + fn sign_typed_data_sync(&self, payload: &T, domain: &Eip712Domain) -> Result where Self: Sized, { - self.sign_hash_sync(payload.eip712_signing_hash(domain)) + self.sign_hash_sync(&payload.eip712_signing_hash(domain)) } /// Returns the signer's chain ID. @@ -222,7 +144,7 @@ mod tests { async fn test_unsized_unimplemented_signer(s: &S) { assert_matches!( - s.sign_hash(B256::ZERO).await, + s.sign_hash(&B256::ZERO).await, Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); @@ -230,13 +152,11 @@ mod tests { s.sign_message(&[]).await, Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); - - assert!(s.sign_transaction(&mut alloy_consensus::TxLegacy::default()).await.is_err()); } fn test_unsized_unimplemented_signer_sync(s: &S) { assert_matches!( - s.sign_hash_sync(B256::ZERO), + s.sign_hash_sync(&B256::ZERO), Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); @@ -244,8 +164,6 @@ mod tests { s.sign_message_sync(&[]), Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) ); - - assert!(s.sign_transaction_sync(&mut alloy_consensus::TxLegacy::default()).is_err()); } struct UnimplementedSigner; @@ -253,7 +171,7 @@ mod tests { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Signer for UnimplementedSigner { - async fn sign_hash(&self, _hash: B256) -> Result { + async fn sign_hash(&self, _hash: &B256) -> Result { Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) } @@ -269,7 +187,7 @@ mod tests { } impl SignerSync for UnimplementedSigner { - fn sign_hash_sync(&self, _hash: B256) -> Result { + fn sign_hash_sync(&self, _hash: &B256) -> Result { Err(Error::UnsupportedOperation(UnsupportedSignerOperation::SignHash)) } diff --git a/crates/signer/src/wallet/mod.rs b/crates/signer/src/wallet/mod.rs index 46b7af6de55..61f018c2486 100644 --- a/crates/signer/src/wallet/mod.rs +++ b/crates/signer/src/wallet/mod.rs @@ -64,7 +64,7 @@ pub struct Wallet { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl + Send + Sync> Signer for Wallet { #[inline] - async fn sign_hash(&self, hash: B256) -> Result { + async fn sign_hash(&self, hash: &B256) -> Result { self.sign_hash_sync(hash) } @@ -86,7 +86,7 @@ impl + Send + Sync> Signer for impl> SignerSync for Wallet { #[inline] - fn sign_hash_sync(&self, hash: B256) -> Result { + fn sign_hash_sync(&self, hash: &B256) -> Result { let (recoverable_sig, recovery_id) = self.signer.sign_prehash(hash.as_ref())?; let mut sig = Signature::from_signature_and_parity(recoverable_sig, recovery_id)?; if let Some(chain_id) = self.chain_id { @@ -101,7 +101,7 @@ impl> SignerSync for Wallet } } -impl + Send + Sync> Wallet { +impl> Wallet { /// Construct a new wallet with an external [`PrehashSigner`]. #[inline] pub const fn new_with_signer(signer: D, address: Address, chain_id: Option) -> Self { @@ -115,9 +115,22 @@ impl + Send + Sync> Wallet { } /// Consumes this wallet and returns its signer. + #[inline] pub fn into_signer(self) -> D { self.signer } + + /// Returns this wallet's chain ID. + #[inline] + pub const fn address(&self) -> Address { + self.address + } + + /// Returns this wallet's chain ID. + #[inline] + pub const fn chain_id(&self) -> Option { + self.chain_id + } } // do not log the signer diff --git a/crates/signer/src/wallet/private_key.rs b/crates/signer/src/wallet/private_key.rs index 4fa52658836..dd9a4ad9d4d 100644 --- a/crates/signer/src/wallet/private_key.rs +++ b/crates/signer/src/wallet/private_key.rs @@ -14,6 +14,19 @@ use std::str::FromStr; use {elliptic_curve::rand_core, std::path::Path}; impl Wallet { + /// Creates a new Wallet instance from a [`SigningKey`]. + /// + /// This can also be used to create a Wallet from a [`SecretKey`](K256SecretKey). + /// See also the `From` implementations. + #[doc(alias = "from_private_key")] + #[doc(alias = "new_private_key")] + #[doc(alias = "new_pk")] + #[inline] + pub fn from_signing_key(signer: SigningKey) -> Self { + let address = secret_key_to_address(&signer); + Self::new_with_signer(signer, address, None) + } + /// Creates a new Wallet instance from a raw scalar serialized as a [`B256`] byte array. /// /// This is identical to [`from_field_bytes`](Self::from_field_bytes). @@ -25,7 +38,7 @@ impl Wallet { /// Creates a new Wallet instance from a raw scalar serialized as a [`FieldBytes`] byte array. #[inline] pub fn from_field_bytes(bytes: &FieldBytes) -> Result { - SigningKey::from_bytes(bytes).map(Self::new_pk) + SigningKey::from_bytes(bytes).map(Self::from_signing_key) } /// Creates a new Wallet instance from a raw scalar serialized as a byte slice. @@ -33,7 +46,7 @@ impl Wallet { /// Byte slices shorter than the field size (32 bytes) are handled by zero padding the input. #[inline] pub fn from_slice(bytes: &[u8]) -> Result { - SigningKey::from_slice(bytes).map(Self::new_pk) + SigningKey::from_slice(bytes).map(Self::from_signing_key) } /// Creates a new random keypair seeded with [`rand::thread_rng()`]. @@ -45,13 +58,7 @@ impl Wallet { /// Creates a new random keypair seeded with the provided RNG. #[inline] pub fn random_with(rng: &mut R) -> Self { - Self::new_pk(SigningKey::random(rng)) - } - - #[inline] - fn new_pk(signer: SigningKey) -> Self { - let address = secret_key_to_address(&signer); - Self::new_with_signer(signer, address, None) + Self::from_signing_key(SigningKey::random(rng)) } /// Borrow the secret [`NonZeroScalar`] value for this key. @@ -146,13 +153,13 @@ impl PartialEq for Wallet { impl From for Wallet { fn from(value: SigningKey) -> Self { - Self::new_pk(value) + Self::from_signing_key(value) } } impl From for Wallet { fn from(value: K256SecretKey) -> Self { - Self::new_pk(value.into()) + Self::from_signing_key(value.into()) } } @@ -168,9 +175,8 @@ impl FromStr for Wallet { #[cfg(test)] mod tests { use super::*; - use crate::{LocalWallet, Result, SignableTx, Signer, SignerSync}; - use alloy_consensus::TxLegacy; - use alloy_primitives::{address, b256, ChainId, Signature, U256}; + use crate::{LocalWallet, SignerSync}; + use alloy_primitives::{address, b256}; #[cfg(feature = "keystore")] use tempfile::tempdir; @@ -258,85 +264,9 @@ mod tests { assert_eq!(recovered2, address); } - #[tokio::test] - async fn signs_tx() { - async fn sign_tx_test(tx: &mut TxLegacy, chain_id: Option) -> Result { - let mut before = tx.clone(); - let sig = sign_dyn_tx_test(tx, chain_id).await?; - if let Some(chain_id) = chain_id { - assert_eq!(tx.chain_id, Some(chain_id), "chain ID was not set"); - before.chain_id = Some(chain_id); - } - assert_eq!(*tx, before); - Ok(sig) - } - - async fn sign_dyn_tx_test( - tx: &mut SignableTx, - chain_id: Option, - ) -> Result { - let mut wallet: Wallet = - "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".parse().unwrap(); - wallet.set_chain_id(chain_id); - - let sig = wallet.sign_transaction_sync(tx)?; - let sighash = tx.signature_hash(); - assert_eq!(sig.recover_address_from_prehash(&sighash).unwrap(), wallet.address); - - let sig_async = wallet.sign_transaction(tx).await.unwrap(); - assert_eq!(sig_async, sig); - - Ok(sig) - } - - // retrieved test vector from: - // https://web3js.readthedocs.io/en/v1.2.0/web3-eth-accounts.html#eth-accounts-signtransaction - let mut tx = TxLegacy { - to: alloy_consensus::TxKind::Call(address!("F0109fC8DF283027b6285cc889F5aA624EaC1F55")), - value: U256::from(1_000_000_000), - gas_limit: 2_000_000, - nonce: 0, - gas_price: 21_000_000_000, - input: Default::default(), - chain_id: None, - }; - let sig_none = sign_tx_test(&mut tx, None).await.unwrap(); - - tx.chain_id = Some(1); - let sig_1 = sign_tx_test(&mut tx, None).await.unwrap(); - let expected = "c9cf86333bcb065d140032ecaab5d9281bde80f21b9687b3e94161de42d51895727a108a0b8d101465414033c3f705a9c7b826e596766046ee1183dbc8aeaa6825".parse().unwrap(); - assert_eq!(sig_1, expected); - assert_ne!(sig_1, sig_none); - - tx.chain_id = Some(2); - let sig_2 = sign_tx_test(&mut tx, None).await.unwrap(); - assert_ne!(sig_2, sig_1); - assert_ne!(sig_2, sig_none); - - // Sets chain ID. - tx.chain_id = None; - let sig_none_none = sign_tx_test(&mut tx, None).await.unwrap(); - assert_eq!(sig_none_none, sig_none); - - tx.chain_id = None; - let sig_none_1 = sign_tx_test(&mut tx, Some(1)).await.unwrap(); - assert_eq!(sig_none_1, sig_1); - - tx.chain_id = None; - let sig_none_2 = sign_tx_test(&mut tx, Some(2)).await.unwrap(); - assert_eq!(sig_none_2, sig_2); - - // Errors on mismatch. - tx.chain_id = Some(2); - let error = sign_tx_test(&mut tx, Some(1)).await.unwrap_err(); - let expected_error = crate::Error::TransactionChainIdMismatch { signer: 1, tx: 2 }; - assert_eq!(error.to_string(), expected_error.to_string()); - } - #[test] #[cfg(feature = "eip712")] fn typed_data() { - use crate::Signer; use alloy_primitives::{keccak256, Address, I256, U256}; use alloy_sol_types::{eip712_domain, sol, SolStruct}; @@ -371,7 +301,7 @@ mod tests { let hash = foo_bar.eip712_signing_hash(&domain); let sig = wallet.sign_typed_data_sync(&foo_bar, &domain).unwrap(); assert_eq!(sig.recover_address_from_prehash(&hash).unwrap(), wallet.address()); - assert_eq!(wallet.sign_hash_sync(hash).unwrap(), sig); + assert_eq!(wallet.sign_hash_sync(&hash).unwrap(), sig); } #[test] diff --git a/crates/signer/src/wallet/yubi.rs b/crates/signer/src/wallet/yubi.rs index b6d81c7d1a7..fc81c741a06 100644 --- a/crates/signer/src/wallet/yubi.rs +++ b/crates/signer/src/wallet/yubi.rs @@ -66,7 +66,7 @@ impl From> for Wallet> { #[cfg(test)] mod tests { use super::*; - use crate::{Signer, SignerSync}; + use crate::SignerSync; use alloy_primitives::{address, hex}; #[test]