use std::{future::Future, pin::Pin};
use alloy::{
consensus::{TxEip1559, TypedTransaction},
dyn_abi::TypedData,
primitives::{Address, Signature, B256},
};
use num_bigint::BigUint;
use crate::{error::FyndError, Quote};
#[derive(Debug)]
pub struct FyndPayload {
quote: Quote,
tx: TypedTransaction,
}
impl FyndPayload {
pub(crate) fn new(quote: Quote, tx: TypedTransaction) -> Self {
Self { quote, tx }
}
pub fn quote(&self) -> &Quote {
&self.quote
}
pub fn tx(&self) -> &TypedTransaction {
&self.tx
}
pub(crate) fn into_parts(self) -> (Quote, TypedTransaction) {
(self.quote, self.tx)
}
}
#[derive(Debug)]
pub struct TurbinePayload {
#[allow(dead_code)]
_order_quote: (),
}
#[derive(Debug)]
pub enum SwapPayload {
Fynd(Box<FyndPayload>),
Turbine(TurbinePayload),
}
impl SwapPayload {
pub fn signing_hash(&self) -> B256 {
match self {
Self::Fynd(p) => {
use alloy::consensus::SignableTransaction;
p.tx.signature_hash()
}
Self::Turbine(_) => unimplemented!("Turbine signing not yet implemented"),
}
}
pub fn typed_data(&self) -> Option<&TypedData> {
match self {
Self::Fynd(_) | Self::Turbine(_) => None,
}
}
pub fn quote(&self) -> &Quote {
match self {
Self::Fynd(p) => &p.quote,
Self::Turbine(_) => unimplemented!("Turbine signing not yet implemented"),
}
}
pub(crate) fn into_fynd_parts(
self,
) -> Result<(Quote, TypedTransaction), crate::error::FyndError> {
match self {
Self::Fynd(p) => Ok(p.into_parts()),
Self::Turbine(_) => Err(crate::error::FyndError::Protocol(
"Turbine execution not yet implemented".into(),
)),
}
}
}
pub struct SignedSwap {
payload: SwapPayload,
signature: Signature,
}
impl SignedSwap {
pub fn assemble(payload: SwapPayload, signature: Signature) -> Self {
Self { payload, signature }
}
pub fn payload(&self) -> &SwapPayload {
&self.payload
}
pub fn signature(&self) -> &Signature {
&self.signature
}
pub(crate) fn into_parts(self) -> (SwapPayload, Signature) {
(self.payload, self.signature)
}
}
pub struct SettledOrder {
settled_amount: Option<BigUint>,
gas_cost: BigUint,
}
impl SettledOrder {
pub(crate) fn new(settled_amount: Option<BigUint>, gas_cost: BigUint) -> Self {
Self { settled_amount, gas_cost }
}
pub fn settled_amount(&self) -> Option<&BigUint> {
self.settled_amount.as_ref()
}
pub fn gas_cost(&self) -> &BigUint {
&self.gas_cost
}
}
pub enum ExecutionReceipt {
Transaction(Pin<Box<dyn Future<Output = Result<SettledOrder, FyndError>> + Send + 'static>>),
}
impl Future for ExecutionReceipt {
type Output = Result<SettledOrder, FyndError>;
fn poll(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
match self.get_mut() {
Self::Transaction(fut) => fut.as_mut().poll(cx),
}
}
}
pub struct ApprovalPayload {
pub(crate) tx: TxEip1559,
pub(crate) token: bytes::Bytes,
pub(crate) spender: bytes::Bytes,
pub(crate) amount: BigUint,
}
impl ApprovalPayload {
pub fn signing_hash(&self) -> B256 {
use alloy::consensus::SignableTransaction;
self.tx.signature_hash()
}
pub fn tx(&self) -> &TxEip1559 {
&self.tx
}
pub fn token(&self) -> &bytes::Bytes {
&self.token
}
pub fn spender(&self) -> &bytes::Bytes {
&self.spender
}
pub fn amount(&self) -> &BigUint {
&self.amount
}
}
pub struct SignedApproval {
payload: ApprovalPayload,
signature: Signature,
}
impl SignedApproval {
pub fn assemble(payload: ApprovalPayload, signature: Signature) -> Self {
Self { payload, signature }
}
pub fn payload(&self) -> &ApprovalPayload {
&self.payload
}
pub fn signature(&self) -> &Signature {
&self.signature
}
pub(crate) fn into_parts(self) -> (ApprovalPayload, Signature) {
(self.payload, self.signature)
}
}
pub struct MinedTx {
tx_hash: B256,
gas_cost: BigUint,
}
impl MinedTx {
pub(crate) fn new(tx_hash: B256, gas_cost: BigUint) -> Self {
Self { tx_hash, gas_cost }
}
pub fn tx_hash(&self) -> B256 {
self.tx_hash
}
pub fn gas_cost(&self) -> &BigUint {
&self.gas_cost
}
}
pub enum TxReceipt {
Pending(Pin<Box<dyn Future<Output = Result<MinedTx, FyndError>> + Send + 'static>>),
}
impl Future for TxReceipt {
type Output = Result<MinedTx, FyndError>;
fn poll(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
match self.get_mut() {
Self::Pending(fut) => fut.as_mut().poll(cx),
}
}
}
pub(crate) fn compute_settled_amount(
receipt: &alloy::rpc::types::TransactionReceipt,
token_out_addr: &Address,
receiver_addr: &Address,
) -> Option<BigUint> {
use alloy::primitives::keccak256;
let erc20_topic = keccak256(b"Transfer(address,address,uint256)");
let erc6909_topic = keccak256(b"Transfer(address,address,address,uint256,uint256)");
let mut total = BigUint::ZERO;
let mut found = false;
for log in receipt.logs() {
if log.address() != *token_out_addr {
continue;
}
let topics = log.topics();
if topics.is_empty() {
continue;
}
if topics[0] == erc20_topic && topics.len() >= 3 {
let to = Address::from_slice(&topics[2].as_slice()[12..]);
if to == *receiver_addr {
let data = &log.data().data;
if data.len() >= 32 {
let amount = BigUint::from_bytes_be(&data[0..32]);
total += amount;
found = true;
}
}
} else if topics[0] == erc6909_topic && topics.len() >= 3 {
let to = Address::from_slice(&topics[2].as_slice()[12..]);
if to == *receiver_addr {
let data = &log.data().data;
if data.len() >= 64 {
let amount = BigUint::from_bytes_be(&data[32..64]);
total += amount;
found = true;
}
}
}
}
if found {
Some(total)
} else {
None
}
}
#[cfg(test)]
mod tests {
use alloy::{
primitives::{keccak256, Address, Bytes as AlloyBytes, LogData, B256},
rpc::types::{Log, TransactionReceipt},
};
use super::*;
fn make_receipt(logs: Vec<Log>) -> TransactionReceipt {
use alloy::{
consensus::{Receipt, ReceiptEnvelope, ReceiptWithBloom},
primitives::{Bloom, TxHash},
};
TransactionReceipt {
inner: ReceiptEnvelope::Eip1559(ReceiptWithBloom {
receipt: Receipt {
status: alloy::consensus::Eip658Value::Eip658(true),
cumulative_gas_used: 21_000,
logs,
},
logs_bloom: Bloom::default(),
}),
transaction_hash: TxHash::default(),
transaction_index: None,
block_hash: None,
block_number: None,
gas_used: 21_000,
effective_gas_price: 1,
blob_gas_used: None,
blob_gas_price: None,
from: Address::ZERO,
to: None,
contract_address: None,
}
}
fn erc20_topic() -> B256 {
keccak256(b"Transfer(address,address,uint256)")
}
fn erc6909_topic() -> B256 {
keccak256(b"Transfer(address,address,address,uint256,uint256)")
}
fn addr_topic(addr: Address) -> B256 {
let mut topic = [0u8; 32];
topic[12..].copy_from_slice(addr.as_slice());
B256::from(topic)
}
fn encode_u256(amount: u64) -> Vec<u8> {
let mut buf = [0u8; 32];
let bytes = amount.to_be_bytes();
buf[24..].copy_from_slice(&bytes);
buf.to_vec()
}
fn make_log(address: Address, topics: Vec<B256>, data: Vec<u8>) -> Log {
Log {
inner: alloy::primitives::Log {
address,
data: LogData::new_unchecked(topics, AlloyBytes::from(data)),
},
block_hash: None,
block_number: None,
block_timestamp: None,
transaction_hash: None,
transaction_index: None,
log_index: None,
removed: false,
}
}
#[test]
fn erc20_transfer_log_matched() {
let token = Address::with_last_byte(0x01);
let from = Address::with_last_byte(0x02);
let receiver = Address::with_last_byte(0x03);
let log = make_log(
token,
vec![erc20_topic(), addr_topic(from), addr_topic(receiver)],
encode_u256(500),
);
let receipt = make_receipt(vec![log]);
let result = compute_settled_amount(&receipt, &token, &receiver);
assert_eq!(result, Some(BigUint::from(500u64)));
}
#[test]
fn erc20_transfer_log_wrong_token() {
let token = Address::with_last_byte(0x01);
let other_token = Address::with_last_byte(0x99);
let receiver = Address::with_last_byte(0x03);
let log = make_log(
other_token, vec![erc20_topic(), addr_topic(Address::ZERO), addr_topic(receiver)],
encode_u256(500),
);
let receipt = make_receipt(vec![log]);
let result = compute_settled_amount(&receipt, &token, &receiver);
assert!(result.is_none());
}
#[test]
fn erc20_transfer_log_wrong_receiver() {
let token = Address::with_last_byte(0x01);
let receiver = Address::with_last_byte(0x03);
let other_receiver = Address::with_last_byte(0x04);
let log = make_log(
token,
vec![erc20_topic(), addr_topic(Address::ZERO), addr_topic(other_receiver)],
encode_u256(500),
);
let receipt = make_receipt(vec![log]);
let result = compute_settled_amount(&receipt, &token, &receiver);
assert!(result.is_none());
}
#[test]
fn erc6909_transfer_log_matched() {
let token = Address::with_last_byte(0x10);
let from = Address::with_last_byte(0x11);
let receiver = Address::with_last_byte(0x12);
let mut data = [0u8; 64];
data[12..32].copy_from_slice(Address::with_last_byte(0xca).as_slice());
let amount_bytes = encode_u256(750);
data[32..].copy_from_slice(&amount_bytes);
let log = make_log(
token,
vec![erc6909_topic(), addr_topic(from), addr_topic(receiver)],
data.to_vec(),
);
let receipt = make_receipt(vec![log]);
let result = compute_settled_amount(&receipt, &token, &receiver);
assert_eq!(result, Some(BigUint::from(750u64)));
}
#[test]
fn no_matching_logs_returns_none() {
let token = Address::with_last_byte(0x01);
let receiver = Address::with_last_byte(0x03);
let unrelated_topic = keccak256(b"Approval(address,address,uint256)");
let log = make_log(
token,
vec![unrelated_topic, addr_topic(Address::ZERO), addr_topic(receiver)],
encode_u256(100),
);
let receipt = make_receipt(vec![log]);
let result = compute_settled_amount(&receipt, &token, &receiver);
assert!(result.is_none());
}
#[test]
fn empty_logs_returns_none() {
let token = Address::with_last_byte(0x01);
let receiver = Address::with_last_byte(0x03);
let receipt = make_receipt(vec![]);
assert!(compute_settled_amount(&receipt, &token, &receiver).is_none());
}
#[test]
fn multiple_matching_logs_amounts_summed() {
let token = Address::with_last_byte(0x01);
let from = Address::with_last_byte(0x02);
let receiver = Address::with_last_byte(0x03);
let log1 = make_log(
token,
vec![erc20_topic(), addr_topic(from), addr_topic(receiver)],
encode_u256(100),
);
let log2 = make_log(
token,
vec![erc20_topic(), addr_topic(from), addr_topic(receiver)],
encode_u256(200),
);
let log3 = make_log(
token,
vec![erc20_topic(), addr_topic(from), addr_topic(Address::with_last_byte(0xff))],
encode_u256(999),
);
let receipt = make_receipt(vec![log1, log2, log3]);
let result = compute_settled_amount(&receipt, &token, &receiver);
assert_eq!(result, Some(BigUint::from(300u64)));
}
}