altius-tx-sdk 0.1.0

SDK for signing and sending Altius USD multi-token transactions
Documentation
//! Transaction types for Altius USD Multi-Token transactions

use crate::{Error, Result, BASE_FEE_ATTO, DEFAULT_GAS_LIMIT};
use alloy_primitives::{Address, Bytes, B256, U256, keccak256};
use alloy_rlp::Encodable;

/// Magic byte for fee payer signature (0x7B)
const FEE_PAYER_SIGNATURE_MAGIC_BYTE: u8 = 0x7B;

/// USD Multi-Token transaction (0x7a type)
///
/// This is the core transaction type for Altius chains using the USD Multi-Token fee model.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TxUsdMultiToken {
    /// Chain ID
    pub chain_id: u64,
    /// Transaction nonce
    pub nonce: u64,
    /// Max priority fee per gas (in wei)
    pub max_priority_fee_per_gas: u128,
    /// Max fee per gas (in wei)
    pub max_fee_per_gas: u128,
    /// Gas limit
    pub gas_limit: u64,
    /// Recipient address (or create for contract creation)
    pub to: Option<Address>,
    /// Transaction value (in wei)
    pub value: U256,
    /// Input data (calldata)
    pub input: Bytes,
    /// Fee token address (ERC20)
    pub fee_token: Address,
    /// Fee payer address (ZERO_ADDRESS means sender pays)
    pub fee_payer: Address,
    /// Max fee per gas in USD attodollars
    pub max_fee_per_gas_usd_attodollars: u128,
    /// Fee payer signature (65 bytes: v, r, s)
    pub fee_payer_signature: Bytes,
}

impl Default for TxUsdMultiToken {
    fn default() -> Self {
        Self {
            chain_id: 0,
            nonce: 0,
            max_priority_fee_per_gas: 0,
            max_fee_per_gas: 0,
            gas_limit: DEFAULT_GAS_LIMIT,
            to: None,
            value: U256::ZERO,
            input: Bytes::default(),
            fee_token: Address::ZERO,
            fee_payer: Address::ZERO,
            max_fee_per_gas_usd_attodollars: (BASE_FEE_ATTO as u128) * 2,
            fee_payer_signature: Bytes::default(),
        }
    }
}

/// Calculate the hash that fee_payer should sign.
pub fn fee_payer_signature_hash(tx: &TxUsdMultiToken, sender: Address) -> B256 {
    let mut buf = Vec::new();
    buf.push(FEE_PAYER_SIGNATURE_MAGIC_BYTE);
    tx.chain_id.encode(&mut buf);
    tx.nonce.encode(&mut buf);
    tx.gas_limit.encode(&mut buf);
    tx.fee_token.encode(&mut buf);
    tx.fee_payer.encode(&mut buf);
    tx.max_fee_per_gas_usd_attodollars.encode(&mut buf);
    sender.encode(&mut buf);
    keccak256(&buf)
}

impl TxUsdMultiToken {
    /// Compute the signature hash for this transaction
    pub fn signature_hash(&self) -> B256 {
        let mut buf = Vec::new();
        buf.push(0x7a);
        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);
        // Encode to field
        match &self.to {
            None => buf.push(0x80), // Empty byte for contract creation
            Some(addr) => addr.encode(&mut buf),
        }
        self.value.encode(&mut buf);
        self.input.encode(&mut buf);
        // Empty access list (0xc0 in RLP)
        buf.push(0xc0);
        self.fee_token.encode(&mut buf);
        self.fee_payer.encode(&mut buf);
        self.max_fee_per_gas_usd_attodollars.encode(&mut buf);
        self.fee_payer_signature.encode(&mut buf);
        keccak256(&buf)
    }
}

/// Signature components
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Signature {
    pub v: u64,
    pub r: B256,
    pub s: B256,
}

impl Signature {
    pub fn new(v: u64, r: B256, s: B256) -> Self {
        Self { v, r, s }
    }

    pub fn to_bytes(&self) -> Bytes {
        let mut buf = Vec::with_capacity(65);
        buf.push(self.v as u8);
        buf.extend_from_slice(self.r.as_slice());
        buf.extend_from_slice(self.s.as_slice());
        Bytes::from(buf)
    }
}

/// Signed USD Multi-Token transaction
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SignedTxUsdMultiToken {
    pub tx: TxUsdMultiToken,
    pub signature: Signature,
}

impl SignedTxUsdMultiToken {
    pub fn encode_2718(&self) -> Bytes {
        let mut encoded = vec![0x7a];
        let mut tx_bytes = Vec::new();
        self.tx.encode(&mut tx_bytes);
        encoded.extend_from_slice(&tx_bytes);
        Bytes::from(encoded)
    }
}

/// Builder for TxUsdMultiToken
#[derive(Debug, Clone)]
pub struct TxBuilder {
    chain_id: Option<u64>,
    nonce: Option<u64>,
    max_priority_fee_per_gas: u128,
    max_fee_per_gas: u128,
    gas_limit: u64,
    to: Option<Address>,
    value: U256,
    input: Bytes,
    fee_token: Address,
    fee_payer: Address,
    max_fee_per_gas_usd_attodollars: u128,
    fee_payer_signature: Bytes,
}

impl Default for TxBuilder {
    fn default() -> Self {
        Self {
            chain_id: None,
            nonce: None,
            max_priority_fee_per_gas: 0,
            max_fee_per_gas: 0,
            gas_limit: DEFAULT_GAS_LIMIT,
            to: None,
            value: U256::ZERO,
            input: Bytes::default(),
            fee_token: Address::ZERO,
            fee_payer: Address::ZERO,
            max_fee_per_gas_usd_attodollars: (BASE_FEE_ATTO as u128) * 2,
            fee_payer_signature: Bytes::default(),
        }
    }
}

impl TxBuilder {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn chain_id(mut self, chain_id: u64) -> Self {
        self.chain_id = Some(chain_id);
        self
    }

    pub fn nonce(mut self, nonce: u64) -> Self {
        self.nonce = Some(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 value(mut self, value: U256) -> Self {
        self.value = value;
        self
    }

    pub fn input(mut self, input: Bytes) -> Self {
        self.input = input;
        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: Address) -> Self {
        self.fee_payer = fee_payer;
        self
    }

    pub fn max_fee_per_gas_usd(mut self, max_fee: u128) -> Self {
        self.max_fee_per_gas_usd_attodollars = max_fee;
        self
    }

    pub fn fee_payer_signature(mut self, signature: Bytes) -> Self {
        self.fee_payer_signature = signature;
        self
    }

    pub fn fee_payer_with_signature(mut self, fee_payer: Address, signature: Bytes) -> Self {
        self.fee_payer = fee_payer;
        self.fee_payer_signature = signature;
        self
    }

    /// Build an ERC20 transfer call (manual encoding)
    pub fn erc20_transfer(mut self, token: Address, recipient: Address, amount: U256) -> Self {
        // ERC20 transfer selector: 0xa9059cbb
        let mut data = vec![0xa9, 0x05, 0x9c, 0xbb];
        // Pad recipient address (32 bytes)
        let mut recipient_bytes = [0u8; 32];
        recipient_bytes[12..32].copy_from_slice(recipient.as_slice());
        data.extend_from_slice(&recipient_bytes);
        // Pad amount (32 bytes) - big endian
        let amount_le: [u8; 32] = amount.to_be_bytes();
        data.extend_from_slice(&amount_le);

        self.to = Some(token);
        self.input = Bytes::from(data);
        self
    }

    pub fn build(self) -> TxUsdMultiToken {
        TxUsdMultiToken {
            chain_id: self.chain_id.expect("chain_id is required"),
            nonce: self.nonce.expect("nonce is required"),
            max_priority_fee_per_gas: self.max_priority_fee_per_gas,
            max_fee_per_gas: self.max_fee_per_gas,
            gas_limit: self.gas_limit,
            to: self.to,
            value: self.value,
            input: self.input,
            fee_token: self.fee_token,
            fee_payer: self.fee_payer,
            max_fee_per_gas_usd_attodollars: self.max_fee_per_gas_usd_attodollars,
            fee_payer_signature: self.fee_payer_signature,
        }
    }

    pub fn try_build(self) -> Result<TxUsdMultiToken> {
        let chain_id = self.chain_id.ok_or_else(|| Error::MissingField("chain_id".into()))?;
        let nonce = self.nonce.ok_or_else(|| Error::MissingField("nonce".into()))?;

        Ok(TxUsdMultiToken {
            chain_id,
            nonce,
            max_priority_fee_per_gas: self.max_priority_fee_per_gas,
            max_fee_per_gas: self.max_fee_per_gas,
            gas_limit: self.gas_limit,
            to: self.to,
            value: self.value,
            input: self.input,
            fee_token: self.fee_token,
            fee_payer: self.fee_payer,
            max_fee_per_gas_usd_attodollars: self.max_fee_per_gas_usd_attodollars,
            fee_payer_signature: self.fee_payer_signature,
        })
    }
}

impl Encodable for TxUsdMultiToken {
    fn encode(&self, w: &mut dyn alloy_rlp::BufMut) {
        w.put_slice(&[0x7a]);
        self.chain_id.encode(w);
        self.nonce.encode(w);
        self.max_priority_fee_per_gas.encode(w);
        self.max_fee_per_gas.encode(w);
        self.gas_limit.encode(w);
        // Encode to field (None = create = 0x80, Some(addr) = address bytes)
        match &self.to {
            None => w.put_slice(&[0x80]), // Empty byte for contract creation
            Some(addr) => addr.encode(w),
        }
        self.value.encode(w);
        self.input.encode(w);
        // Empty access list
        w.put_slice(&[0xc0]);
        self.fee_token.encode(w);
        self.fee_payer.encode(w);
        self.max_fee_per_gas_usd_attodollars.encode(w);
        self.fee_payer_signature.encode(w);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_build_erc20_transfer() {
        let token: Address = "0xa1700000000000000000000000000000000000002".parse().unwrap();
        let recipient: Address = "0x1234567890123456789012345678901234567890".parse().unwrap();
        let amount = U256::from(100_000_000u64);

        let tx = TxBuilder::new()
            .chain_id(1337)
            .nonce(0)
            .erc20_transfer(token, recipient, amount)
            .fee_token(token)
            .max_fee_per_gas_usd(BASE_FEE_ATTO * 2)
            .build();

        assert_eq!(tx.chain_id, 1337);
        assert_eq!(tx.nonce, 0);
    }
}