Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: alloy-dyn-contract #149

Merged
merged 18 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ rustdoc-args = ["--cfg", "docsrs"]

[workspace.dependencies]
alloy-consensus = { version = "0.1.0", path = "crates/consensus" }
alloy-dyn-abi = "0.6.0"
alloy-eips = { version = "0.1.0", path = "crates/eips" }
alloy-json-abi = "0.6.0"
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-pubsub = { version = "0.1.0", path = "crates/pubsub" }
alloy-providers = { version = "0.1.0", path = "crates/providers" }
alloy-rpc-client = { version = "0.1.0", path = "crates/rpc-client" }
alloy-rpc-engine-types = { version = "0.1.0", path = "crates/rpc-engine-types" }
alloy-rpc-trace-types = { version = "0.1.0", path = "crates/rpc-trace-types" }
Expand Down
18 changes: 18 additions & 0 deletions crates/alloy-dyn-contract/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "alloy-dyn-contract"
onbjerg marked this conversation as resolved.
Show resolved Hide resolved
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-dyn-abi.workspace = true
alloy-json-abi.workspace = true
alloy-primitives.workspace = true
alloy-providers.workspace = true
alloy-rpc-types.workspace = true
alloy-transport.workspace = true
6 changes: 6 additions & 0 deletions crates/alloy-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.
187 changes: 187 additions & 0 deletions crates/alloy-dyn-contract/src/call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use crate::error::{Error, 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.
pub struct CallBuilder<P> {
// todo: this will not work with `send_transaction` and does not differentiate between EIP-1559
// and legacy tx
request: CallRequest,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having access to the CallRequest would be ideal. This way we can get around not having send_transaction on the provider by transferring the fields manually to a signable/rlp-able tx type from alloy consensus e.g TxLegacy

// todo: only used to decode - should it be some type D to dedupe with `sol!` contracts?
function: Function,
block: Option<BlockId>,
state: Option<StateOverride>,
provider: P,
}

impl<P> CallBuilder<P> {
pub(crate) fn new(provider: P, function: Function, input: Bytes) -> Self {
let request = CallRequest {
input: CallInput { input: Some(input), ..Default::default() },
..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.data.clone()
}
}

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 = if let Some(state) = &self.state {
self.provider
.call_with_overrides(self.request.clone(), self.block, state.clone())
.await
.map_err(Error::from)?
} else {
self.provider.call(self.request.clone(), self.block).await.map_err(Error::from)?
};

// decode output
let data = self.function.abi_decode_output(bytes.as_ref(), true)?;

Ok(data)
}

/// Signs and broadcasts the provided transaction
pub async fn send(&self) -> Result<()> {
todo!()
}
}

impl<P> Clone for CallBuilder<P>
where
P: Clone,
{
fn clone(&self) -> Self {
CallBuilder {
request: self.request.clone(),
function: self.function.clone(),
block: self.block,
onbjerg marked this conversation as resolved.
Show resolved Hide resolved
state: self.state.clone(),
provider: self.provider.clone(),
}
}
}

/// [`CallBuilder`] can be turned into a [`Future`] automatically with `.await`.
///
/// Defaults to calling [`CallBuilder::call`].
impl<P> IntoFuture for CallBuilder<P>
where
Self: 'static,
P: TempProvider,
{
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>>;
onbjerg marked this conversation as resolved.
Show resolved Hide resolved

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()
}
}
63 changes: 63 additions & 0 deletions crates/alloy-dyn-contract/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use std::fmt;

use alloy_dyn_abi::Error as AbiError;
use alloy_primitives::{hex, Selector};
use alloy_transport::TransportError;

/// 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 {} does not exist",
hex::encode(selector)
onbjerg marked this conversation as resolved.
Show resolved Hide resolved
)
}

Self::AbiError(e) => e.fmt(f),
Self::TransportError(e) => e.fmt(f),
}
}
}
Loading
Loading