feat: Create a EVMEncoderBuilder

- It has two three methods to be created:
  - new: where the user can pass any custom StrategyEncoder
  - tycho_router: where the default SplitSwapStrategyEncoder with Tycho Router will be used
  - direct_execution: where the user can encode only the execution data and integrate this into their workflow.
- This replaces StrategyEncoderRegistry
- EVMTychoEncoder holds directly the StrategyEncoder and not the registry (per one init of the EVMTychoEncoder there is only one possible StrategyEncoder)
- Update quickstart
- Update bin (add subcommands to bin to create a different instance of EVMEncoderBuilder)

--- don't change below this line ---
ENG-4246 Took 33 minutes

Took 38 seconds

Took 38 seconds
This commit is contained in:
Diana Carvalho
2025-02-18 16:49:38 +00:00
parent f76e320ff7
commit 03506fabe9
15 changed files with 156 additions and 186 deletions

View File

@@ -24,30 +24,46 @@ cargo build --release
cargo install --path .
```
After installation, the `tycho-encode` command will be available to use from any directory in your terminal. The command
accepts the following options:
After installation, the `tycho-encode` command will be available to use from any directory in your terminal.
### Commands
The command lets you choose the encoding strategy to be used. The available strategies are:
#### Tycho Router
`tycho-router`: Encodes a transaction using the Tycho Router encoding strategy. Requires a private key for signing
Permit2.
Example:
```bash
echo '<solution_payload>' | tycho-encode tycho-router -p 0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234
```
#### Direct execution
`direct-execution`: Encodes a transaction using the direct execution encoding strategy. Does not require a private key.
Example:
```bash
echo '<solution_payload>' | tycho-encode direct-execution
```
### Encoding Transactions
The commands accept the following options:
- `-c`: Path to the executor addresses configuration file (defaults to `src/encoding/config/executor_addresses.json`)
- `-p`: Private key for signing approvals (required when direct_execution is false)
### Encoding Transactions
To encode a transaction, you can pipe a JSON payload to the binary:
```bash
# Using default config path
echo '<solution_payload>' | tycho-encode
# Using custom config path
echo '<solution_payload>' | tycho-encode -c /path/to/your/config.json
```
#### Example
Here's a complete example that encodes a swap from WETH to DAI using Uniswap V2:
Here's a complete example that encodes a swap from WETH to DAI using Uniswap V2 and the Tycho Router strategy:
```bash
echo '{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","checked_amount":"990000000000000000","router_address":"0xaa820C29648D5EA543d712cC928377Bd7206a0E7","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f"},"change":"Update","creation_tx":"0x0000000000000000000000000000000000000000000000000000000000000000","created_at":"2024-02-28T12:00:00"},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":1.0}],"direct_execution":true}' | tycho-encode
echo '{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"slippage":0.01,"expected_amount":"1000000000000000000","checked_amount":"990000000000000000","router_address":"0xaa820C29648D5EA543d712cC928377Bd7206a0E7","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f"},"change":"Update","creation_tx":"0x0000000000000000000000000000000000000000000000000000000000000000","created_at":"2024-02-28T12:00:00"},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":0.0}],"direct_execution":true}' | tycho-encode tycho-router -p 0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234
```
#### JSON Payload Structure: Solution struct

View File

@@ -6,12 +6,8 @@ use tycho_core::{
Bytes,
};
use tycho_execution::encoding::{
evm::{
strategy_encoder::strategy_encoder_registry::EVMStrategyEncoderRegistry,
tycho_encoder::EVMTychoEncoder,
},
evm::encoder_builder::EVMEncoderBuilder,
models::{Solution, Swap},
strategy_encoder::StrategyEncoderRegistry,
tycho_encoder::TychoEncoder,
};
@@ -20,16 +16,15 @@ fn main() {
let router_address = Bytes::from_str("0x1234567890abcdef1234567890abcdef12345678")
.expect("Failed to create router address");
let signer_pk =
Some("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string());
"0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234".to_string();
let user_address = Bytes::from_str("0xcd09f75E2BF2A4d11F3AB23f1389FcC1621c0cc2")
.expect("Failed to create user address");
// Initialize the encoder
let strategy_encoder_registry =
EVMStrategyEncoderRegistry::new(TychoCoreChain::Ethereum, None, signer_pk.clone())
.expect("Failed to create strategy encoder registry");
let encoder = EVMTychoEncoder::new(strategy_encoder_registry, TychoCoreChain::Ethereum)
.expect("Failed to create encoder");
let encoder = EVMEncoderBuilder::tycho_router(TychoCoreChain::Ethereum, signer_pk, None)
.expect("Failed to create encoder builder")
.build()
.expect("Failed to build encoder");
// ------------------- Encode a simple swap -------------------

View File

@@ -1,4 +1,5 @@
pub use clap::Parser;
use clap::Subcommand;
#[derive(Parser)]
/// Encode swap transactions for the Tycho router
@@ -33,12 +34,24 @@ pub use clap::Parser;
/// "direct_execution": false
/// }
/// ```
#[command(author, version, about, long_about = None)]
pub struct Cli {
/// Private key for signing approvals (required when direct_execution is false)
#[arg(short)]
pub private_key: Option<String>,
/// Path to the executor addresses configuration file
#[arg(short)]
pub config_path: Option<String>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
/// Use the Tycho router encoding strategy
TychoRouter {
#[arg(short, long)]
config_path: Option<String>,
#[arg(short, long)]
private_key: String,
},
/// Use the direct execution encoding strategy
DirectExecution {
#[arg(short, long)]
config_path: Option<String>,
},
}

View File

@@ -3,21 +3,16 @@ use std::io::{self, Read};
use clap::Parser;
use serde_json::Value;
use tycho_core::dto::Chain;
use tycho_execution::encoding::{
evm::{
strategy_encoder::strategy_encoder_registry::EVMStrategyEncoderRegistry,
tycho_encoder::EVMTychoEncoder,
},
models::Solution,
strategy_encoder::StrategyEncoderRegistry,
tycho_encoder::TychoEncoder,
};
use tycho_execution::encoding::{models::Solution, tycho_encoder::TychoEncoder};
mod lib {
pub mod cli;
}
use lib::cli::Cli;
use tycho_execution::encoding::{errors::EncodingError, evm::encoder_builder::EVMEncoderBuilder};
use crate::lib::cli::Commands;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
@@ -33,8 +28,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
// Encode the solution
let encoded = encode_swaps(&buffer, cli.config_path, cli.private_key)?;
let encoded = match cli.command {
Commands::TychoRouter { config_path, private_key } => {
encode_swaps(&buffer, config_path, Some(private_key), true)?
}
Commands::DirectExecution { config_path } => {
encode_swaps(&buffer, config_path, None, false)?
}
};
// Output the encoded result as JSON to stdout
println!(
"{}",
@@ -49,12 +50,20 @@ fn encode_swaps(
input: &str,
config_path: Option<String>,
private_key: Option<String>,
) -> Result<Value, Box<dyn std::error::Error>> {
use_tycho_router: bool,
) -> Result<Value, EncodingError> {
let solution: Solution = serde_json::from_str(input)?;
let chain = Chain::Ethereum;
let strategy_selector = EVMStrategyEncoderRegistry::new(chain, config_path, private_key)?;
let encoder = EVMTychoEncoder::new(strategy_selector, chain)?;
let encoder = if use_tycho_router {
let private_key = private_key.ok_or(EncodingError::FatalError(
"Private key is required for tycho_router".to_string(),
))?;
EVMEncoderBuilder::tycho_router(chain, private_key, config_path)?.build()?
} else {
EVMEncoderBuilder::direct_execution(chain, config_path)?.build()?
};
let transactions = encoder.encode_router_calldata(vec![solution])?;
Ok(serde_json::json!({

View File

@@ -0,0 +1,44 @@
use tycho_core::dto::Chain;
use crate::encoding::{
errors::EncodingError,
evm::{
strategy_encoders::{ExecutorStrategyEncoder, SplitSwapStrategyEncoder},
swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
tycho_encoder::EVMTychoEncoder,
},
strategy_encoder::StrategyEncoder,
};
pub struct EVMEncoderBuilder {
strategy: Box<dyn StrategyEncoder>,
chain: Chain,
}
impl EVMEncoderBuilder {
pub fn new(chain: Chain, strategy: Box<dyn StrategyEncoder>) -> Self {
EVMEncoderBuilder { chain, strategy }
}
pub fn tycho_router(
chain: Chain,
signer_pk: String,
executors_file_path: Option<String>,
) -> Result<Self, EncodingError> {
let swap_encoder_registry = SwapEncoderRegistry::new(executors_file_path, chain)?;
let strategy =
Box::new(SplitSwapStrategyEncoder::new(signer_pk, chain, swap_encoder_registry)?);
Ok(EVMEncoderBuilder { chain, strategy })
}
pub fn direct_execution(
chain: Chain,
executors_file_path: Option<String>,
) -> Result<Self, EncodingError> {
let swap_encoder_registry = SwapEncoderRegistry::new(executors_file_path, chain)?;
let strategy = Box::new(ExecutorStrategyEncoder::new(swap_encoder_registry));
Ok(EVMEncoderBuilder { chain, strategy })
}
pub fn build(self) -> Result<EVMTychoEncoder, EncodingError> {
EVMTychoEncoder::new(self.chain, self.strategy)
}
}

View File

@@ -1,6 +1,7 @@
pub mod approvals;
mod constants;
pub mod strategy_encoder;
pub mod encoder_builder;
pub mod strategy_encoders;
mod swap_encoder;
pub mod tycho_encoder;
pub mod utils;

View File

@@ -1,4 +1,3 @@
mod group_swaps;
pub mod strategy_encoder_registry;
mod strategy_encoders;
mod strategy_validators;

View File

@@ -1,69 +0,0 @@
use std::collections::HashMap;
use crate::encoding::{
errors::EncodingError,
evm::{
strategy_encoder::strategy_encoders::{ExecutorStrategyEncoder, SplitSwapStrategyEncoder},
swap_encoder::swap_encoder_registry::SwapEncoderRegistry,
},
models::{Chain, Solution},
strategy_encoder::{StrategyEncoder, StrategyEncoderRegistry},
};
/// Contains all supported strategies to encode a solution.
///
/// # Fields
/// * `strategies` - A hashmap containing the name of the strategy as a key and the strategy encoder
/// as a value.
pub struct EVMStrategyEncoderRegistry {
strategies: HashMap<String, Box<dyn StrategyEncoder>>,
}
impl StrategyEncoderRegistry for EVMStrategyEncoderRegistry {
fn new(
chain: tycho_core::dto::Chain,
executors_file_path: Option<String>,
signer_pk: Option<String>,
) -> Result<Self, EncodingError> {
let chain = Chain::from(chain);
let swap_encoder_registry = SwapEncoderRegistry::new(executors_file_path, chain.clone())?;
let mut strategies: HashMap<String, Box<dyn StrategyEncoder>> = HashMap::new();
strategies.insert(
"executor".to_string(),
Box::new(ExecutorStrategyEncoder::new(swap_encoder_registry.clone())),
);
if let Some(signer) = signer_pk {
strategies.insert(
"split_swap".to_string(),
Box::new(
SplitSwapStrategyEncoder::new(signer, chain, swap_encoder_registry).unwrap(),
),
);
}
Ok(Self { strategies })
}
fn get_encoder(&self, solution: &Solution) -> Result<&Box<dyn StrategyEncoder>, EncodingError> {
if solution.direct_execution {
self.strategies
.get("executor")
.ok_or(EncodingError::FatalError("Executor strategy not found".to_string()))
} else {
self.strategies
.get("split_swap")
.ok_or(EncodingError::FatalError("Split swap strategy not found. Please pass the signer private key to the StrategySelector constructor".to_string()))
}
}
}
impl Clone for EVMStrategyEncoderRegistry {
fn clone(&self) -> Self {
Self {
strategies: self
.strategies
.iter()
.map(|(k, v)| (k.clone(), v.clone_box()))
.collect(),
}
}
}

View File

@@ -89,9 +89,10 @@ pub struct SplitSwapStrategyEncoder {
impl SplitSwapStrategyEncoder {
pub fn new(
signer_pk: String,
chain: Chain,
blockchain: tycho_core::dto::Chain,
swap_encoder_registry: SwapEncoderRegistry,
) -> Result<Self, EncodingError> {
let chain = Chain::from(blockchain);
let selector = "swap(uint256,address,address,uint256,bool,bool,uint256,address,((address,uint160,uint48,uint48),address,uint256),bytes,bytes)".to_string();
Ok(Self {
permit2: Permit2::new(signer_pk, chain.clone())?,
@@ -337,8 +338,8 @@ mod tests {
use super::*;
use crate::encoding::models::Swap;
fn eth_chain() -> Chain {
TychoCoreChain::Ethereum.into()
fn eth_chain() -> TychoCoreChain {
TychoCoreChain::Ethereum
}
fn eth() -> Bytes {

View File

@@ -19,8 +19,9 @@ impl SwapEncoderRegistry {
/// executors' addresses in the file at the given path.
pub fn new(
executors_file_path: Option<String>,
blockchain: Chain,
blockchain: tycho_core::dto::Chain,
) -> Result<Self, EncodingError> {
let chain = Chain::from(blockchain);
let config_str = if let Some(ref path) = executors_file_path {
fs::read_to_string(path).map_err(|e| {
EncodingError::FatalError(format!(
@@ -34,8 +35,8 @@ impl SwapEncoderRegistry {
let config: HashMap<String, HashMap<String, String>> = serde_json::from_str(&config_str)?;
let mut encoders = HashMap::new();
let executors = config
.get(&blockchain.name)
.ok_or(EncodingError::FatalError("No executors found for blockchain".to_string()))?;
.get(&chain.name)
.ok_or(EncodingError::FatalError("No executors found for chain".to_string()))?;
for (protocol, executor_address) in executors {
let builder = SwapEncoderBuilder::new(protocol, executor_address);
let encoder = builder.build()?;

View File

@@ -4,26 +4,27 @@ use tycho_core::Bytes;
use crate::encoding::{
errors::EncodingError,
models::{Chain, NativeAction, Solution, Transaction},
strategy_encoder::StrategyEncoderRegistry,
strategy_encoder::StrategyEncoder,
tycho_encoder::TychoEncoder,
};
/// Represents an encoder for a swap using any strategy supported by the strategy registry.
///
/// # Fields
/// * `strategy_registry`: S, the strategy registry to use to select the best strategy to encode a
/// solution, based on its supported strategies and the solution attributes.
/// * `strategy_encoder`: Strategy encoder to follow for encoding the solution
/// * `native_address`: Address of the chain's native token
/// * `wrapped_address`: Address of the chain's wrapped native token
#[derive(Clone)]
pub struct EVMTychoEncoder<S: StrategyEncoderRegistry> {
strategy_registry: S,
pub struct EVMTychoEncoder {
strategy_encoder: Box<dyn StrategyEncoder>,
native_address: Bytes,
wrapped_address: Bytes,
}
impl<S: StrategyEncoderRegistry> EVMTychoEncoder<S> {
pub fn new(strategy_registry: S, chain: tycho_core::dto::Chain) -> Result<Self, EncodingError> {
impl EVMTychoEncoder {
pub fn new(
chain: tycho_core::dto::Chain,
strategy_encoder: Box<dyn StrategyEncoder>,
) -> Result<Self, EncodingError> {
let chain: Chain = Chain::from(chain);
if chain.name != *"ethereum" {
return Err(EncodingError::InvalidInput(
@@ -31,14 +32,14 @@ impl<S: StrategyEncoderRegistry> EVMTychoEncoder<S> {
));
}
Ok(EVMTychoEncoder {
strategy_registry,
strategy_encoder,
native_address: chain.native_token()?,
wrapped_address: chain.wrapped_token()?,
})
}
}
impl<S: StrategyEncoderRegistry> EVMTychoEncoder<S> {
impl EVMTychoEncoder {
/// Raises an `EncodingError` if the solution is not considered valid.
///
/// A solution is considered valid if all the following conditions are met:
@@ -92,7 +93,7 @@ impl<S: StrategyEncoderRegistry> EVMTychoEncoder<S> {
}
}
impl<S: StrategyEncoderRegistry> TychoEncoder<S> for EVMTychoEncoder<S> {
impl TychoEncoder for EVMTychoEncoder {
fn encode_router_calldata(
&self,
solutions: Vec<Solution>,
@@ -101,11 +102,9 @@ impl<S: StrategyEncoderRegistry> TychoEncoder<S> for EVMTychoEncoder<S> {
for solution in solutions.iter() {
self.validate_solution(solution)?;
let strategy = self
.strategy_registry
.get_encoder(solution)?;
let (contract_interaction, target_address, selector) =
strategy.encode_strategy(solution.clone())?;
let (contract_interaction, target_address, selector) = self
.strategy_encoder
.encode_strategy(solution.clone())?;
let value = match solution.native_action.as_ref() {
Some(NativeAction::Wrap) => solution.given_amount.clone(),
@@ -134,10 +133,6 @@ mod tests {
models::Swap, strategy_encoder::StrategyEncoder, swap_encoder::SwapEncoder,
};
struct MockStrategyRegistry {
strategy: Box<dyn StrategyEncoder>,
}
fn dai() -> Bytes {
Bytes::from_str("0x6b175474e89094c44da98b954eedeac495271d0f").unwrap()
}
@@ -150,23 +145,6 @@ mod tests {
Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap()
}
impl StrategyEncoderRegistry for MockStrategyRegistry {
fn new(
_chain: tycho_core::dto::Chain,
_executors_file_path: Option<String>,
_signer_pk: Option<String>,
) -> Result<MockStrategyRegistry, EncodingError> {
Ok(Self { strategy: Box::new(MockStrategy) })
}
fn get_encoder(
&self,
_solution: &Solution,
) -> Result<&Box<dyn StrategyEncoder>, EncodingError> {
Ok(&self.strategy)
}
}
#[derive(Clone)]
struct MockStrategy;
@@ -192,10 +170,9 @@ mod tests {
}
}
fn get_mocked_tycho_encoder() -> EVMTychoEncoder<MockStrategyRegistry> {
let strategy_registry =
MockStrategyRegistry::new(TychoCoreChain::Ethereum, None, None).unwrap();
EVMTychoEncoder::new(strategy_registry, TychoCoreChain::Ethereum).unwrap()
fn get_mocked_tycho_encoder() -> EVMTychoEncoder {
let strategy_encoder = Box::new(MockStrategy {});
EVMTychoEncoder::new(TychoCoreChain::Ethereum, strategy_encoder).unwrap()
}
#[test]

View File

@@ -1,4 +1,4 @@
mod errors;
pub mod errors;
#[cfg(feature = "evm")]
pub mod evm;
pub mod models;

View File

@@ -1,4 +1,4 @@
use tycho_core::{dto::Chain, Bytes};
use tycho_core::Bytes;
use crate::encoding::{errors::EncodingError, models::Solution, swap_encoder::SwapEncoder};
@@ -13,19 +13,3 @@ pub trait StrategyEncoder {
fn get_swap_encoder(&self, protocol_system: &str) -> Option<&Box<dyn SwapEncoder>>;
fn clone_box(&self) -> Box<dyn StrategyEncoder>;
}
/// Contains the supported strategies to encode a solution, and chooses the best strategy to encode
/// a solution based on the solution's attributes.
pub trait StrategyEncoderRegistry {
fn new(
chain: Chain,
executors_file_path: Option<String>,
signer_pk: Option<String>,
) -> Result<Self, EncodingError>
where
Self: Sized;
/// Returns the strategy encoder that should be used to encode the given solution.
#[allow(clippy::borrowed_box)]
fn get_encoder(&self, solution: &Solution) -> Result<&Box<dyn StrategyEncoder>, EncodingError>;
}

View File

@@ -1,12 +1,11 @@
use crate::encoding::{
errors::EncodingError,
models::{Solution, Transaction},
strategy_encoder::StrategyEncoderRegistry,
};
/// An encoder must implement this trait in order to encode a solution into a Transaction for
/// execution using a Tycho router or related contracts.
pub trait TychoEncoder<S: StrategyEncoderRegistry> {
pub trait TychoEncoder {
fn encode_router_calldata(
&self,
solutions: Vec<Solution>,