Skip to content
Logo

Example: encode_decode_transaction_envelopes

To run this example:

  • Clone the examples repository: git clone git@github.com:alloy-rs/examples.git
  • Run: cargo run --locked -p examples-transactions --example encode_decode_transaction_envelopes
//! Encode and decode signed Ethereum transaction envelopes with EIP-2718.
//!
//! Always wrap signed transactions in [`TxEnvelope`] before encoding them. The envelope preserves
//! the transaction type byte and handles legacy transactions, which have no type byte.
 
use alloy::{
    consensus::{
        transaction::SignerRecoverable, SignableTransaction, Signed, TxEip1559, TxEip2930,
        TxEip4844, TxEip7702, TxEnvelope, TxLegacy, TxType,
    },
    eips::{
        eip2718::{Decodable2718, Encodable2718},
        eip2930::AccessList,
        eip7702::Authorization,
    },
    primitives::{address, b256, Address, Signature, TxKind, U256},
    rpc::types::Transaction as RpcTransaction,
    signers::{local::PrivateKeySigner, SignerSync},
};
use eyre::Result;
 
fn main() -> Result<()> {
    // Fixed test-only keys keep the bytes and hashes reproducible. Never use them on a real chain.
    let signer = PrivateKeySigner::from_bytes(&b256!(
        "0000000000000000000000000000000000000000000000000000000000000001"
    ))?;
    let authorization_signer = PrivateKeySigner::from_bytes(&b256!(
        "0000000000000000000000000000000000000000000000000000000000000002"
    ))?;
    let signer_address = signer.address();
    let to = address!("1111111111111111111111111111111111111111");
    let authorization = Authorization { chain_id: U256::from(1), address: to, nonce: 0 };
    let authorization_signature =
        authorization_signer.sign_hash_sync(&authorization.signature_hash())?;
    let signed_authorization = authorization.into_signed(authorization_signature);
    assert_eq!(signed_authorization.recover_authority()?, authorization_signer.address());
 
    let envelopes = [
        (
            TxType::Legacy,
            sign(
                TxLegacy {
                    chain_id: Some(1),
                    nonce: 0,
                    gas_price: 20_000_000_000,
                    gas_limit: 21_000,
                    to: TxKind::Call(to),
                    value: U256::from(1),
                    ..Default::default()
                },
                &signer,
            )?,
        ),
        (
            TxType::Eip2930,
            sign(
                TxEip2930 {
                    chain_id: 1,
                    nonce: 1,
                    gas_price: 20_000_000_000,
                    gas_limit: 21_000,
                    to: TxKind::Call(to),
                    value: U256::from(2),
                    access_list: AccessList::default(),
                    ..Default::default()
                },
                &signer,
            )?,
        ),
        (
            TxType::Eip1559,
            sign(
                TxEip1559 {
                    chain_id: 1,
                    nonce: 2,
                    max_fee_per_gas: 20_000_000_000,
                    max_priority_fee_per_gas: 1_000_000_000,
                    gas_limit: 21_000,
                    to: TxKind::Call(to),
                    value: U256::from(3),
                    access_list: AccessList::default(),
                    ..Default::default()
                },
                &signer,
            )?,
        ),
        (
            TxType::Eip4844,
            // This is the execution/block form. Submitting a blob transaction requires a sidecar;
            // see the `send_eip4844_transaction` example for the pooled representation.
            sign(
                TxEip4844 {
                    chain_id: 1,
                    nonce: 3,
                    max_fee_per_gas: 20_000_000_000,
                    max_priority_fee_per_gas: 1_000_000_000,
                    gas_limit: 100_000,
                    to,
                    value: U256::from(4),
                    access_list: AccessList::default(),
                    blob_versioned_hashes: vec![b256!(
                        "0100000000000000000000000000000000000000000000000000000000000000"
                    )],
                    max_fee_per_blob_gas: 1_000_000_000,
                    ..Default::default()
                },
                &signer,
            )?,
        ),
        (
            TxType::Eip7702,
            sign(
                TxEip7702 {
                    chain_id: 1,
                    nonce: 4,
                    max_fee_per_gas: 20_000_000_000,
                    max_priority_fee_per_gas: 1_000_000_000,
                    gas_limit: 50_000,
                    to,
                    value: U256::from(5),
                    access_list: AccessList::default(),
                    authorization_list: vec![signed_authorization],
                    ..Default::default()
                },
                &signer,
            )?,
        ),
    ];
 
    for (tx_type, envelope) in envelopes {
        round_trip(envelope, tx_type, signer_address)?;
    }
 
    // Ethereum's concrete JSON-RPC transaction response wraps a `TxEnvelope` with the `from`
    // address reported by the node.
    // `into_inner` removes the RPC-only block metadata without serializing through JSON again.
    let rpc_transaction: RpcTransaction = serde_json::from_str(RPC_TRANSACTION)?;
    let rpc_from = rpc_transaction.inner.signer();
    let reported_hash = *rpc_transaction.inner.tx_hash();
    let envelope: TxEnvelope = rpc_transaction.into_inner();
 
    // Verify the node-reported sender against the signature. The envelope initially retains the
    // hash reported in JSON, so decode its canonical bytes to independently recompute the hash.
    assert_eq!(envelope.recover_signer()?, rpc_from);
    assert_eq!(*envelope.tx_hash(), reported_hash);
    let encoded = envelope.encoded_2718();
    let decoded = TxEnvelope::decode_2718_exact(&encoded)?;
    assert_eq!(decoded.tx_type(), TxType::Eip1559);
    assert_eq!(decoded.recover_signer()?, rpc_from);
    assert_eq!(*decoded.tx_hash(), reported_hash);
 
    Ok(())
}
 
fn sign<T>(transaction: T, signer: &PrivateKeySigner) -> Result<TxEnvelope>
where
    T: SignableTransaction<Signature>,
    Signed<T>: Into<TxEnvelope>,
{
    let signature = signer.sign_hash_sync(&transaction.signature_hash())?;
    Ok(transaction.into_signed(signature).into())
}
 
fn round_trip(envelope: TxEnvelope, tx_type: TxType, signer: Address) -> Result<()> {
    let tx_hash = *envelope.tx_hash();
 
    // `encoded_2718` allocates. `encode_2718` appends to a caller-owned buffer.
    let encoded = envelope.encoded_2718();
    let mut reusable_buffer = Vec::with_capacity(envelope.encode_2718_len());
    envelope.encode_2718(&mut reusable_buffer);
    assert_eq!(encoded, reusable_buffer);
    reusable_buffer.clear();
    envelope.encode_2718(&mut reusable_buffer);
    assert_eq!(encoded, reusable_buffer);
    if tx_type == TxType::Legacy {
        assert!(encoded[0] >= 0xc0);
    } else {
        assert_eq!(encoded[0], tx_type as u8);
    }
 
    // Use the exact decoder for a buffer that must contain one envelope and no trailing bytes.
    let decoded = TxEnvelope::decode_2718_exact(&encoded)?;
    assert_eq!(decoded.tx_type(), tx_type);
    assert_eq!(*decoded.tx_hash(), tx_hash);
    assert_eq!(decoded.recover_signer()?, signer);
    assert_eq!(decoded, envelope);
 
    Ok(())
}
 
// Response from `eth_getTransactionByHash` on a local Anvil chain.
const RPC_TRANSACTION: &str = r#"{
  "hash": "0x018b2331d461a4aeedf6a1f9cc37463377578244e6a35216057a8370714e798f",
  "nonce": "0x1",
  "blockHash": "0x6e4e53d1de650d5a5ebed19b38321db369ef1dc357904284ecf4d89b8834969c",
  "blockNumber": "0x2",
  "transactionIndex": "0x0",
  "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
  "to": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
  "value": "0x0",
  "gasPrice": "0x3a29f0f8",
  "gas": "0x1c9c380",
  "maxFeePerGas": "0xba43b7400",
  "maxPriorityFeePerGas": "0x5f5e100",
  "input": "0xd09de08a",
  "r": "0xd309309a59a49021281cb6bb41d164c96eab4e50f0c1bd24c03ca336e7bc2bb7",
  "s": "0x28a7f089143d0a1355ebeb2a1b9f0e5ad9eca4303021c1400d61bc23c9ac5319",
  "v": "0x0",
  "yParity": "0x0",
  "chainId": "0x7a69",
  "accessList": [],
  "type": "0x2"
}"#;

Find the source code on Github here.