Skip to content

Commit

Permalink
Feature: Joinable transaction fillers (#426)
Browse files Browse the repository at this point in the history
* feature: TxFiller

* lint: clippy

* doc: CONSIDER

* doc: more notes

* fix: get rid of ugly lifetimes

* fix: docs and lints

* fix: remove populate functions

* nit: remove commented code

* feature: FillerControlFlow

* doc: lifecycle notes

* refactor: request -> prepare

* lint: clippy

* fix: missing block in absorb

* fix: absorb preserves association

* refactor: additional info in missing

* fix: impl_future macro

* fix: resolve considers

* refactor: gas layer to gas filler

* refactor: improve provider builder

* refactor: filler is outmost layer

* refactor: protect against double-fill and add anvil shortcut

* doc: improve docstrings for noncemanager and gas filler

* fix: delete unused types

* refactor: layers to fillers

* feature: chain id filler

* refactor: send_transaction_inner and SendableTx

* wip: sig filler

* refactor: SignerFiller

* fix: remove clone

* docs: fix some

* fix: complete todo

* feature: anvil feature for alloy-provider

* wip: tests

* fix: apply changes from other PRs

* chore: fmt

* feature: on_anvil

* fix: workaround anvil gas est issue

* fix: doctests

* fix: anvil => request

* fix: test

* chore: note about blocking on TODO

* feature: local usage error

* fix: review nits

* Update crates/provider/src/fillers/mod.rs

Co-authored-by: Oliver Nordbjerg <[email protected]>

* fix: capitalization so @DaniPopes doesn't hurt me

---------

Co-authored-by: Oliver Nordbjerg <[email protected]>
  • Loading branch information
prestwich and onbjerg authored Apr 8, 2024
1 parent c4cbdf5 commit da41ddb
Show file tree
Hide file tree
Showing 26 changed files with 1,544 additions and 796 deletions.
2 changes: 1 addition & 1 deletion crates/alloy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ contract = ["dep:alloy-contract", "dyn-abi", "json-abi", "json", "sol-types"]
eips = ["dep:alloy-eips"]
genesis = ["dep:alloy-genesis"]
network = ["dep:alloy-network"]
node-bindings = ["dep:alloy-node-bindings"]
node-bindings = ["dep:alloy-node-bindings", "alloy-provider?/anvil"]

# providers
providers = ["dep:alloy-provider"]
Expand Down
2 changes: 1 addition & 1 deletion crates/contract/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ sol! {
}
// Build a provider.
let provider = ProviderBuilder::new().with_recommended_layers().on_builtin("http://localhost:8545").await?;
let provider = ProviderBuilder::new().with_recommended_fillers().on_builtin("http://localhost:8545").await?;
// If `#[sol(bytecode = "0x...")]` is provided, the contract can be deployed with `MyContract::deploy`,
// and a new instance will be created.
Expand Down
12 changes: 12 additions & 0 deletions crates/eips/src/eip2930.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ pub struct AccessList(
pub Vec<AccessListItem>,
);

impl From<Vec<AccessListItem>> for AccessList {
fn from(list: Vec<AccessListItem>) -> Self {
Self(list)
}
}

impl From<AccessList> for Vec<AccessListItem> {
fn from(this: AccessList) -> Self {
this.0
}
}

impl AccessList {
/// Converts the list into a vec, expected by revm
pub fn flattened(&self) -> Vec<(Address, Vec<U256>)> {
Expand Down
31 changes: 28 additions & 3 deletions crates/json-rpc/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ pub enum RpcError<E, ErrResp = Box<RawValue>> {
#[error("unsupported feature: {0}")]
UnsupportedFeature(&'static str),

/// Returned when a local pre-processing step fails. This allows custom
/// errors from local signers or request pre-processors.
#[error("local usage error: {0}")]
LocalUsageError(#[source] Box<dyn std::error::Error + Send + Sync>),

/// JSON serialization error.
#[error("serialization error: {0}")]
SerError(
Expand Down Expand Up @@ -52,12 +57,22 @@ impl<E, ErrResp> RpcError<E, ErrResp>
where
ErrResp: RpcReturn,
{
/// Instantiate a new `TransportError` from an error response.
/// Instantiate a new `ErrorResp` from an error response.
pub const fn err_resp(err: ErrorPayload<ErrResp>) -> Self {
Self::ErrorResp(err)
}

/// Instantiate a new `TransportError` from a [`serde_json::Error`] and the
/// Instantiate a new `LocalUsageError` from a custom error.
pub fn local_usage(err: impl std::error::Error + Send + Sync + 'static) -> Self {
Self::LocalUsageError(Box::new(err))
}

/// Instantiate a new `LocalUsageError` from a custom error message.
pub fn local_usage_str(err: &str) -> Self {
Self::LocalUsageError(err.into())
}

/// Instantiate a new `DeserError` from a [`serde_json::Error`] and the
/// text. This should be called when the error occurs during
/// deserialization.
///
Expand All @@ -76,7 +91,7 @@ where
}

impl<E, ErrResp> RpcError<E, ErrResp> {
/// Instantiate a new `TransportError` from a [`serde_json::Error`]. This
/// Instantiate a new `SerError` from a [`serde_json::Error`]. This
/// should be called when the error occurs during serialization.
pub const fn ser_err(err: serde_json::Error) -> Self {
Self::SerError(err)
Expand Down Expand Up @@ -107,6 +122,16 @@ impl<E, ErrResp> RpcError<E, ErrResp> {
matches!(self, Self::NullResp)
}

/// Check if the error is an unsupported feature error.
pub const fn is_unsupported_feature(&self) -> bool {
matches!(self, Self::UnsupportedFeature(_))
}

/// Check if the error is a local usage error.
pub const fn is_local_usage_error(&self) -> bool {
matches!(self, Self::LocalUsageError(_))
}

/// Fallible conversion to an error response.
pub const fn as_error_resp(&self) -> Option<&ErrorPayload<ErrResp>> {
match self {
Expand Down
28 changes: 23 additions & 5 deletions crates/network/src/any/builder.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::ops::{Deref, DerefMut};

use alloy_consensus::BlobTransactionSidecar;
use alloy_rpc_types::{TransactionRequest, WithOtherFields};
use alloy_rpc_types::{AccessList, TransactionRequest, WithOtherFields};

use crate::{
any::AnyNetwork, ethereum::build_unsigned, BuilderResult, Network, TransactionBuilder,
Expand Down Expand Up @@ -96,18 +96,36 @@ impl TransactionBuilder<AnyNetwork> for WithOtherFields<TransactionRequest> {
self.deref_mut().set_gas_limit(gas_limit);
}

fn build_unsigned(self) -> BuilderResult<<AnyNetwork as Network>::UnsignedTx> {
build_unsigned::<AnyNetwork>(self.inner)
/// Get the EIP-2930 access list for the transaction.
fn access_list(&self) -> Option<&AccessList> {
self.deref().access_list()
}

/// Sets the EIP-2930 access list.
fn set_access_list(&mut self, access_list: AccessList) {
self.deref_mut().set_access_list(access_list)
}

fn get_blob_sidecar(&self) -> Option<&BlobTransactionSidecar> {
self.deref().get_blob_sidecar()
fn blob_sidecar(&self) -> Option<&BlobTransactionSidecar> {
self.deref().blob_sidecar()
}

fn set_blob_sidecar(&mut self, sidecar: BlobTransactionSidecar) {
self.deref_mut().set_blob_sidecar(sidecar)
}

fn can_build(&self) -> bool {
self.deref().can_build()
}

fn can_submit(&self) -> bool {
self.deref().can_submit()
}

fn build_unsigned(self) -> BuilderResult<<AnyNetwork as Network>::UnsignedTx> {
build_unsigned::<AnyNetwork>(self.inner)
}

async fn build<S: crate::NetworkSigner<AnyNetwork>>(
self,
signer: &S,
Expand Down
60 changes: 46 additions & 14 deletions crates/network/src/ethereum/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ use crate::{
};
use alloy_consensus::{
BlobTransactionSidecar, TxEip1559, TxEip2930, TxEip4844, TxEip4844Variant, TxLegacy,
TypedTransaction,
};
use alloy_primitives::{Address, TxKind};
use alloy_rpc_types::request::TransactionRequest;
use alloy_primitives::{Address, Bytes, ChainId, TxKind, U256};
use alloy_rpc_types::{request::TransactionRequest, AccessList};

impl TransactionBuilder<Ethereum> for alloy_rpc_types::TransactionRequest {
fn chain_id(&self) -> Option<alloy_primitives::ChainId> {
impl TransactionBuilder<Ethereum> for TransactionRequest {
fn chain_id(&self) -> Option<ChainId> {
self.chain_id
}

fn set_chain_id(&mut self, chain_id: alloy_primitives::ChainId) {
fn set_chain_id(&mut self, chain_id: ChainId) {
self.chain_id = Some(chain_id);
}

Expand All @@ -24,11 +25,11 @@ impl TransactionBuilder<Ethereum> for alloy_rpc_types::TransactionRequest {
self.nonce = Some(nonce);
}

fn input(&self) -> Option<&alloy_primitives::Bytes> {
fn input(&self) -> Option<&Bytes> {
self.input.input()
}

fn set_input(&mut self, input: alloy_primitives::Bytes) {
fn set_input(&mut self, input: Bytes) {
self.input.input = Some(input);
}

Expand All @@ -40,22 +41,22 @@ impl TransactionBuilder<Ethereum> for alloy_rpc_types::TransactionRequest {
self.from = Some(from);
}

fn to(&self) -> Option<alloy_primitives::TxKind> {
fn to(&self) -> Option<TxKind> {
self.to.map(TxKind::Call).or(Some(TxKind::Create))
}

fn set_to(&mut self, to: alloy_primitives::TxKind) {
fn set_to(&mut self, to: TxKind) {
match to {
TxKind::Create => self.to = None,
TxKind::Call(to) => self.to = Some(to),
}
}

fn value(&self) -> Option<alloy_primitives::U256> {
fn value(&self) -> Option<U256> {
self.value
}

fn set_value(&mut self, value: alloy_primitives::U256) {
fn set_value(&mut self, value: U256) {
self.value = Some(value)
}

Expand Down Expand Up @@ -99,11 +100,15 @@ impl TransactionBuilder<Ethereum> for alloy_rpc_types::TransactionRequest {
self.gas = Some(gas_limit);
}

fn build_unsigned(self) -> BuilderResult<<Ethereum as Network>::UnsignedTx> {
build_unsigned::<Ethereum>(self)
fn access_list(&self) -> Option<&AccessList> {
self.access_list.as_ref()
}

fn set_access_list(&mut self, access_list: AccessList) {
self.access_list = Some(access_list);
}

fn get_blob_sidecar(&self) -> Option<&BlobTransactionSidecar> {
fn blob_sidecar(&self) -> Option<&BlobTransactionSidecar> {
self.sidecar.as_ref()
}

Expand All @@ -112,6 +117,33 @@ impl TransactionBuilder<Ethereum> for alloy_rpc_types::TransactionRequest {
self.sidecar = Some(sidecar);
}

fn can_submit(&self) -> bool {
// value and data may be None. If they are, they will be set to default.
// gas fields and nonce may be None, if they are, they will be populated
// with default values by the RPC server
self.to.is_some() && self.from.is_some()
}

fn can_build(&self) -> bool {
// value and data may be none. If they are, they will be set to default
// values.

// chain_id and from may be none.
let common = self.to.is_some() && self.gas.is_some() && self.nonce.is_some();

let legacy = self.gas_price.is_some();
let eip2930 = legacy && self.access_list().is_some();

let eip1559 = self.max_fee_per_gas.is_some() && self.max_priority_fee_per_gas.is_some();

let eip4844 = eip1559 && self.sidecar.is_some();
common && (legacy || eip2930 || eip1559 || eip4844)
}

fn build_unsigned(self) -> BuilderResult<TypedTransaction> {
build_unsigned::<Ethereum>(self)
}

async fn build<S: NetworkSigner<Ethereum>>(
self,
signer: &S,
Expand Down
10 changes: 5 additions & 5 deletions crates/network/src/ethereum/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ impl EthereumSigner {
Self(Arc::new(signer))
}

async fn sign_transaction(
async fn sign_transaction_inner(
&self,
tx: &mut dyn SignableTransaction<Signature>,
) -> alloy_signer::Result<Signature> {
Expand All @@ -49,19 +49,19 @@ where
async fn sign_transaction(&self, tx: TypedTransaction) -> alloy_signer::Result<TxEnvelope> {
match tx {
TypedTransaction::Legacy(mut t) => {
let sig = self.sign_transaction(&mut t).await?;
let sig = self.sign_transaction_inner(&mut t).await?;
Ok(t.into_signed(sig).into())
}
TypedTransaction::Eip2930(mut t) => {
let sig = self.sign_transaction(&mut t).await?;
let sig = self.sign_transaction_inner(&mut t).await?;
Ok(t.into_signed(sig).into())
}
TypedTransaction::Eip1559(mut t) => {
let sig = self.sign_transaction(&mut t).await?;
let sig = self.sign_transaction_inner(&mut t).await?;
Ok(t.into_signed(sig).into())
}
TypedTransaction::Eip4844(mut t) => {
let sig = self.sign_transaction(&mut t).await?;
let sig = self.sign_transaction_inner(&mut t).await?;
Ok(t.into_signed(sig).into())
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/network/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ pub trait ReceiptResponse {
/// Networks are only containers for types, so it is recommended to use ZSTs for their definition.
// todo: block responses are ethereum only, so we need to include this in here too, or make `Block`
// generic over tx/header type
pub trait Network: Clone + Copy + Sized + Send + Sync + 'static {
pub trait Network: std::fmt::Debug + Clone + Copy + Sized + Send + Sync + 'static {
// -- Consensus types --

/// The network transaction envelope type.
type TxEnvelope: Eip2718Envelope;
type TxEnvelope: Eip2718Envelope + std::fmt::Debug;

/// An enum over the various transaction types.
type UnsignedTx: From<Self::TxEnvelope>;
Expand Down
23 changes: 22 additions & 1 deletion crates/network/src/transaction/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::signer::NetworkSigner;
use crate::Network;
use alloy_consensus::BlobTransactionSidecar;
use alloy_primitives::{Address, Bytes, ChainId, TxKind, U256};
use alloy_rpc_types::AccessList;
use futures_utils_wasm::impl_future;

/// Error type for transaction builders.
Expand Down Expand Up @@ -191,8 +192,20 @@ pub trait TransactionBuilder<N: Network>: Default + Sized + Send + Sync + 'stati
self
}

/// Get the EIP-2930 access list for the transaction.
fn access_list(&self) -> Option<&AccessList>;

/// Sets the EIP-2930 access list.
fn set_access_list(&mut self, access_list: AccessList);

/// Builder-pattern method for setting the access list.
fn with_access_list(mut self, access_list: AccessList) -> Self {
self.set_access_list(access_list);
self
}

/// Gets the EIP-4844 blob sidecar of the transaction.
fn get_blob_sidecar(&self) -> Option<&BlobTransactionSidecar>;
fn blob_sidecar(&self) -> Option<&BlobTransactionSidecar>;

/// Sets the EIP-4844 blob sidecar of the transaction.
///
Expand All @@ -206,6 +219,14 @@ pub trait TransactionBuilder<N: Network>: Default + Sized + Send + Sync + 'stati
self
}

/// True if the builder contains all necessary information to be submitted
/// to the `eth_sendTransaction` endpoint.
fn can_submit(&self) -> bool;

/// True if the builder contains all necessary information to be built into
/// a valid transaction.
fn can_build(&self) -> bool;

/// Build an unsigned, but typed, transaction.
fn build_unsigned(self) -> BuilderResult<N::UnsignedTx>;

Expand Down
13 changes: 11 additions & 2 deletions crates/network/src/transaction/signer.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::Network;
use crate::{Network, TransactionBuilder};
use alloy_consensus::SignableTransaction;
use async_trait::async_trait;

Expand All @@ -9,9 +9,18 @@ use async_trait::async_trait;
/// [`TxSigner`] to signify signing capability for specific signature types.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NetworkSigner<N: Network>: Send + Sync {
pub trait NetworkSigner<N: Network>: std::fmt::Debug + Send + Sync {
/// Asynchronously sign an unsigned transaction.
async fn sign_transaction(&self, tx: N::UnsignedTx) -> alloy_signer::Result<N::TxEnvelope>;

/// Asynchronously sign a transaction request.
async fn sign_request(
&self,
request: N::TransactionRequest,
) -> alloy_signer::Result<N::TxEnvelope> {
let tx = request.build_unsigned().map_err(alloy_signer::Error::other)?;
self.sign_transaction(tx).await
}
}

/// Asynchronous transaction signer, capable of signing any [`SignableTransaction`] for the given
Expand Down
Loading

0 comments on commit da41ddb

Please sign in to comment.