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"]