use alloy_primitives::{Address, Bytes, B256, U256, keccak256, ChainId};
use alloy_rlp::Encodable;
pub const TX_TYPE_USD_MULTI_TOKEN: u8 = 0x7a;
pub const FEE_PAYER_SIGNATURE_MAGIC_BYTE: u8 = 0x7b;
#[derive(Debug, Clone, Default)]
pub struct AccessListItem {
pub address: Address,
pub storage_keys: Vec<B256>,
}
#[derive(Debug, Clone)]
pub struct TxBuilder {
pub chain_id: u64,
pub nonce: u64,
pub gas_limit: u64,
pub to: Option<Address>,
pub value: U256,
pub data: Bytes,
pub access_list: Vec<AccessListItem>, pub max_priority_fee_per_gas: u128,
pub max_fee_per_gas: u128,
pub fee_token: Address,
pub fee_payer: Option<Address>,
pub max_fee_per_gas_usd: Option<u128>,
pub fee_payer_signature: Option<Bytes>,
}
impl Default for TxBuilder {
fn default() -> Self {
Self::new()
}
}
impl TxBuilder {
pub fn new() -> Self {
Self {
chain_id: 0,
nonce: 0,
gas_limit: 21000,
to: Some(Address::ZERO),
value: U256::ZERO,
data: Bytes::new(),
access_list: Vec::new(), max_priority_fee_per_gas: 0,
max_fee_per_gas: 0,
fee_token: Address::ZERO,
fee_payer: None, max_fee_per_gas_usd: Some(40_000_000_000), fee_payer_signature: None,
}
}
pub fn chain_id(mut self, chain_id: u64) -> Self {
self.chain_id = chain_id;
self
}
pub fn nonce(mut self, nonce: u64) -> Self {
self.nonce = nonce;
self
}
pub fn gas_limit(mut self, gas_limit: u64) -> Self {
self.gas_limit = gas_limit;
self
}
pub fn to(mut self, to: Address) -> Self {
self.to = Some(to);
self
}
pub fn to_address(mut self, address: Address) -> Self {
self.to = Some(address);
self
}
pub fn create(mut self) -> Self {
self.to = None;
self
}
pub fn value(mut self, value: U256) -> Self {
self.value = value;
self
}
pub fn data(mut self, data: Bytes) -> Self {
self.data = data;
self
}
pub fn max_priority_fee_per_gas(mut self, max_priority_fee_per_gas: u128) -> Self {
self.max_priority_fee_per_gas = max_priority_fee_per_gas;
self
}
pub fn max_fee_per_gas(mut self, max_fee_per_gas: u128) -> Self {
self.max_fee_per_gas = max_fee_per_gas;
self
}
pub fn fee_token(mut self, fee_token: Address) -> Self {
self.fee_token = fee_token;
self
}
pub fn fee_payer(mut self, fee_payer: Option<Address>) -> Self {
self.fee_payer = fee_payer;
self
}
pub fn max_fee_per_gas_usd(mut self, max_fee_per_gas_usd: u128) -> Self {
self.max_fee_per_gas_usd = Some(max_fee_per_gas_usd);
self
}
pub fn fee_payer_signature(mut self, signature: Bytes) -> Self {
self.fee_payer_signature = Some(signature);
self
}
pub fn erc20_transfer(mut self, token: Address, to: Address, amount: U256) -> Self {
let mut data = vec![0xa9, 0x05, 0x9c, 0xbb];
let mut recipient = [0u8; 32];
recipient[12..].copy_from_slice(to.as_slice());
data.extend_from_slice(&recipient);
let mut amount_padded = [0u8; 32];
let amount_bytes: [u8; 32] = amount.to_be_bytes();
amount_padded.copy_from_slice(&amount_bytes);
data.extend_from_slice(&amount_padded);
self.to = Some(token);
self.data = Bytes::from(data);
self
}
pub fn build(&self) -> TxFields {
TxFields {
chain_id: self.chain_id,
nonce: self.nonce,
gas_limit: self.gas_limit,
to: self.to,
value: self.value,
data: self.data.clone(),
access_list: self.access_list.clone(), max_priority_fee_per_gas: self.max_priority_fee_per_gas,
max_fee_per_gas: self.max_fee_per_gas,
fee_token: self.fee_token,
fee_payer: self.fee_payer.unwrap_or(Address::ZERO),
max_fee_per_gas_usd: self.max_fee_per_gas_usd.unwrap_or(40_000_000_000),
fee_payer_signature: self.fee_payer_signature.clone(),
}
}
pub fn signature_hash(&self) -> B256 {
let fields = self.build();
let payload_len = fields.payload_len_for_signing();
let mut full_buf = Vec::new();
full_buf.push(TX_TYPE_USD_MULTI_TOKEN);
alloy_rlp::Header { list: true, payload_length: payload_len }.encode(&mut full_buf);
fields.encode_for_signing(&mut full_buf);
let hash = keccak256(&full_buf);
hash
}
pub fn sign(&self, signer: &impl Signer) -> Result<SignedTx, Box<dyn std::error::Error>> {
let sender = signer.address();
let fee_payer = self.fee_payer.unwrap_or(sender);
let hash = {
let mut resolved = self.clone();
resolved.fee_payer = Some(fee_payer);
resolved.signature_hash()
};
let signature = signer.sign_hash(&hash)?;
let fee_payer_signature = if self.fee_payer_signature.is_none() {
if fee_payer == sender {
Some(signature.to_bytes())
} else {
return Err("fee_payer != sender requires external fee_payer_signature".into());
}
} else {
self.fee_payer_signature.clone()
};
let fields = TxFields {
chain_id: self.chain_id,
nonce: self.nonce,
gas_limit: self.gas_limit,
to: self.to,
value: self.value.clone(),
data: self.data.clone(),
access_list: self.access_list.clone(), max_priority_fee_per_gas: self.max_priority_fee_per_gas,
max_fee_per_gas: self.max_fee_per_gas,
fee_token: self.fee_token,
fee_payer,
max_fee_per_gas_usd: self.max_fee_per_gas_usd.unwrap_or(40_000_000_000),
fee_payer_signature,
};
let y_parity = signature.y_parity();
let raw_tx = fields.encode_signed(y_parity, signature.r(), signature.s());
let tx_hash = keccak256(&raw_tx);
Ok(SignedTx {
raw_transaction: format!("0x{}", hex::encode(&raw_tx)),
transaction_hash: tx_hash,
chain_id: fields.chain_id,
nonce: fields.nonce,
gas_limit: fields.gas_limit,
to: fields.to,
value: fields.value,
data: fields.data,
max_priority_fee_per_gas: fields.max_priority_fee_per_gas,
max_fee_per_gas: fields.max_fee_per_gas,
fee_token: fields.fee_token,
fee_payer: fields.fee_payer,
max_fee_per_gas_usd_attodollars: fields.max_fee_per_gas_usd,
fee_payer_signature: fields.fee_payer_signature,
v: y_parity,
r: signature.r(),
s: signature.s(),
})
}
}
pub trait Signer {
fn sign_hash(&self, hash: &B256) -> Result<Signature, Box<dyn std::error::Error>>;
fn address(&self) -> Address;
}
#[derive(Debug, Clone)]
pub struct Signature {
pub r: B256,
pub s: B256,
pub v: u8,
}
impl Signature {
pub fn new(r: B256, s: B256, v: u8) -> Self {
Self { r, s, v }
}
pub fn y_parity(&self) -> u8 {
self.v
}
pub fn r(&self) -> B256 {
self.r
}
pub fn s(&self) -> B256 {
self.s
}
pub fn to_bytes(&self) -> Bytes {
let mut buf = Vec::with_capacity(65);
buf.extend_from_slice(self.r.as_slice()); buf.extend_from_slice(self.s.as_slice()); let v_legacy = 27 + self.v; buf.push(v_legacy); Bytes::from(buf)
}
}
#[derive(Debug, Clone)]
pub struct SignedTx {
pub raw_transaction: String,
pub transaction_hash: B256,
pub chain_id: u64,
pub nonce: u64,
pub gas_limit: u64,
pub to: Option<Address>,
pub value: U256,
pub data: Bytes,
pub max_priority_fee_per_gas: u128,
pub max_fee_per_gas: u128,
pub fee_token: Address,
pub fee_payer: Address,
pub max_fee_per_gas_usd_attodollars: u128,
pub fee_payer_signature: Option<Bytes>,
pub v: u8,
pub r: B256,
pub s: B256,
}
#[derive(Debug, Clone)]
pub struct TxFields {
pub chain_id: u64,
pub nonce: u64,
pub gas_limit: u64,
pub to: Option<Address>,
pub value: U256,
pub data: Bytes,
pub access_list: Vec<AccessListItem>, pub max_priority_fee_per_gas: u128,
pub max_fee_per_gas: u128,
pub fee_token: Address,
pub fee_payer: Address,
pub max_fee_per_gas_usd: u128,
pub fee_payer_signature: Option<Bytes>,
}
impl TxFields {
fn encode_access_list(access_list: &[AccessListItem], out: &mut Vec<u8>) {
if access_list.is_empty() {
alloy_rlp::Header { list: true, payload_length: 0 }.encode(out);
} else {
let payload_len = access_list.iter().map(|item| {
item.address.length() + item.storage_keys.iter().map(|k| k.length()).sum::<usize>()
}).sum::<usize>() + access_list.len();
alloy_rlp::Header { list: true, payload_length: payload_len }.encode(out);
for item in access_list {
let inner_len = item.address.length() +
item.storage_keys.iter().map(|k| k.length()).sum::<usize>() +
1; alloy_rlp::Header { list: true, payload_length: inner_len }.encode(out);
item.address.encode(out);
if item.storage_keys.is_empty() {
alloy_rlp::Header { list: true, payload_length: 0 }.encode(out);
} else {
let keys_len: usize = item.storage_keys.iter().map(|k| k.length()).sum();
alloy_rlp::Header { list: true, payload_length: keys_len }.encode(out);
for key in &item.storage_keys {
key.encode(out);
}
}
}
}
}
pub fn encode_for_signing(&self, out: &mut Vec<u8>) {
ChainId::from(self.chain_id).encode(out);
self.nonce.encode(out);
self.max_priority_fee_per_gas.encode(out);
self.max_fee_per_gas.encode(out);
self.gas_limit.encode(out);
Self::encode_to(&self.to, out);
self.value.encode(out);
self.data.encode(out);
Self::encode_access_list(&self.access_list, out);
self.fee_token.encode(out);
self.fee_payer.encode(out);
self.max_fee_per_gas_usd.encode(out);
}
fn encode_to(to: &Option<Address>, out: &mut Vec<u8>) {
match to {
Some(addr) => addr.encode(out),
None => out.push(0x80), }
}
fn to_length(to: &Option<Address>) -> usize {
match to {
Some(addr) => addr.length(),
None => 1, }
}
fn access_list_len(access_list: &[AccessListItem]) -> usize {
if access_list.is_empty() {
return 1; }
let keys_len: usize = access_list.iter()
.map(|item| item.address.length() + item.storage_keys.iter().map(|k| k.length()).sum::<usize>() + 1)
.sum();
1 + keys_len
}
fn payload_len_for_signing(&self) -> usize {
self.chain_id.length() +
self.nonce.length() +
self.max_priority_fee_per_gas.length() +
self.max_fee_per_gas.length() +
self.gas_limit.length() +
Self::to_length(&self.to) +
self.value.length() +
self.data.length() +
Self::access_list_len(&self.access_list) +
self.fee_token.length() +
self.fee_payer.length() +
self.max_fee_per_gas_usd.length()
}
pub fn encode_signed(&self, y_parity: u8, r: B256, s: B256) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(TX_TYPE_USD_MULTI_TOKEN);
let payload_len = self.payload_len_for_signed(&r, &s);
alloy_rlp::Header { list: true, payload_length: payload_len }.encode(&mut buf);
self.chain_id.encode(&mut buf);
self.nonce.encode(&mut buf);
self.max_priority_fee_per_gas.encode(&mut buf);
self.max_fee_per_gas.encode(&mut buf);
self.gas_limit.encode(&mut buf);
Self::encode_to(&self.to, &mut buf);
self.value.encode(&mut buf);
self.data.encode(&mut buf);
Self::encode_access_list(&self.access_list, &mut buf);
self.fee_token.encode(&mut buf);
self.fee_payer.encode(&mut buf);
self.max_fee_per_gas_usd.encode(&mut buf);
if let Some(ref sig) = self.fee_payer_signature {
sig.encode(&mut buf);
} else {
Bytes::new().encode(&mut buf);
}
y_parity.encode(&mut buf);
U256::from_be_bytes(r.0).encode(&mut buf);
U256::from_be_bytes(s.0).encode(&mut buf);
buf
}
fn payload_len_for_signed(&self, r: &B256, s: &B256) -> usize {
let fee_payer_sig_len = self.fee_payer_signature.as_ref()
.map(|sig| sig.length())
.unwrap_or_else(|| Bytes::new().length());
self.payload_len_for_signing() +
fee_payer_sig_len +
1 + U256::from_be_bytes(r.0).length() +
U256::from_be_bytes(s.0).length()
}
}
pub fn fee_payer_signature_hash(
chain_id: u64,
nonce: u64,
gas_limit: u64,
fee_token: Address,
fee_payer: Address,
max_fee_per_gas_usd: u128,
sender: Address,
) -> B256 {
use alloy_rlp::{Encodable, Header};
let payload_len = chain_id.length()
+ nonce.length()
+ gas_limit.length()
+ fee_token.length()
+ fee_payer.length()
+ max_fee_per_gas_usd.length()
+ sender.length();
let mut buf = Vec::with_capacity(1 + Header { list: true, payload_length: payload_len }.length() + payload_len);
buf.push(FEE_PAYER_SIGNATURE_MAGIC_BYTE);
Header { list: true, payload_length: payload_len }.encode(&mut buf);
chain_id.encode(&mut buf);
nonce.encode(&mut buf);
gas_limit.encode(&mut buf);
fee_token.encode(&mut buf);
fee_payer.encode(&mut buf);
max_fee_per_gas_usd.encode(&mut buf);
sender.encode(&mut buf);
let hash = keccak256(&buf);
hash
}
pub fn create_transaction() -> TxBuilder {
TxBuilder::new()
}