diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccbdcd995fe..6292a457cc5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,11 +16,11 @@ jobs: strategy: fail-fast: false matrix: - rust: ["stable", "nightly", "1.68"] # MSRV + rust: ["stable", "nightly", "1.75"] # MSRV flags: ["--no-default-features", "", "--all-features"] exclude: # Some features have higher MSRV. - - rust: "1.68" # MSRV + - rust: "1.75" # MSRV flags: "--all-features" steps: - uses: actions/checkout@v3 @@ -39,13 +39,13 @@ jobs: cache-on-failure: true # Only run tests on latest stable and above - name: Install cargo-nextest - if: ${{ matrix.rust != '1.68' }} # MSRV + if: ${{ matrix.rust != '1.75' }} # MSRV uses: taiki-e/install-action@nextest - name: build - if: ${{ matrix.rust == '1.68' }} # MSRV + if: ${{ matrix.rust == '1.75' }} # MSRV run: cargo build --workspace ${{ matrix.flags }} - name: test - if: ${{ matrix.rust != '1.68' }} # MSRV + if: ${{ matrix.rust != '1.75' }} # MSRV run: cargo nextest run --workspace ${{ matrix.flags }} wasm: diff --git a/Cargo.toml b/Cargo.toml index 71a043e7c4f..05d7ef0b994 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.package] version = "0.1.0" edition = "2021" -rust-version = "1.68" +rust-version = "1.75" authors = ["Alloy Contributors"] license = "MIT OR Apache-2.0" homepage = "https://github.com/alloy-rs/next" @@ -97,3 +97,7 @@ proptest-derive = "0.4" serial_test = "3.0" similar-asserts = "1.5" tempfile = "3.8" + +# TODO: Remove once alloy-contract is stable. This is only used in tests for `sol!`. +[patch.crates-io] +alloy-sol-macro = { git = "https://github.com/alloy-rs/core" } diff --git a/README.md b/README.md index 06c49c89189..7000887cd15 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ When updating this, also update: Alloy will keep a rolling MSRV (minimum supported rust version) policy of **at least** 6 months. When increasing the MSRV, the new Rust version must have been -released at least six months ago. The current MSRV is 1.68. +released at least six months ago. The current MSRV is 1.75. Note that the MSRV is not increased automatically, and only as part of a minor release. diff --git a/clippy.toml b/clippy.toml index fa19647b105..cc4ad18b187 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.68" +msrv = "1.75" diff --git a/crates/dyn-contract/Cargo.toml b/crates/contract/Cargo.toml similarity index 60% rename from crates/dyn-contract/Cargo.toml rename to crates/contract/Cargo.toml index ad490f602d7..bcc7089396d 100644 --- a/crates/dyn-contract/Cargo.toml +++ b/crates/contract/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "alloy-dyn-contract" -description = "Helpers for interacting with contracts that do not have a known ABI at compile time" +name = "alloy-contract" +description = "Helpers for interacting with on-chain contracts" version.workspace = true edition.workspace = true @@ -19,3 +19,10 @@ alloy-transport.workspace = true alloy-dyn-abi.workspace = true alloy-json-abi.workspace = true alloy-primitives.workspace = true +alloy-sol-types.workspace = true + +thiserror.workspace = true + +[dev-dependencies] +alloy-node-bindings.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/contract/README.md b/crates/contract/README.md new file mode 100644 index 00000000000..6ad0e5c7291 --- /dev/null +++ b/crates/contract/README.md @@ -0,0 +1,7 @@ +# alloy-contract + +Helpers for interacting with on-chain contracts. + +The main type is `CallBuilder`, which is a builder for constructing calls to on-chain contracts. +It provides a way to encode and decode data for on-chain calls, and to send those calls to the chain. +See its documentation for more details. diff --git a/crates/contract/src/call.rs b/crates/contract/src/call.rs new file mode 100644 index 00000000000..22490ea5751 --- /dev/null +++ b/crates/contract/src/call.rs @@ -0,0 +1,564 @@ +use crate::{Error, Result}; +use alloy_dyn_abi::{DynSolValue, FunctionExt, JsonAbiExt}; +use alloy_json_abi::Function; +use alloy_primitives::{Address, Bytes, U256, U64}; +use alloy_providers::provider::TempProvider; +use alloy_rpc_types::{state::StateOverride, BlockId, CallInput, CallRequest, TransactionReceipt}; +use alloy_sol_types::SolCall; +use std::{ + future::{Future, IntoFuture}, + marker::PhantomData, + pin::Pin, +}; + +/// [`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>; + +/// [`CallBuilder`] using a [`Function`] as the call decoder. +pub type DynCallBuilder

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

= CallBuilder; + +mod private { + pub trait Sealed {} + impl Sealed for super::Function {} + impl Sealed for super::PhantomData {} + impl Sealed for () {} +} + +/// A trait for decoding the output of a contract function. +/// +/// This trait is sealed and cannot be implemented manually. +/// It is an implementation detail of [`CallBuilder`]. +pub trait CallDecoder: private::Sealed { + // Not public API. + + /// The output type of the contract function. + #[doc(hidden)] + type CallOutput; + + /// Decodes the output of a contract function. + #[doc(hidden)] + fn abi_decode_output(&self, data: Bytes, validate: bool) -> Result; + + #[doc(hidden)] + fn as_debug_field(&self) -> impl std::fmt::Debug; +} + +impl CallDecoder for Function { + type CallOutput = Vec; + + #[inline] + fn abi_decode_output(&self, data: Bytes, validate: bool) -> Result { + FunctionExt::abi_decode_output(self, &data, validate).map_err(Error::AbiError) + } + + #[inline] + fn as_debug_field(&self) -> impl std::fmt::Debug { + self + } +} + +impl CallDecoder for PhantomData { + type CallOutput = C::Return; + + #[inline] + fn abi_decode_output(&self, data: Bytes, validate: bool) -> Result { + C::abi_decode_returns(&data, validate).map_err(|e| Error::AbiError(e.into())) + } + + #[inline] + fn as_debug_field(&self) -> impl std::fmt::Debug { + std::any::type_name::() + } +} + +impl CallDecoder for () { + type CallOutput = Bytes; + + #[inline] + fn abi_decode_output(&self, data: Bytes, _validate: bool) -> Result { + Ok(data) + } + + #[inline] + fn as_debug_field(&self) -> impl std::fmt::Debug { + format_args!("()") + } +} + +/// A builder for sending a transaction via `eth_sendTransaction`, or calling a contract via +/// `eth_call`. +/// +/// The builder can be `.await`ed directly, which is equivalent to invoking [`call`]. +/// Prefer using [`call`] when possible, as `await`ing the builder directly will consume it, and +/// currently also boxes the future due to type system limitations. +/// +/// A call builder can currently be instantiated in the following ways: +/// - by [`sol!`][sol]-generated contract structs' methods (through the `#[sol(rpc)]` attribute) +/// ([`SolCallBuilder`]); +/// - by [`ContractInstance`](crate::ContractInstance)'s methods ([`DynCallBuilder`]); +/// - using [`CallBuilder::new_raw`] ([`RawCallBuilder`]). +/// +/// Each method represents a different way to decode the output of the contract call. +/// +/// [`call`]: CallBuilder::call +/// +/// # Note +/// +/// This will set [state overrides](https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set) +/// for `eth_call`, but this is not supported by all clients. +/// +/// # Examples +/// +/// Using [`sol!`][sol]: +/// +/// ```no_run +/// # async fn test(provider: P) -> Result<(), Box> { +/// use alloy_contract::SolCallBuilder; +/// use alloy_primitives::{Address, U256}; +/// use alloy_sol_types::sol; +/// +/// sol! { +/// #[sol(rpc)] // <-- Important! +/// contract MyContract { +/// function doStuff(uint a, bool b) public returns(address c, bytes32 d); +/// } +/// } +/// +/// # stringify!( +/// let provider = ...; +/// # ); +/// let address = Address::ZERO; +/// let contract = MyContract::new(address, &provider); +/// +/// // Through `contract.(args...)` +/// let a = U256::ZERO; +/// let b = true; +/// 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 MyContract::doStuffReturn { c: _, d: _ } = builder.call().await?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Using [`ContractInstance`](crate::ContractInstance): +/// +/// ```no_run +/// # 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}; +/// +/// # stringify!( +/// let dynamic_abi: JsonAbi = ...; +/// # ); +/// let interface = Interface::new(dynamic_abi); +/// +/// # stringify!( +/// let provider = ...; +/// # ); +/// let address = Address::ZERO; +/// 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 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_result: Bytes = raw_builder.call().await?; +/// // Decode the raw bytes: +/// let decoded_result: Vec = call_builder.decode_output(raw_result, false)?; +/// # Ok(()) +/// # } +/// ``` +/// +/// [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: CallRequest, + block: Option, + state: Option, + provider: P, + decoder: D, +} + +// See [`ContractInstance`]. +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

{ + RawCallBuilder { + request: self.request, + block: self.block, + state: self.state, + provider: self.provider, + decoder: (), + } + } +} + +impl 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)] + pub fn new_sol(provider: P, address: &Address, call: &C) -> Self { + Self::new_inner(provider, call.abi_encode().into(), PhantomData::).to(Some(*address)) + } + + /// Clears the decoder, returning a raw call builder. + #[inline] + pub fn clear_decoder(self) -> RawCallBuilder

{ + RawCallBuilder { + request: self.request, + block: self.block, + state: self.state, + provider: self.provider, + decoder: (), + } + } +} + +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 + /// same as [`call_raw`](Self::call_raw). + #[inline] + pub fn new_raw(provider: P, input: Bytes) -> Self { + Self::new_inner(provider, input, ()) + } +} + +impl CallBuilder { + fn new_inner(provider: P, input: Bytes, decoder: D) -> Self { + let request = CallRequest { input: CallInput::new(input), ..Default::default() }; + Self { request, decoder, provider, block: None, state: None } + } + + /// 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 + } + + /// 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 + } + + /// Uses a Legacy transaction instead of an EIP-1559 one to execute the call + pub fn legacy(self) -> Self { + todo!() + } + + /// Sets the `gas` field in the transaction to the provided value + pub fn gas(mut self, gas: U256) -> Self { + self.request = self.request.gas(gas); + self + } + + /// Sets the `gas_price` field in the transaction to the provided value + /// 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.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 + } + + /// 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 + } + + /// Sets the `block` field for sending the tx to the chain + pub const fn block(mut self, block: BlockId) -> Self { + self.block = Some(block); + self + } + + /// Sets the [state override set](https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set). + /// + /// # Note + /// + /// Not all client implementations will support this as a parameter to `eth_call`. + pub fn state(mut self, state: StateOverride) -> Self { + self.state = Some(state); + self + } + + /// Returns the underlying transaction's ABI-encoded data. + pub fn calldata(&self) -> &Bytes { + self.request.input.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) + } + + /// Queries the blockchain via an `eth_call` without submitting a transaction to the network. + /// + /// Returns the decoded the output by using the provided decoder. + /// If this is not desired, use [`call_raw`](Self::call_raw) to get the raw output data. + pub async fn call(&self) -> Result { + let data = self.call_raw().await?; + self.decode_output(data, false) + } + + /// Queries the blockchain via an `eth_call` without submitting a transaction to the network. + /// + /// Does not decode the output of the call, returning the raw output data instead. + /// + /// 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 + } else { + self.provider.call(self.request.clone(), self.block).await + } + .map_err(Into::into) + } + + /// Decodes the output of a contract function using the provided decoder. + #[inline] + pub fn decode_output(&self, data: Bytes, validate: bool) -> Result { + self.decoder.abi_decode_output(data, validate) + } + + /// Broadcasts the underlying transaction to the network as a deployment transaction, returning + /// the address of the deployed contract after the transaction has been confirmed. + /// + /// Returns an error if the transaction is not a deployment transaction, or if the contract + /// address is not found in the deployment transaction’s receipt. + /// + /// For more fine-grained control over the deployment process, use [`send`](Self::send) instead. + /// + /// 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() { + return Err(Error::NotADeploymentTransaction); + } + let pending_tx = self.send().await?; + let receipt = pending_tx.await?; + receipt.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)) + } + + /// 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
{ + self.request.calculate_create_address() + } +} + +impl CallBuilder<&P, D> { + /// Clones the provider and returns a new builder with the cloned provider. + pub fn with_cloned_provider(self) -> CallBuilder { + CallBuilder { + request: self.request, + block: self.block, + state: self.state, + provider: self.provider.clone(), + decoder: self.decoder, + } + } +} + +/// [`CallBuilder`] can be turned into a [`Future`] automatically with `.await`. +/// +/// Defaults to calling [`CallBuilder::call`]. +impl IntoFuture for CallBuilder +where + P: TempProvider, + D: CallDecoder + Send + Sync, + Self: 'static, +{ + type Output = Result; + #[cfg(target_arch = "wasm32")] + type IntoFuture = Pin>>; + #[cfg(not(target_arch = "wasm32"))] + type IntoFuture = Pin + Send>>; + + #[inline] + fn into_future(self) -> Self::IntoFuture { + #[allow(clippy::redundant_async_block)] + Box::pin(async move { self.call().await }) + } +} + +impl std::fmt::Debug for CallBuilder { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CallBuilder") + .field("request", &self.request) + .field("block", &self.block) + .field("state", &self.state) + .field("decoder", &self.decoder.as_debug_field()) + .finish() + } +} + +#[cfg(test)] +#[allow(unused_imports)] +mod tests { + use super::*; + use alloy_node_bindings::{Anvil, AnvilInstance}; + use alloy_primitives::{address, b256, bytes, hex}; + use alloy_providers::provider::{HttpProvider, Provider}; + use alloy_sol_types::sol; + + #[test] + fn empty_constructor() { + sol! { + #[sol(rpc, bytecode = "6942")] + contract EmptyConstructor { + constructor(); + } + } + + let provider = Provider::try_from("http://localhost:8545").unwrap(); + let call_builder = EmptyConstructor::deploy_builder(&provider); + assert_eq!(*call_builder.calldata(), bytes!("6942")); + } + + sol! { + // Solc: 0.8.24+commit.e11b9ed9.Linux.g++ + // Command: solc a.sol --bin --via-ir --optimize --optimize-runs 1 + #[sol(rpc, bytecode = "60803461006357601f61014838819003918201601f19168301916001600160401b038311848410176100675780849260209460405283398101031261006357518015158091036100635760ff80195f54169116175f5560405160cc908161007c8239f35b5f80fd5b634e487b7160e01b5f52604160045260245ffdfe60808060405260043610156011575f80fd5b5f3560e01c9081638bf1799f14607a575063b09a261614602f575f80fd5b346076576040366003190112607657602435801515810360765715606f57604060015b81516004356001600160a01b0316815260ff919091166020820152f35b60405f6052565b5f80fd5b346076575f36600319011260765760209060ff5f541615158152f3fea264697066735822122043709781c9bdc30c530978abf5db25a4b4ccfebf989baafd2ba404519a7f7e8264736f6c63430008180033")] + contract MyContract { + bool public myState; + + constructor(bool myState_) { + myState = myState_; + } + + function doStuff(uint a, bool b) external pure returns(address c, bytes32 d) { + return (address(uint160(a)), bytes32(uint256(b ? 1 : 0))); + } + } + } + + #[test] + fn call_encoding() { + let provider = Provider::try_from("http://localhost:8545").unwrap(); + let contract = MyContract::new(Address::ZERO, &&provider).with_cloned_provider(); + let call_builder = contract.doStuff(U256::ZERO, true).with_cloned_provider(); + assert_eq!( + *call_builder.calldata(), + bytes!( + "b09a2616" + "0000000000000000000000000000000000000000000000000000000000000000" + "0000000000000000000000000000000000000000000000000000000000000001" + ), + ); + // Box the future to assert its concrete output type. + let _future: Box> + Send> = + Box::new(call_builder.call()); + } + + #[test] + fn deploy_encoding() { + let provider = Provider::try_from("http://localhost:8545").unwrap(); + let bytecode = &MyContract::BYTECODE[..]; + let call_builder = MyContract::deploy_builder(&provider, false); + assert_eq!( + call_builder.calldata()[..], + [ + bytecode, + &hex!("0000000000000000000000000000000000000000000000000000000000000000")[..] + ] + .concat(), + ); + let call_builder = MyContract::deploy_builder(&provider, true); + assert_eq!( + call_builder.calldata()[..], + [ + bytecode, + &hex!("0000000000000000000000000000000000000000000000000000000000000001")[..] + ] + .concat(), + ); + } + + // TODO: send_transaction, PendingTransaction + #[tokio::test(flavor = "multi_thread")] + async fn deploy_and_call() { + /* + let (provider, anvil) = spawn_anvil(); + + let my_contract = MyContract::deploy(provider, true).await.unwrap(); + let expected_address = anvil.addresses()[0].create(0); + assert_eq!(*my_contract.address(), expected_address); + + let my_state_builder = my_contract.myState(); + assert_eq!(my_state_builder.calldata()[..], MyContract::myStateCall {}.abi_encode(),); + let result: MyContract::myStateReturn = my_state_builder.call().await.unwrap(); + assert_eq!(result._0, true); + + let do_stuff_builder = my_contract.doStuff(U256::from(0x69), true); + assert_eq!( + do_stuff_builder.calldata()[..], + MyContract::doStuffCall { a: U256::from(0x69), b: true }.abi_encode(), + ); + let result: MyContract::doStuffReturn = do_stuff_builder.call().await.unwrap(); + assert_eq!(result.c, address!("0000000000000000000000000000000000000069")); + assert_eq!( + result.d, + b256!("0000000000000000000000000000000000000000000000000000000000000001"), + ); + */ + } + + #[allow(dead_code)] + 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/error.rs b/crates/contract/src/error.rs new file mode 100644 index 00000000000..895d7eba171 --- /dev/null +++ b/crates/contract/src/error.rs @@ -0,0 +1,30 @@ +use alloy_dyn_abi::Error as AbiError; +use alloy_primitives::Selector; +use alloy_transport::TransportError; +use thiserror::Error; + +/// Dynamic contract result type. +pub type Result = core::result::Result; + +/// Error when interacting with contracts. +#[derive(Debug, Error)] +pub enum Error { + /// Unknown function referenced. + #[error("unknown function: function {0} does not exist")] + UnknownFunction(String), + /// Unknown function selector referenced. + #[error("unknown function: function with selector {0} does not exist")] + UnknownSelector(Selector), + /// Called `deploy` with a transaction that is not a deployment transaction. + #[error("transaction is not a deployment transaction")] + NotADeploymentTransaction, + /// `contractAddress` was not found in the deployment transaction’s receipt. + #[error("missing `contractAddress` from deployment transaction receipt")] + ContractNotDeployed, + /// An error occurred ABI encoding or decoding. + #[error(transparent)] + AbiError(#[from] AbiError), + /// An error occurred interacting with a contract over RPC. + #[error(transparent)] + TransportError(#[from] TransportError), +} diff --git a/crates/dyn-contract/src/instance.rs b/crates/contract/src/instance.rs similarity index 60% rename from crates/dyn-contract/src/instance.rs rename to crates/contract/src/instance.rs index 2271f31d7c7..b0a05bb9c32 100644 --- a/crates/dyn-contract/src/instance.rs +++ b/crates/contract/src/instance.rs @@ -1,6 +1,6 @@ use crate::{CallBuilder, Interface, Result}; -use alloy_dyn_abi::{DynSolValue, JsonAbiExt}; -use alloy_json_abi::JsonAbi; +use alloy_dyn_abi::DynSolValue; +use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Selector}; use alloy_providers::provider::TempProvider; @@ -8,6 +8,7 @@ use alloy_providers::provider::TempProvider; /// /// 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

{ address: Address, provider: P, @@ -16,55 +17,64 @@ pub struct 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 } } - /// Returns the contract's address. - pub const fn address(&self) -> Address { - self.address + /// Returns a reference to the contract's address. + #[inline] + pub const fn address(&self) -> &Address { + &self.address + } + + /// Sets the contract's address. + #[inline] + pub fn set_address(&mut self, address: Address) { + self.address = address; + } + + /// Returns a new contract instance at `address`. + #[inline] + pub fn at(mut self, address: Address) -> ContractInstance

{ + self.set_address(address); + self } /// Returns a reference to the contract's ABI. + #[inline] pub const fn abi(&self) -> &JsonAbi { self.interface.abi() } /// Returns a reference to the contract's provider. - pub const fn provider_ref(&self) -> &P { + #[inline] + pub const fn provider(&self) -> &P { &self.provider } } -impl

ContractInstance

-where - P: Clone, -{ - /// Returns a clone of the contract's provider. - pub fn provider(&self) -> P { - self.provider.clone() - } - - /// Returns a new contract instance at `address`. - /// - /// Clones `self` internally - #[must_use] - pub fn at(&self, address: Address) -> ContractInstance

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

{ + ContractInstance { + address: self.address, + provider: self.provider.clone(), + interface: self.interface, + } } } -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 + /// + /// 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> { - let func = self.interface.get_from_name(name)?; - let data = func.abi_encode_input(args)?; - Ok(CallBuilder::new(self.provider.clone(), func.clone(), data.into())) + pub fn function(&self, name: &str, args: &[DynSolValue]) -> Result> { + let function = self.interface.get_from_name(name)?; + CallBuilder::new_dyn(&self.provider, function, args) } /// Returns a transaction builder for the provided function selector. @@ -72,23 +82,9 @@ impl ContractInstance

{ &self, selector: &Selector, args: &[DynSolValue], - ) -> Result> { - let func = self.interface.get_from_selector(selector)?; - let data = func.abi_encode_input(args)?; - Ok(CallBuilder::new(self.provider.clone(), func.clone(), data.into())) - } -} - -impl

Clone for ContractInstance

-where - P: Clone, -{ - fn clone(&self) -> Self { - ContractInstance { - address: self.address, - provider: self.provider.clone(), - interface: self.interface.clone(), - } + ) -> Result> { + let function = self.interface.get_from_selector(selector)?; + CallBuilder::new_dyn(&self.provider, function, args) } } diff --git a/crates/dyn-contract/src/interface.rs b/crates/contract/src/interface.rs similarity index 82% rename from crates/dyn-contract/src/interface.rs rename to crates/contract/src/interface.rs index db18a31c52f..cf719d0795c 100644 --- a/crates/dyn-contract/src/interface.rs +++ b/crates/contract/src/interface.rs @@ -2,7 +2,7 @@ use crate::{ContractInstance, Error, Result}; use alloy_dyn_abi::{DynSolValue, FunctionExt, JsonAbiExt}; use alloy_json_abi::{Function, JsonAbi}; use alloy_primitives::{Address, Selector}; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; /// A smart contract interface. #[derive(Debug, Clone)] @@ -11,14 +11,14 @@ pub struct Interface { functions: HashMap, } -impl From for Interface { - fn from(abi: JsonAbi) -> Self { - Self { abi, functions: Default::default() } - } -} - // TODO: events/errors impl Interface { + /// Creates a new contract interface from the provided ABI. + pub fn new(abi: JsonAbi) -> Self { + let functions = create_mapping(&abi.functions, Function::selector); + Self { abi, functions } + } + /// Returns the ABI encoded data (including the selector) for the provided function and /// arguments. /// @@ -120,3 +120,24 @@ impl Interface { ContractInstance::new(address, provider, self) } } + +/// Utility function for creating a mapping between a unique signature and a +/// name-index pair for accessing contract ABI items. +fn create_mapping( + elements: &BTreeMap>, + signature: F, +) -> HashMap +where + S: std::hash::Hash + Eq, + F: Fn(&T) -> S + Copy, +{ + elements + .iter() + .flat_map(|(name, sub_elements)| { + sub_elements + .iter() + .enumerate() + .map(move |(index, element)| (signature(element), (name.to_owned(), index))) + }) + .collect() +} diff --git a/crates/dyn-contract/src/lib.rs b/crates/contract/src/lib.rs similarity index 72% rename from crates/dyn-contract/src/lib.rs rename to crates/contract/src/lib.rs index 5c77c93f2b1..5d12208e72d 100644 --- a/crates/dyn-contract/src/lib.rs +++ b/crates/contract/src/lib.rs @@ -15,6 +15,9 @@ #![deny(unused_must_use, rust_2018_idioms)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#[cfg(test)] +extern crate self as alloy_contract; + mod error; pub use error::*; @@ -26,3 +29,10 @@ pub use instance::*; mod call; pub use call::*; + +// Not public API. +// 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::provider::TempProvider as Provider; +} diff --git a/crates/dyn-contract/README.md b/crates/dyn-contract/README.md deleted file mode 100644 index 46aae4c08b5..00000000000 --- a/crates/dyn-contract/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# alloy-dyn-contract - -Helpers for interacting with contracts that do not have a known ABI at compile time. - -This is primarily useful for e.g. CLIs that load an ABI at run-time, -and ABI encode/decode data based on user input. diff --git a/crates/dyn-contract/src/call.rs b/crates/dyn-contract/src/call.rs deleted file mode 100644 index 665b992dd1c..00000000000 --- a/crates/dyn-contract/src/call.rs +++ /dev/null @@ -1,172 +0,0 @@ -use crate::Result; -use alloy_dyn_abi::{DynSolValue, FunctionExt}; -use alloy_json_abi::Function; -use alloy_primitives::{Address, Bytes, U256, U64}; -use alloy_providers::provider::TempProvider; -use alloy_rpc_types::{state::StateOverride, BlockId, CallInput, CallRequest}; -use std::{ - future::{Future, IntoFuture}, - pin::Pin, -}; - -/// A builder for sending a transaction via. `eth_sendTransaction`, or calling a function via -/// `eth_call`. -/// -/// The builder can be `.await`ed directly which is equivalent to invoking [`CallBuilder::call`]. -/// -/// # Note -/// -/// Sets the [state overrides](https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set) for `eth_call`, but this is not supported by all clients. -#[derive(Clone)] -pub struct CallBuilder

{ - // todo: this will not work with `send_transaction` and does not differentiate between EIP-1559 - // and legacy tx - request: CallRequest, - block: Option, - state: Option, - provider: P, - // todo: only used to decode - should it be some type D to dedupe with `sol!` contracts? - function: Function, -} - -impl

CallBuilder

{ - pub(crate) fn new(provider: P, function: Function, input: Bytes) -> Self { - let request = CallRequest { input: CallInput::new(input), ..Default::default() }; - Self { request, function, provider, block: None, state: None } - } - - /// Sets the `from` field in the transaction to the provided value - pub fn from(mut self, from: Address) -> Self { - self.request = self.request.from(from); - self - } - - /// Uses a Legacy transaction instead of an EIP-1559 one to execute the call - pub fn legacy(self) -> Self { - todo!() - } - - /// Sets the `gas` field in the transaction to the provided value - pub fn gas(mut self, gas: U256) -> Self { - self.request = self.request.gas(gas); - self - } - - /// Sets the `gas_price` field in the transaction to the provided value - /// 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.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 - } - - /// 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 - } - - /// Sets the `block` field for sending the tx to the chain - pub const fn block(mut self, block: BlockId) -> Self { - self.block = Some(block); - self - } - - /// Sets the [state override set](https://geth.ethereum.org/docs/rpc/ns-eth#3-object---state-override-set). - /// - /// # Note - /// - /// Not all client implementations will support this as a parameter to `eth_call`. - pub fn state(mut self, state: StateOverride) -> Self { - self.state = Some(state); - self - } - - /// Returns the underlying transaction's ABI encoded data - pub fn calldata(&self) -> Option<&Bytes> { - self.request.input.input() - } -} - -impl

CallBuilder

-where - P: TempProvider, -{ - /// 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) - } - - /// Queries the blockchain via an `eth_call` for the provided transaction. - /// - /// If executed on a non-state mutating smart contract function (i.e. `view`, `pure`) - /// then it will return the raw data from the chain. - /// - /// If executed on a mutating smart contract function, it will do a "dry run" of the call - /// and return the return type of the transaction without mutating the state. - /// - /// # Note - /// - /// This function _does not_ send a transaction from your account. - pub async fn call(&self) -> Result> { - let bytes = self.call_raw().await?; - - // decode output - let data = self.function.abi_decode_output(&bytes, true)?; - - Ok(data) - } - - /// Queries the blockchain via an `eth_call` for the provided transaction without decoding - /// the output. - 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 - } else { - self.provider.call(self.request.clone(), self.block).await - } - .map_err(Into::into) - } - - /// Signs and broadcasts the provided transaction - pub async fn send(&self) -> Result<()> { - todo!() - } -} - -/// [`CallBuilder`] can be turned into a [`Future`] automatically with `.await`. -/// -/// Defaults to calling [`CallBuilder::call`]. -impl

IntoFuture for CallBuilder

-where - P: TempProvider + 'static, -{ - type Output = Result>; - - #[cfg(target_arch = "wasm32")] - type IntoFuture = Pin>>; - - #[cfg(not(target_arch = "wasm32"))] - type IntoFuture = Pin + Send>>; - - fn into_future(self) -> Self::IntoFuture { - #[allow(clippy::redundant_async_block)] - Box::pin(async move { self.call().await }) - } -} - -impl

std::fmt::Debug for CallBuilder

{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CallBuilder") - .field("function", &self.function) - .field("block", &self.block) - .field("state", &self.state) - .finish() - } -} diff --git a/crates/dyn-contract/src/error.rs b/crates/dyn-contract/src/error.rs deleted file mode 100644 index 1206a65ae0f..00000000000 --- a/crates/dyn-contract/src/error.rs +++ /dev/null @@ -1,58 +0,0 @@ -use alloy_dyn_abi::Error as AbiError; -use alloy_primitives::Selector; -use alloy_transport::TransportError; -use std::fmt; - -/// Dynamic contract result type. -pub type Result = core::result::Result; - -/// Error when interacting with contracts. -#[derive(Debug)] -pub enum Error { - /// Unknown function referenced. - UnknownFunction(String), - /// Unknown function selector referenced. - UnknownSelector(Selector), - /// An error occurred ABI encoding or decoding. - AbiError(AbiError), - /// An error occurred interacting with a contract over RPC. - TransportError(TransportError), -} - -impl From for Error { - fn from(error: AbiError) -> Self { - Self::AbiError(error) - } -} - -impl From for Error { - fn from(error: TransportError) -> Self { - Self::TransportError(error) - } -} - -impl std::error::Error for Error { - #[inline] - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::AbiError(e) => Some(e), - _ => None, - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::UnknownFunction(name) => { - write!(f, "unknown function: function {name} does not exist",) - } - Self::UnknownSelector(selector) => { - write!(f, "unknown function: function with selector {selector} does not exist") - } - - Self::AbiError(e) => e.fmt(f), - Self::TransportError(e) => e.fmt(f), - } - } -} diff --git a/crates/providers/src/provider.rs b/crates/providers/src/provider.rs index ba96982989c..1c4e99fa7a4 100644 --- a/crates/providers/src/provider.rs +++ b/crates/providers/src/provider.rs @@ -27,6 +27,9 @@ pub enum ClientError { 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. diff --git a/crates/rpc-types/src/eth/call.rs b/crates/rpc-types/src/eth/call.rs index 2f433c8352f..a954bb76b70 100644 --- a/crates/rpc-types/src/eth/call.rs +++ b/crates/rpc-types/src/eth/call.rs @@ -157,6 +157,13 @@ impl CallRequest { self } + /// Sets the `to` field in the call to the provided address + #[inline] + pub const fn to(mut self, to: Option

) -> Self { + self.to = to; + self + } + /// Sets the `gas` field in the transaction to the provided value pub const fn gas(mut self, gas: U256) -> Self { self.gas = Some(gas); @@ -184,6 +191,19 @@ impl CallRequest { self.nonce = Some(nonce); 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/rpc-types/src/eth/transaction/receipt.rs b/crates/rpc-types/src/eth/transaction/receipt.rs index 8369c089d03..d4d0ff34b1f 100644 --- a/crates/rpc-types/src/eth/transaction/receipt.rs +++ b/crates/rpc-types/src/eth/transaction/receipt.rs @@ -32,7 +32,7 @@ pub struct TransactionReceipt { pub blob_gas_price: Option, /// Address of the sender pub from: Address, - /// Address of the receiver. null when its a contract creation transaction. + /// Address of the receiver. None when its a contract creation transaction. pub to: Option
, /// Contract address created, or None if not a deployment. pub contract_address: Option
, @@ -55,3 +55,16 @@ pub struct TransactionReceipt { #[serde(flatten)] pub other: OtherFields, } + +impl TransactionReceipt { + /// 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` field is not set. + pub fn calculate_create_address(&self, nonce: u64) -> Option
{ + if self.to.is_some() { + return None; + } + Some(self.from.create(nonce)) + } +} diff --git a/deny.toml b/deny.toml index 50af8fcc72a..a916dc7f954 100644 --- a/deny.toml +++ b/deny.toml @@ -25,7 +25,7 @@ allow = [ "Unlicense", "MPL-2.0", # https://github.com/briansmith/ring/issues/902 - "LicenseRef-ring" + "LicenseRef-ring", ] exceptions = [ @@ -52,3 +52,5 @@ license-files = [{ path = "LICENSE", hash = 0x001c7e6c }] [sources] unknown-registry = "deny" unknown-git = "deny" +# TODO: Remove once alloy-contract is stable. This is only used in tests for `sol!`. +allow-git = ["https://github.com/alloy-rs/core"]