Skip to content

Commit

Permalink
feat: alloy-dyn-contract (#149)
Browse files Browse the repository at this point in the history
* feat: alloy-dyn-contract

* chore: clippy

* chore: lints

* fix: do not set eip-1559 gas

* chore: add some builders to `CallRequest`

* refactor: split `call` and `call_with_overrides`

* chore: fmt

* chore: switch to eip-1559 first

* chore: set cargo desc

* chore: derive clone

* chore: simplify error msg

* feat: add `Interface::into_abi`

* chore: nits

* chore: rename folder

* docs: touch up docs in Interface

* feat: fn call_raw in CallBuilder

* chore: clippy

---------

Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
  • Loading branch information
onbjerg and DaniPopes authored Jan 29, 2024
1 parent 916f3ad commit ba70833
Show file tree
Hide file tree
Showing 10 changed files with 587 additions and 4 deletions.
12 changes: 9 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ rustdoc-args = ["--cfg", "docsrs"]

[workspace.dependencies]
alloy-consensus = { version = "0.1.0", path = "crates/consensus" }
alloy-dyn-contract = { version = "0.1.0", path = "crates/dyn-contract" }
alloy-eips = { version = "0.1.0", path = "crates/eips" }
alloy-genesis = { version = "0.1.0", path = "crates/genesis" }
alloy-json-rpc = { version = "0.1.0", path = "crates/json-rpc" }
alloy-network = { version = "0.1.0", path = "crates/network" }
alloy-node-bindings = { version = "0.1.0", path = "crates/node-bindings" }
alloy-providers = { version = "0.1.0", path = "crates/providers" }
alloy-pubsub = { version = "0.1.0", path = "crates/pubsub" }
alloy-rpc-client = { version = "0.1.0", path = "crates/rpc-client" }
alloy-rpc-engine-types = { version = "0.1.0", path = "crates/rpc-engine-types" }
Expand All @@ -36,9 +39,12 @@ alloy-transport = { version = "0.1.0", path = "crates/transport" }
alloy-transport-http = { version = "0.1.0", path = "crates/transport-http" }
alloy-transport-ipc = { version = "0.1.0", path = "crates/transport-ipc" }
alloy-transport-ws = { version = "0.1.0", path = "crates/transport-ws" }
alloy-genesis = {version = "0.1.0", path = "crates/genesis" }
alloy-primitives = { version = "0.6", default-features = false, features = ["std"] }
alloy-sol-types = { version = "0.6", default-features = false, features = ["std"] }

alloy-primitives = "0.6"
alloy-sol-types = "0.6"
alloy-dyn-abi = "0.6"
alloy-json-abi = "0.6"

alloy-rlp = "0.3"

# ethereum
Expand Down
21 changes: 21 additions & 0 deletions crates/dyn-contract/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "alloy-dyn-contract"
description = "Helpers for interacting with contracts that do not have a known ABI at compile time"

version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
exclude.workspace = true

[dependencies]
alloy-providers.workspace = true
alloy-rpc-types.workspace = true
alloy-transport.workspace = true

alloy-dyn-abi.workspace = true
alloy-json-abi.workspace = true
alloy-primitives.workspace = true
6 changes: 6 additions & 0 deletions crates/dyn-contract/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 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.
172 changes: 172 additions & 0 deletions crates/dyn-contract/src/call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
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<P> {
// todo: this will not work with `send_transaction` and does not differentiate between EIP-1559
// and legacy tx
request: CallRequest,
block: Option<BlockId>,
state: Option<StateOverride>,
provider: P,
// todo: only used to decode - should it be some type D to dedupe with `sol!` contracts?
function: Function,
}

impl<P> CallBuilder<P> {
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<P> CallBuilder<P>
where
P: TempProvider,
{
/// Returns the estimated gas cost for the underlying transaction to be executed
pub async fn estimate_gas(&self) -> Result<U256> {
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<Vec<DynSolValue>> {
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<Bytes> {
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<P> IntoFuture for CallBuilder<P>
where
P: TempProvider + 'static,
{
type Output = Result<Vec<DynSolValue>>;

#[cfg(target_arch = "wasm32")]
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;

#[cfg(not(target_arch = "wasm32"))]
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;

fn into_future(self) -> Self::IntoFuture {
#[allow(clippy::redundant_async_block)]
Box::pin(async move { self.call().await })
}
}

impl<P> std::fmt::Debug for CallBuilder<P> {
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()
}
}
58 changes: 58 additions & 0 deletions crates/dyn-contract/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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<T, E = Error> = core::result::Result<T, E>;

/// 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<AbiError> for Error {
fn from(error: AbiError) -> Self {
Self::AbiError(error)
}
}

impl From<TransportError> 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),
}
}
}
107 changes: 107 additions & 0 deletions crates/dyn-contract/src/instance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use crate::{CallBuilder, Interface, Result};
use alloy_dyn_abi::{DynSolValue, JsonAbiExt};
use alloy_json_abi::JsonAbi;
use alloy_primitives::{Address, Selector};
use alloy_providers::provider::TempProvider;

/// 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).
pub struct ContractInstance<P> {
address: Address,
provider: P,
interface: Interface,
}

impl<P> ContractInstance<P> {
/// Creates a new contract from the provided address, provider, and interface.
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 ABI.
pub const fn abi(&self) -> &JsonAbi {
self.interface.abi()
}

/// Returns a reference to the contract's provider.
pub const fn provider_ref(&self) -> &P {
&self.provider
}
}

impl<P> ContractInstance<P>
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<P> {
let mut this = self.clone();
this.address = address;
this
}
}

impl<P: TempProvider + Clone> ContractInstance<P> {
/// 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<CallBuilder<P>> {
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()))
}

/// Returns a transaction builder for the provided function selector.
pub fn function_from_selector(
&self,
selector: &Selector,
args: &[DynSolValue],
) -> Result<CallBuilder<P>> {
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<P> Clone for ContractInstance<P>
where
P: Clone,
{
fn clone(&self) -> Self {
ContractInstance {
address: self.address,
provider: self.provider.clone(),
interface: self.interface.clone(),
}
}
}

impl<P> std::ops::Deref for ContractInstance<P> {
type Target = Interface;

fn deref(&self) -> &Self::Target {
&self.interface
}
}

impl<P> std::fmt::Debug for ContractInstance<P> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ContractInstance").field("address", &self.address).finish()
}
}
Loading

0 comments on commit ba70833

Please sign in to comment.