-
Notifications
You must be signed in to change notification settings - Fork 256
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
10 changed files
with
587 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
Oops, something went wrong.