diff --git a/Cargo.lock b/Cargo.lock index 370911a..91af9cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3039,7 +3039,7 @@ dependencies = [ [[package]] name = "tycho-execution" -version = "1.0.0" +version = "0.1.0" dependencies = [ "alloy", "alloy-primitives", diff --git a/Cargo.toml b/Cargo.toml index 62c3807..3f6057e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tycho-execution" -version = "1.0.0" +version = "0.1.0" edition = "2021" [dependencies] diff --git a/src/encoding/approvals/approvals_manager.rs b/src/encoding/approvals/approvals_manager.rs new file mode 100644 index 0000000..fde5442 --- /dev/null +++ b/src/encoding/approvals/approvals_manager.rs @@ -0,0 +1,49 @@ +use std::{env, sync::Arc}; + +use alloy::{ + providers::{ProviderBuilder, RootProvider}, + transports::BoxTransport, +}; +use alloy_primitives::Address; +use dotenv::dotenv; + +#[allow(dead_code)] +pub struct ProtocolApprovalsManager { + client: Arc>, +} +impl ProtocolApprovalsManager { + pub fn new() -> Self { + Self { client: get_client() } + } + pub async fn approval_needed( + &self, + _token: Address, + _spender_address: Address, + _router_address: Address, + ) -> bool { + todo!() + // should be something like + // let allowance = self + // .client + // .call(token, "allowance(address,address)(uint256)", (router_address, + // spender_address)) .await; + // + // allowance == U256::ZERO // If allowance is 0, approval is needed + } +} + +pub fn get_client() -> Arc> { + dotenv().ok(); + let eth_rpc_url = env::var("ETH_RPC_URL").expect("Missing ETH_RPC_URL in environment"); + let runtime = tokio::runtime::Handle::try_current() + .is_err() + .then(|| tokio::runtime::Runtime::new().unwrap()) + .unwrap(); + let client = runtime.block_on(async { + ProviderBuilder::new() + .on_builtin(ð_rpc_url) + .await + .unwrap() + }); + Arc::new(client) +} diff --git a/src/encoding/approvals/interface.rs b/src/encoding/approvals/interface.rs new file mode 100644 index 0000000..45ca74e --- /dev/null +++ b/src/encoding/approvals/interface.rs @@ -0,0 +1,15 @@ +use num_bigint::BigUint; +use tycho_core::Bytes; + +#[allow(dead_code)] +pub struct Approval { + pub spender: Bytes, + pub owner: Bytes, + pub token: Bytes, + pub amount: BigUint, +} + +pub trait UserApprovalsManager { + #[allow(dead_code)] + fn encode_approvals(&self, approvals: Vec) -> Vec; +} diff --git a/src/encoding/approvals/mod.rs b/src/encoding/approvals/mod.rs new file mode 100644 index 0000000..8fbe120 --- /dev/null +++ b/src/encoding/approvals/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod approvals_manager; +pub(crate) mod interface; +mod permit2; diff --git a/src/encoding/approvals/permit2.rs b/src/encoding/approvals/permit2.rs new file mode 100644 index 0000000..e89adbd --- /dev/null +++ b/src/encoding/approvals/permit2.rs @@ -0,0 +1,42 @@ +use std::str::FromStr; + +use alloy_primitives::U256; +use tycho_core::Bytes; + +use crate::encoding::approvals::interface::{Approval, UserApprovalsManager}; + +#[allow(dead_code)] +pub struct Permit2 { + pub address: Bytes, +} + +#[allow(dead_code)] +impl Permit2 { + pub fn new() -> Self { + Self { + address: Bytes::from_str("0x000000000022D473030F116dDEE9F6B43aC78BA3") + .expect("Permit2 address not valid"), + } + } + fn get_allowance_data( + &self, + _user: Bytes, + _router_address: Bytes, + _token: Bytes, + ) -> (U256, u64, U256) { + // get allowance data (if it exists) and the nonce + // returns permitAmount, expiration, nonce + todo!() + } +} +impl UserApprovalsManager for Permit2 { + fn encode_approvals(&self, _approvals: Vec) -> Vec { + // calls get_allowance_data to get nonce + // checks if we are not permitted already + // puts data into a permitSingle struct if there is only 1 PermitDetails, if there are + // several, use PermitBatch adds the nonce and the expiration (uniswap recommends + // 30 days for expiration) signs data + // returns encoded data + todo!() + } +} diff --git a/src/encoding/mod.rs b/src/encoding/mod.rs new file mode 100644 index 0000000..d9e8233 --- /dev/null +++ b/src/encoding/mod.rs @@ -0,0 +1,7 @@ +mod approvals; +mod models; +mod router_encoder; +mod strategy_encoder; +mod strategy_selector; +mod swap_encoder; +mod utils; diff --git a/src/encoding/models.rs b/src/encoding/models.rs new file mode 100644 index 0000000..89ad2e1 --- /dev/null +++ b/src/encoding/models.rs @@ -0,0 +1,86 @@ +use std::{env, str::FromStr}; + +use lazy_static::lazy_static; +use num_bigint::BigUint; +use tycho_core::{dto::ProtocolComponent, Bytes}; + +lazy_static! { + pub static ref PROPELLER_ROUTER_ADDRESS: Bytes = Bytes::from_str( + &env::var("ROUTER_ADDRESS").expect("Missing ROUTER_ADDRESS in environment"), + ) + .expect("Invalid ROUTER_ADDRESS"); +} + +#[derive(Clone)] +#[allow(dead_code)] +pub struct Solution { + /// True if the solution is an exact output solution. + pub exact_out: bool, + /// The token being sold (exact in) or bought (exact out). + pub given_token: Bytes, + /// Amount of the given token. + pub given_amount: BigUint, + /// The token being bought (exact in) or sold (exact out). + checked_token: Bytes, + /// Expected amount of the bought token (exact in) or sold token (exact out). + pub expected_amount: BigUint, + /// Minimum amount to be checked for the solution to be valid. + /// If not set, the check will not be performed. + pub check_amount: Option, + /// Address of the sender. + pub sender: Bytes, + /// Address of the receiver. + pub receiver: Bytes, + /// List of swaps to fulfill the solution. + pub swaps: Vec, + /// If set to true, the solution will be encoded to be sent directly to the SwapExecutor and + /// skip the router. The user is responsible for managing necessary approvals and token + /// transfers. + pub straight_to_pool: bool, + // if not set, then the Propeller Router will be used + pub router_address: Option, + // if set, it will be applied to check_amount + pub slippage: Option, + // if set, the corresponding native action will be executed + pub native_action: Option, +} + +#[derive(Clone, PartialEq)] +#[allow(dead_code)] +pub enum NativeAction { + Wrap, + Unwrap, +} + +#[derive(Clone)] +pub struct Swap { + /// Protocol component from tycho indexer + pub component: ProtocolComponent, + /// Token being input into the pool. + pub token_in: Bytes, + /// Token being output from the pool. + pub token_out: Bytes, + /// Fraction of the amount to be swapped in this operation. + pub split: f64, +} + +#[allow(dead_code)] +pub struct Transaction { + pub data: Vec, + // ETH value to be sent with the transaction. + pub value: BigUint, +} + +pub struct EncodingContext { + pub receiver: Bytes, + pub exact_out: bool, + pub address_for_approvals: Bytes, +} + +pub enum ActionType { + SingleExactIn = 1, + SingleExactOut = 2, + SequentialExactIn = 3, + SequentialExactOut = 4, + SplitIn = 5, +} diff --git a/src/encoding/router_encoder.rs b/src/encoding/router_encoder.rs new file mode 100644 index 0000000..1f44bf9 --- /dev/null +++ b/src/encoding/router_encoder.rs @@ -0,0 +1,78 @@ +use alloy_sol_types::SolValue; +use anyhow::Error; +use num_bigint::BigUint; + +use crate::encoding::{ + approvals::interface::{Approval, UserApprovalsManager}, + models::{NativeAction, Solution, Transaction, PROPELLER_ROUTER_ADDRESS}, + strategy_selector::StrategySelector, + utils::{encode_input, ple_encode}, +}; + +#[allow(dead_code)] +struct RouterEncoder { + strategy_selector: S, + approvals_manager: A, +} + +#[allow(dead_code)] +impl RouterEncoder { + pub fn new(strategy_selector: S, approvals_manager: A) -> Self { + RouterEncoder { strategy_selector, approvals_manager } + } + pub fn encode_router_calldata(&self, solutions: Vec) -> Result { + let _approvals_calldata = self.handle_approvals(&solutions)?; // TODO: where should we append this? + let mut calldata_list: Vec> = Vec::new(); + let encode_for_batch_execute = solutions.len() > 1; + let mut value = BigUint::ZERO; + for solution in solutions.iter() { + let exact_out = solution.exact_out; + let straight_to_pool = solution.straight_to_pool; + + let strategy = self + .strategy_selector + .select_strategy(solution); + let method_calldata = strategy.encode_strategy((*solution).clone())?; + + let contract_interaction = if encode_for_batch_execute { + let args = (strategy.action_type(exact_out) as u16, method_calldata); + args.abi_encode() + } else if straight_to_pool { + method_calldata + } else { + encode_input(strategy.selector(exact_out), method_calldata) + }; + calldata_list.push(contract_interaction); + + if solution.native_action.clone().unwrap() == NativeAction::Wrap { + value += solution.given_amount.clone(); + } + } + let data = if encode_for_batch_execute { + let args = (false, ple_encode(calldata_list)); + encode_input("batchExecute(bytes)", args.abi_encode()) + } else { + calldata_list[0].clone() + }; + + Ok(Transaction { data, value }) + } + + fn handle_approvals(&self, solutions: &[Solution]) -> Result, Error> { + let mut approvals = Vec::new(); + for solution in solutions.iter() { + approvals.push(Approval { + token: solution.given_token.clone(), + spender: solution + .router_address + .clone() + .unwrap_or(PROPELLER_ROUTER_ADDRESS.clone()), + amount: solution.given_amount.clone(), + owner: solution.sender.clone(), + }); + } + Ok(self + .approvals_manager + .encode_approvals(approvals)) + } +} diff --git a/src/encoding/strategy_encoder.rs b/src/encoding/strategy_encoder.rs new file mode 100644 index 0000000..63d3ef4 --- /dev/null +++ b/src/encoding/strategy_encoder.rs @@ -0,0 +1,192 @@ +use std::cmp::min; + +use alloy_primitives::Address; +use alloy_sol_types::SolValue; +use anyhow::Error; +use num_bigint::BigUint; +use num_traits::Zero; + +use crate::encoding::{ + models::{ActionType, EncodingContext, NativeAction, Solution, PROPELLER_ROUTER_ADDRESS}, + swap_encoder::SWAP_ENCODER_REGISTRY, + utils::{biguint_to_u256, ple_encode}, +}; + +#[allow(dead_code)] +pub trait StrategyEncoder { + fn encode_strategy(&self, to_encode: Solution) -> Result, Error>; + + fn action_type(&self, exact_out: bool) -> ActionType; + fn selector(&self, exact_out: bool) -> &str; + + fn encode_protocol_header( + &self, + protocol_data: Vec, + executor_address: Address, + // Token indices, split, and token inclusion are only used for split swaps + token_in: u16, + token_out: u16, + split: u16, // not sure what should be the type of this :/ + ) -> Vec { + let args = (executor_address, token_in, token_out, split, protocol_data); + args.abi_encode() + } +} + +pub struct SingleSwapStrategyEncoder {} + +impl StrategyEncoder for SingleSwapStrategyEncoder { + fn encode_strategy(&self, _solution: Solution) -> Result, Error> { + todo!() + } + + fn action_type(&self, exact_out: bool) -> ActionType { + if exact_out { + ActionType::SingleExactOut + } else { + ActionType::SingleExactIn + } + } + + fn selector(&self, exact_out: bool) -> &str { + if exact_out { + "singleExactOut(uint256, bytes)" + } else { + "singleExactIn(uint256, bytes)" + } + } +} + +pub struct SequentialStrategyEncoder {} + +impl StrategyEncoder for SequentialStrategyEncoder { + fn encode_strategy(&self, solution: Solution) -> Result, Error> { + let check_amount = if solution.check_amount.is_some() { + let mut check_amount = solution.check_amount.clone().unwrap(); + if solution.slippage.is_some() { + let one_hundred = BigUint::from(100u32); + let slippage_percent = BigUint::from((solution.slippage.unwrap() * 100.0) as u32); + let multiplier = &one_hundred - slippage_percent; + let expected_amount_with_slippage = + (&solution.expected_amount * multiplier) / one_hundred; + check_amount = min(check_amount, expected_amount_with_slippage); + } + check_amount + } else { + BigUint::ZERO + }; + + let mut swaps = vec![]; + for (index, swap) in solution.swaps.iter().enumerate() { + let is_last = index == solution.swaps.len() - 1; + let registry = SWAP_ENCODER_REGISTRY.read().unwrap(); + let swap_encoder = registry + .get_encoder(&swap.component.protocol_system) + .expect("Swap encoder not found"); + let router_address = if solution.router_address.is_some() { + solution.router_address.clone().unwrap() + } else { + PROPELLER_ROUTER_ADDRESS.clone() + }; + let receiver = if is_last { solution.receiver.clone() } else { router_address.clone() }; + + let encoding_context = EncodingContext { + receiver, + exact_out: solution.exact_out, + address_for_approvals: router_address, + }; + let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context)?; + let executor_address = swap_encoder.executor_address(); + let swap_data = self.encode_protocol_header(protocol_data, executor_address, 0, 0, 0); + swaps.push(swap_data); + } + + let encoded_swaps = ple_encode(swaps); + + let (mut unwrap, mut wrap) = (false, false); + if solution.native_action.is_some() { + match solution.native_action.unwrap() { + NativeAction::Wrap => wrap = true, + NativeAction::Unwrap => unwrap = true, + } + } + let method_calldata = ( + wrap, + unwrap, + biguint_to_u256(&solution.given_amount), + !check_amount.is_zero(), /* if check_amount is zero, then we don't need to check */ + biguint_to_u256(&check_amount), + encoded_swaps, + ) + .abi_encode(); + Ok(method_calldata) + } + + fn action_type(&self, exact_out: bool) -> ActionType { + if exact_out { + ActionType::SequentialExactOut + } else { + ActionType::SequentialExactIn + } + } + + fn selector(&self, exact_out: bool) -> &str { + if exact_out { + "sequentialExactOut(uint256, uint256, bytes[])" + } else { + "sequentialExactIn(uint256, uint256, bytes[])" + } + } +} + +pub struct SplitSwapStrategyEncoder {} + +impl StrategyEncoder for SplitSwapStrategyEncoder { + fn encode_strategy(&self, _solution: Solution) -> Result, Error> { + todo!() + } + fn action_type(&self, _exact_out: bool) -> ActionType { + ActionType::SplitIn + } + + fn selector(&self, _exact_out: bool) -> &str { + "splitExactIn(uint256, address, uint256, bytes[])" + } +} + +/// This strategy encoder is used for solutions that are sent directly to the pool. +/// Only 1 solution with 1 swap is supported. +pub struct StraightToPoolStrategyEncoder {} + +impl StrategyEncoder for StraightToPoolStrategyEncoder { + fn encode_strategy(&self, solution: Solution) -> Result, Error> { + if solution.router_address.is_none() { + return Err(anyhow::anyhow!( + "Router address is required for straight to pool solutions" + )); + } + let swap = solution.swaps.first().unwrap(); + let registry = SWAP_ENCODER_REGISTRY.read().unwrap(); + let swap_encoder = registry + .get_encoder(&swap.component.protocol_system) + .expect("Swap encoder not found"); + let router_address = solution.router_address.unwrap(); + + let encoding_context = EncodingContext { + receiver: solution.receiver, + exact_out: solution.exact_out, + address_for_approvals: router_address, + }; + let protocol_data = swap_encoder.encode_swap(swap.clone(), encoding_context)?; + // TODO: here we need to pass also the address of the executor to be used + Ok(protocol_data) + } + + fn action_type(&self, _exact_out: bool) -> ActionType { + unimplemented!(); + } + + fn selector(&self, _exact_out: bool) -> &str { + unimplemented!(); + } +} diff --git a/src/encoding/strategy_selector.rs b/src/encoding/strategy_selector.rs new file mode 100644 index 0000000..4a50139 --- /dev/null +++ b/src/encoding/strategy_selector.rs @@ -0,0 +1,32 @@ +use crate::encoding::{ + models::Solution, + strategy_encoder::{ + SequentialStrategyEncoder, SingleSwapStrategyEncoder, SplitSwapStrategyEncoder, + StraightToPoolStrategyEncoder, StrategyEncoder, + }, +}; + +pub trait StrategySelector { + #[allow(dead_code)] + fn select_strategy(&self, solution: &Solution) -> Box; +} + +pub struct DefaultStrategySelector; + +impl StrategySelector for DefaultStrategySelector { + fn select_strategy(&self, solution: &Solution) -> Box { + if solution.straight_to_pool { + Box::new(StraightToPoolStrategyEncoder {}) + } else if solution.swaps.len() == 1 { + Box::new(SingleSwapStrategyEncoder {}) + } else if solution + .swaps + .iter() + .all(|s| s.split == 0.0) + { + Box::new(SequentialStrategyEncoder {}) + } else { + Box::new(SplitSwapStrategyEncoder {}) + } + } +} diff --git a/src/encoding/swap_encoder/builder.rs b/src/encoding/swap_encoder/builder.rs new file mode 100644 index 0000000..492962d --- /dev/null +++ b/src/encoding/swap_encoder/builder.rs @@ -0,0 +1,30 @@ +use std::str::FromStr; + +use alloy_primitives::Address; + +use crate::encoding::swap_encoder::swap_struct_encoder::{ + BalancerV2SwapEncoder, SwapEncoder, UniswapV2SwapEncoder, +}; + +pub struct SwapEncoderBuilder { + protocol_system: String, + executor_address: Address, +} + +impl SwapEncoderBuilder { + pub fn new(protocol_system: &str, executor_address: &str) -> Self { + SwapEncoderBuilder { + protocol_system: protocol_system.to_string(), + executor_address: Address::from_str(executor_address) + .unwrap_or_else(|_| panic!("Invalid address: {}", executor_address)), + } + } + + pub fn build(self) -> Result, String> { + match self.protocol_system.as_str() { + "uniswap_v2" => Ok(Box::new(UniswapV2SwapEncoder::new(self.executor_address))), + "vm:balancer_v2" => Ok(Box::new(BalancerV2SwapEncoder::new(self.executor_address))), + _ => Err(format!("Unknown protocol system: {}", self.protocol_system)), + } + } +} diff --git a/src/encoding/swap_encoder/config.json b/src/encoding/swap_encoder/config.json new file mode 100644 index 0000000..11bc16e --- /dev/null +++ b/src/encoding/swap_encoder/config.json @@ -0,0 +1,6 @@ +{ + "executors": { + "uniswap_v2": "0x5C2F5a71f67c01775180ADc06909288B4C329308", + "vm:balancer_v2": "0x543778987b293C7E8Cf0722BB2e935ba6f4068D4" + } +} \ No newline at end of file diff --git a/src/encoding/swap_encoder/mod.rs b/src/encoding/swap_encoder/mod.rs new file mode 100644 index 0000000..90cee48 --- /dev/null +++ b/src/encoding/swap_encoder/mod.rs @@ -0,0 +1,16 @@ +use std::sync::RwLock; + +use lazy_static::lazy_static; + +use crate::encoding::swap_encoder::registry::{Config, SwapEncoderRegistry}; + +mod builder; +mod registry; +mod swap_struct_encoder; + +lazy_static! { + pub static ref SWAP_ENCODER_REGISTRY: RwLock = { + let config = Config::from_file("config.json").expect("Failed to load configuration file"); + RwLock::new(SwapEncoderRegistry::new(config)) + }; +} diff --git a/src/encoding/swap_encoder/registry.rs b/src/encoding/swap_encoder/registry.rs new file mode 100644 index 0000000..efcfdda --- /dev/null +++ b/src/encoding/swap_encoder/registry.rs @@ -0,0 +1,45 @@ +use std::{collections::HashMap, fs}; + +use serde::Deserialize; + +use crate::encoding::swap_encoder::{ + builder::SwapEncoderBuilder, swap_struct_encoder::SwapEncoder, +}; + +pub struct SwapEncoderRegistry { + encoders: HashMap>, +} + +impl SwapEncoderRegistry { + pub fn new(config: Config) -> Self { + let mut encoders = HashMap::new(); + + for (protocol, executor_address) in config.executors { + let builder = SwapEncoderBuilder::new(&protocol, &executor_address); + let encoder = builder.build().unwrap_or_else(|_| { + panic!("Failed to build swap encoder for protocol: {}", protocol) + }); + encoders.insert(protocol, encoder); + } + + Self { encoders } + } + + #[allow(clippy::borrowed_box)] + pub fn get_encoder(&self, protocol_system: &str) -> Option<&Box> { + self.encoders.get(protocol_system) + } +} + +#[derive(Deserialize)] +pub struct Config { + pub executors: HashMap, // Protocol -> Executor address mapping +} + +impl Config { + pub fn from_file(path: &str) -> Result { + let config_str = fs::read_to_string(path)?; + let config: Config = serde_json::from_str(&config_str)?; + Ok(config) + } +} diff --git a/src/encoding/swap_encoder/swap_struct_encoder.rs b/src/encoding/swap_encoder/swap_struct_encoder.rs new file mode 100644 index 0000000..e8d8d19 --- /dev/null +++ b/src/encoding/swap_encoder/swap_struct_encoder.rs @@ -0,0 +1,93 @@ +use std::str::FromStr; + +use alloy_primitives::Address; +use alloy_sol_types::SolValue; +use anyhow::Error; + +use crate::encoding::{ + approvals::approvals_manager::ProtocolApprovalsManager, + models::{EncodingContext, Swap}, + utils::bytes_to_address, +}; + +pub trait SwapEncoder: Sync + Send { + fn new(executor_address: Address) -> Self + where + Self: Sized; + fn encode_swap(&self, swap: Swap, encoding_context: EncodingContext) -> Result, Error>; + fn executor_address(&self) -> Address; +} + +pub struct UniswapV2SwapEncoder { + executor_address: Address, +} + +impl UniswapV2SwapEncoder {} +impl SwapEncoder for UniswapV2SwapEncoder { + fn new(executor_address: Address) -> Self { + Self { executor_address } + } + fn encode_swap( + &self, + _swap: Swap, + _encoding_context: EncodingContext, + ) -> Result, Error> { + todo!() + } + + fn executor_address(&self) -> Address { + self.executor_address + } +} + +pub struct BalancerV2SwapEncoder { + executor_address: Address, + vault_address: Address, +} + +impl SwapEncoder for BalancerV2SwapEncoder { + fn new(executor_address: Address) -> Self { + Self { + executor_address, + vault_address: Address::from_str("0xba12222222228d8ba445958a75a0704d566bf2c8") + .expect("Invalid string for balancer vault address"), + } + } + fn encode_swap(&self, swap: Swap, encoding_context: EncodingContext) -> Result, Error> { + let token_approvals_manager = ProtocolApprovalsManager::new(); + let runtime = tokio::runtime::Handle::try_current() + .is_err() + .then(|| tokio::runtime::Runtime::new().unwrap()) + .unwrap(); + let token = bytes_to_address(&swap.token_in)?; + let router_address = bytes_to_address(&encoding_context.address_for_approvals)?; + let approval_needed = runtime.block_on(async { + token_approvals_manager + .approval_needed(token, self.vault_address, router_address) + .await + }); + // should we return gas estimation here too?? if there is an approval needed, gas will be + // higher. + let args = ( + bytes_to_address(&swap.token_in)?, + bytes_to_address(&swap.token_out)?, + swap.component.id, + bytes_to_address(&encoding_context.receiver)?, + encoding_context.exact_out, + approval_needed, + ); + Ok(args.abi_encode()) + } + + fn executor_address(&self) -> Address { + self.executor_address + } +} + +#[cfg(test)] +mod tests { + #[tokio::test] + async fn test_encode_swap() { + // Dummy test to make CI pass. Please implement me. + } +} diff --git a/src/encoding/utils.rs b/src/encoding/utils.rs new file mode 100644 index 0000000..d510fa7 --- /dev/null +++ b/src/encoding/utils.rs @@ -0,0 +1,54 @@ +use alloy_primitives::{Address, Keccak256, U256}; +use alloy_sol_types::SolValue; +use anyhow::Error; +use num_bigint::BigUint; +use tycho_core::Bytes; + +/// Safely converts a `Bytes` object to an `Address` object. +/// +/// Checks the length of the `Bytes` before attempting to convert, and returns a `SimulationError` +/// if not 20 bytes long. +pub fn bytes_to_address(address: &Bytes) -> Result { + if address.len() == 20 { + Ok(Address::from_slice(address)) + } else { + Err(anyhow::format_err!("Invalid ERC20 token address: {:?}", address)) + } +} +pub fn biguint_to_u256(value: &BigUint) -> U256 { + let bytes = value.to_bytes_be(); + U256::from_be_slice(&bytes) +} + +pub fn ple_encode(action_data_array: Vec>) -> Vec { + let mut encoded_action_data: Vec = Vec::new(); + + for action_data in action_data_array { + let args = (encoded_action_data, action_data.len() as u16, action_data); + encoded_action_data = args.abi_encode(); + } + + encoded_action_data +} + +#[allow(dead_code)] +pub fn encode_input(selector: &str, mut encoded_args: Vec) -> Vec { + let mut hasher = Keccak256::new(); + hasher.update(selector.as_bytes()); + let selector_bytes = &hasher.finalize()[..4]; + let mut call_data = selector_bytes.to_vec(); + // Remove extra prefix if present (32 bytes for dynamic data) + // Alloy encoding is including a prefix for dynamic data indicating the offset or length + // but at this point we don't want that + if encoded_args.len() > 32 && + encoded_args[..32] == + [0u8; 31] + .into_iter() + .chain([32].to_vec()) + .collect::>() + { + encoded_args = encoded_args[32..].to_vec(); + } + call_data.extend(encoded_args); + call_data +} diff --git a/src/lib.rs b/src/lib.rs index 5512a6a..35ed87a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1 @@ -#[allow(dead_code)] -fn main() { - println!("Hello, world!"); -} - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} +mod encoding;