use crate::wallet;
const TYPE_BYTE: u8 = 0x76;
const SENDER_DOMAIN: u8 = 0x76;
const FEE_PAYER_DOMAIN: u8 = 0x78;
#[derive(Debug, Clone)]
pub struct TempoCall {
pub to: [u8; 20],
pub value_wei: u128,
pub input: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct TempoTx {
pub chain_id: u64,
pub max_priority_fee_per_gas: u128,
pub max_fee_per_gas: u128,
pub gas_limit: u128,
pub calls: Vec<TempoCall>,
pub access_list: Vec<AccessListItem>,
pub nonce_key: u128,
pub nonce: u128,
pub valid_before: Option<u64>,
pub valid_after: Option<u64>,
pub fee_token: Option<[u8; 20]>,
pub aa_authorization_list: Vec<SignedAuthorization>,
pub key_authorization: Option<KeyAuthorization>,
pub(crate) sponsored: bool,
}
#[derive(Debug, Clone)]
pub struct AccessListItem {
pub address: [u8; 20],
pub storage_keys: Vec<[u8; 32]>,
}
#[derive(Debug, Clone)]
pub struct SignedAuthorization {
pub chain_id: u64,
pub address: [u8; 20],
pub nonce: u64,
pub signature: [u8; 65],
}
#[derive(Debug, Clone)]
pub struct KeyAuthorization {
pub raw_rlp: Vec<u8>,
}
impl TempoTx {
pub fn sender_hash(&self) -> [u8; 32] {
let mut items = self.common_rlp_items();
if self.is_sponsored() {
items.push(wallet::rlp_bytes(&[])); items.push(vec![0x00]); } else {
items.push(rlp_fee_token(self.fee_token.as_ref())); items.push(wallet::rlp_bytes(&[])); }
items.push(rlp_authorization_list(&self.aa_authorization_list));
if self.key_authorization.is_some() {
items.push(rlp_key_authorization(self.key_authorization.as_ref()));
}
let body = wallet::rlp_list(&items);
let mut payload = Vec::with_capacity(1 + body.len());
payload.push(SENDER_DOMAIN);
payload.extend_from_slice(&body);
keccak(&payload)
}
pub fn fee_payer_hash(&self, sender_address: &[u8; 20]) -> [u8; 32] {
let mut items = self.common_rlp_items();
items.push(rlp_fee_token(self.fee_token.as_ref()));
items.push(wallet::rlp_bytes(sender_address));
items.push(rlp_authorization_list(&self.aa_authorization_list));
if self.key_authorization.is_some() {
items.push(rlp_key_authorization(self.key_authorization.as_ref()));
}
let body = wallet::rlp_list(&items);
let mut payload = Vec::with_capacity(1 + body.len());
payload.push(FEE_PAYER_DOMAIN);
payload.extend_from_slice(&body);
keccak(&payload)
}
pub fn serialize_signed(
&self,
sender_sig: &[u8; 65],
fee_payer_sig: Option<&[u8; 65]>,
) -> Vec<u8> {
let mut items = self.common_rlp_items();
items.push(rlp_fee_token(self.fee_token.as_ref()));
match fee_payer_sig {
Some(sig) => items.push(rlp_vrs_signature(sig)),
None => items.push(wallet::rlp_bytes(&[])), }
items.push(rlp_authorization_list(&self.aa_authorization_list));
if let Some(_ka) = self.key_authorization.as_ref() {
items.push(rlp_key_authorization(self.key_authorization.as_ref()));
}
items.push(rlp_compact_signature(sender_sig));
let body = wallet::rlp_list(&items);
let mut out = Vec::with_capacity(1 + body.len());
out.push(TYPE_BYTE);
out.extend_from_slice(&body);
out
}
pub fn is_sponsored(&self) -> bool {
self.sponsored
}
}
#[derive(Debug, Clone)]
pub struct TempoTxBuilder {
inner: TempoTx,
sponsored: bool,
}
impl TempoTxBuilder {
pub fn new(chain_id: u64) -> Self {
Self {
inner: TempoTx {
chain_id,
max_priority_fee_per_gas: 0,
max_fee_per_gas: 0,
gas_limit: 0,
calls: Vec::new(),
access_list: Vec::new(),
nonce_key: 0,
nonce: 0,
valid_before: None,
valid_after: None,
fee_token: None,
aa_authorization_list: Vec::new(),
key_authorization: None,
sponsored: false,
},
sponsored: false,
}
}
pub fn max_priority_fee_per_gas(mut self, v: u128) -> Self {
self.inner.max_priority_fee_per_gas = v;
self
}
pub fn max_fee_per_gas(mut self, v: u128) -> Self {
self.inner.max_fee_per_gas = v;
self
}
pub fn gas_limit(mut self, v: u128) -> Self {
self.inner.gas_limit = v;
self
}
pub fn nonce(mut self, v: u128) -> Self {
self.inner.nonce = v;
self
}
pub fn nonce_key(mut self, v: u128) -> Self {
self.inner.nonce_key = v;
self
}
pub fn fee_token(mut self, addr: [u8; 20]) -> Self {
self.inner.fee_token = Some(addr);
self
}
pub fn call(mut self, call: TempoCall) -> Self {
self.inner.calls.push(call);
self
}
pub fn calls(mut self, calls: Vec<TempoCall>) -> Self {
self.inner.calls = calls;
self
}
pub fn sponsored(mut self) -> Self {
self.sponsored = true;
self.inner.sponsored = true;
self
}
pub fn build(self) -> TempoTx {
self.inner
}
}
pub fn sign_self_paid(tx: TempoTx, sender: &k256::ecdsa::SigningKey) -> Vec<u8> {
let sender_hash = tx.sender_hash();
let sig = crate::wallet::sign_hash(sender, &sender_hash);
tx.serialize_signed(&sig, None)
}
pub fn sign_sponsored(
tx: TempoTx,
sender: &k256::ecdsa::SigningKey,
fee_payer: &k256::ecdsa::SigningKey,
) -> Vec<u8> {
debug_assert!(
tx.sponsored,
"sign_sponsored called on a non-sponsored TempoTx — \
use TempoTxBuilder::sponsored()"
);
let sender_addr = crate::wallet::address(sender);
let sender_hash = tx.sender_hash();
let fp_hash = tx.fee_payer_hash(&sender_addr);
let sender_sig = crate::wallet::sign_hash(sender, &sender_hash);
let fp_sig = crate::wallet::sign_hash(fee_payer, &fp_hash);
tx.serialize_signed(&sender_sig, Some(&fp_sig))
}
impl TempoTx {
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub(crate) fn set_sponsored(mut self, sponsored: bool) -> Self {
self.sponsored = sponsored;
self
}
}
fn rlp_fee_token(addr: Option<&[u8; 20]>) -> Vec<u8> {
match addr {
Some(a) => wallet::rlp_bytes(a),
None => wallet::rlp_bytes(&[]),
}
}
fn rlp_key_authorization(ka: Option<&KeyAuthorization>) -> Vec<u8> {
match ka {
Some(k) => k.raw_rlp.clone(),
None => wallet::rlp_bytes(&[]),
}
}
fn rlp_authorization_list(list: &[SignedAuthorization]) -> Vec<u8> {
if list.is_empty() {
return wallet::rlp_list(&[]);
}
let items: Vec<Vec<u8>> = list
.iter()
.map(|a| {
let inner = wallet::rlp_list(&[
wallet::rlp_uint(a.chain_id as u128),
wallet::rlp_bytes(&a.address),
wallet::rlp_uint(a.nonce as u128),
wallet::rlp_bytes(&a.signature),
]);
inner
})
.collect();
wallet::rlp_list(&items)
}
fn rlp_call(call: &TempoCall) -> Vec<u8> {
wallet::rlp_list(&[
wallet::rlp_bytes(&call.to),
wallet::rlp_uint(call.value_wei),
wallet::rlp_bytes(&call.input),
])
}
fn rlp_access_list(list: &[AccessListItem]) -> Vec<u8> {
if list.is_empty() {
return wallet::rlp_list(&[]);
}
let items: Vec<Vec<u8>> = list
.iter()
.map(|item| {
let keys: Vec<Vec<u8>> =
item.storage_keys.iter().map(|k| wallet::rlp_bytes(k)).collect();
wallet::rlp_list(&[
wallet::rlp_bytes(&item.address),
wallet::rlp_list(&keys),
])
})
.collect();
wallet::rlp_list(&items)
}
fn rlp_compact_signature(sig: &[u8; 65]) -> Vec<u8> {
let mut packed = [0u8; 65];
packed.copy_from_slice(sig);
packed[64] = packed[64].saturating_sub(27); wallet::rlp_bytes(&packed)
}
fn rlp_vrs_signature(sig: &[u8; 65]) -> Vec<u8> {
let v = sig[64].saturating_sub(27); wallet::rlp_list(&[
wallet::rlp_uint(v as u128),
wallet::rlp_bytes(&sig[..32]),
wallet::rlp_bytes(&sig[32..64]),
])
}
impl TempoTx {
fn common_rlp_items(&self) -> Vec<Vec<u8>> {
let call_items: Vec<Vec<u8>> = self.calls.iter().map(rlp_call).collect();
vec![
wallet::rlp_uint(self.chain_id as u128),
wallet::rlp_uint(self.max_priority_fee_per_gas),
wallet::rlp_uint(self.max_fee_per_gas),
wallet::rlp_uint(self.gas_limit),
wallet::rlp_list(&call_items),
rlp_access_list(&self.access_list),
rlp_uint_u256(self.nonce_key),
wallet::rlp_uint(self.nonce),
self.valid_before
.map(|v| wallet::rlp_uint(v as u128))
.unwrap_or_else(|| wallet::rlp_bytes(&[])),
self.valid_after
.map(|v| wallet::rlp_uint(v as u128))
.unwrap_or_else(|| wallet::rlp_bytes(&[])),
]
}
}
fn rlp_uint_u256(value: u128) -> Vec<u8> {
wallet::rlp_uint(value)
}
fn keccak(input: &[u8]) -> [u8; 32] {
use sha3::{Digest, Keccak256};
let mut hasher = Keccak256::new();
hasher.update(input);
let mut out = [0u8; 32];
out.copy_from_slice(&hasher.finalize());
out
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_tx() -> TempoTx {
TempoTxBuilder::new(42431)
.max_priority_fee_per_gas(1_000_000_000)
.max_fee_per_gas(40_000_000_000)
.gas_limit(200_000)
.nonce(0)
.call(TempoCall {
to: [0x11; 20],
value_wei: 0,
input: vec![0xde, 0xad, 0xbe, 0xef],
})
.build()
}
#[test]
fn sender_hash_self_paid_is_32_bytes() {
let tx = dummy_tx();
let h = tx.sender_hash();
assert_eq!(h.len(), 32);
}
#[test]
fn sender_hash_sponsored_differs_from_self_paid() {
let tx_self = dummy_tx();
let tx_sponsored = dummy_tx().set_sponsored(true);
assert_ne!(tx_self.sender_hash(), tx_sponsored.sender_hash());
}
#[test]
fn serialized_starts_with_type_byte() {
let tx = dummy_tx();
let sig = [0u8; 65];
let bytes = tx.serialize_signed(&sig, None);
assert_eq!(bytes[0], 0x76);
}
#[test]
fn fee_payer_hash_includes_sender_address() {
let tx = dummy_tx().set_sponsored(true);
let sender = [0x42; 20];
let other_sender = [0x99; 20];
assert_ne!(
tx.fee_payer_hash(&sender),
tx.fee_payer_hash(&other_sender)
);
}
}