feat: implement SwapEncoder for Hashflow
This commit is contained in:
@@ -31,10 +31,6 @@ pub struct Permit2 {
|
||||
address: Address,
|
||||
client: EVMProvider,
|
||||
runtime_handle: Handle,
|
||||
// Store the runtime to prevent it from being dropped before use.
|
||||
// This is required since tycho-execution does not have a pre-existing runtime.
|
||||
// However, if the library is used in a context where a runtime already exists, it is not
|
||||
// necessary to store it.
|
||||
#[allow(dead_code)]
|
||||
runtime: Option<Arc<Runtime>>,
|
||||
}
|
||||
|
||||
@@ -23,10 +23,6 @@ use crate::encoding::{
|
||||
pub struct ProtocolApprovalsManager {
|
||||
client: EVMProvider,
|
||||
runtime_handle: Handle,
|
||||
// Store the runtime to prevent it from being dropped before use.
|
||||
// This is required since tycho-execution does not have a pre-existing runtime.
|
||||
// However, if the library is used in a context where a runtime already exists, it is not
|
||||
// necessary to store it.
|
||||
#[allow(dead_code)]
|
||||
runtime: Option<Arc<Runtime>>,
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ use crate::encoding::{
|
||||
errors::EncodingError,
|
||||
evm::swap_encoder::swap_encoders::{
|
||||
BalancerV2SwapEncoder, BalancerV3SwapEncoder, BebopSwapEncoder, CurveSwapEncoder,
|
||||
EkuboSwapEncoder, MaverickV2SwapEncoder, UniswapV2SwapEncoder, UniswapV3SwapEncoder,
|
||||
UniswapV4SwapEncoder,
|
||||
EkuboSwapEncoder, HashflowSwapEncoder, MaverickV2SwapEncoder, UniswapV2SwapEncoder,
|
||||
UniswapV3SwapEncoder, UniswapV4SwapEncoder,
|
||||
},
|
||||
swap_encoder::SwapEncoder,
|
||||
};
|
||||
@@ -91,6 +91,11 @@ impl SwapEncoderBuilder {
|
||||
"rfq:bebop" => {
|
||||
Ok(Box::new(BebopSwapEncoder::new(self.executor_address, self.chain, self.config)?))
|
||||
}
|
||||
"rfq:hashflow" => Ok(Box::new(HashflowSwapEncoder::new(
|
||||
self.executor_address,
|
||||
self.chain,
|
||||
self.config,
|
||||
)?)),
|
||||
_ => Err(EncodingError::FatalError(format!(
|
||||
"Unknown protocol system: {}",
|
||||
self.protocol_system
|
||||
|
||||
@@ -664,10 +664,6 @@ pub struct BebopSwapEncoder {
|
||||
native_token_bebop_address: Bytes,
|
||||
native_token_address: Bytes,
|
||||
runtime_handle: Handle,
|
||||
// Store the runtime to prevent it from being dropped before use.
|
||||
// This is required since tycho-execution does not have a pre-existing runtime.
|
||||
// However, if the library is used in a context where a runtime already exists, it is not
|
||||
// necessary to store it.
|
||||
#[allow(dead_code)]
|
||||
runtime: Option<Arc<Runtime>>,
|
||||
}
|
||||
@@ -850,6 +846,148 @@ impl SwapEncoder for BebopSwapEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HashflowSwapEncoder {
|
||||
executor_address: String,
|
||||
hashflow_router_address: String,
|
||||
native_token_address: Bytes,
|
||||
runtime_handle: Handle,
|
||||
#[allow(dead_code)]
|
||||
runtime: Option<Arc<Runtime>>,
|
||||
}
|
||||
|
||||
impl SwapEncoder for HashflowSwapEncoder {
|
||||
fn new(
|
||||
executor_address: String,
|
||||
chain: Chain,
|
||||
config: Option<HashMap<String, String>>,
|
||||
) -> Result<Self, EncodingError> {
|
||||
let config = config.ok_or(EncodingError::FatalError(
|
||||
"Missing hashflow specific addresses in config".to_string(),
|
||||
))?;
|
||||
let hashflow_router_address = config
|
||||
.get("hashflow_router_address")
|
||||
.ok_or(EncodingError::FatalError(
|
||||
"Missing bebop settlement address in config".to_string(),
|
||||
))?
|
||||
.to_string();
|
||||
let native_token_address = chain.native_token().address;
|
||||
let (runtime_handle, runtime) = get_runtime()?;
|
||||
Ok(Self {
|
||||
executor_address,
|
||||
hashflow_router_address,
|
||||
native_token_address,
|
||||
runtime_handle,
|
||||
runtime,
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_swap(
|
||||
&self,
|
||||
swap: &Swap,
|
||||
encoding_context: &EncodingContext,
|
||||
) -> Result<Vec<u8>, EncodingError> {
|
||||
// Native tokens doesn't need approval, only ERC20 tokens do
|
||||
let approval_needed: bool;
|
||||
if let Some(router_address) = &encoding_context.router_address {
|
||||
let tycho_router_address = bytes_to_address(router_address)?;
|
||||
let hashflow_router_address = Address::from_str(&self.hashflow_router_address)
|
||||
.map_err(|_| {
|
||||
EncodingError::FatalError("Invalid hashflow router address address".to_string())
|
||||
})?;
|
||||
|
||||
// Native ETH doesn't need approval, only ERC20 tokens do
|
||||
if swap.token_in == self.native_token_address {
|
||||
approval_needed = false;
|
||||
} else {
|
||||
approval_needed = ProtocolApprovalsManager::new()?.approval_needed(
|
||||
bytes_to_address(&swap.token_in)?,
|
||||
tycho_router_address,
|
||||
hashflow_router_address,
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
approval_needed = true;
|
||||
}
|
||||
|
||||
// Get quote
|
||||
let protocol_state = swap
|
||||
.protocol_state
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
EncodingError::FatalError("protocol_state is required for Hashflow".to_string())
|
||||
})?;
|
||||
let amount_in = swap
|
||||
.estimated_amount_in
|
||||
.as_ref()
|
||||
.ok_or(EncodingError::FatalError(
|
||||
"Estimated amount in is mandatory for a Hashflow swap".to_string(),
|
||||
))?
|
||||
.clone();
|
||||
let sender = encoding_context
|
||||
.router_address
|
||||
.clone()
|
||||
.ok_or(EncodingError::FatalError(
|
||||
"The router address is needed to perform a Hashflow swap".to_string(),
|
||||
))?;
|
||||
let signed_quote = block_in_place(|| {
|
||||
self.runtime_handle.block_on(async {
|
||||
protocol_state
|
||||
.as_indicatively_priced()?
|
||||
.request_signed_quote(GetAmountOutParams {
|
||||
amount_in,
|
||||
token_in: swap.token_in.clone(),
|
||||
token_out: swap.token_out.clone(),
|
||||
sender,
|
||||
receiver: encoding_context.receiver.clone(),
|
||||
})
|
||||
.await
|
||||
})
|
||||
})?;
|
||||
|
||||
// Encode packed data for the executor
|
||||
// Format: approval_needed | transfer_type | hashflow_calldata[..]
|
||||
let hashflow_fields = [
|
||||
"pool",
|
||||
"external_account",
|
||||
"trader",
|
||||
"effective_trader",
|
||||
"base_token",
|
||||
"quote_token",
|
||||
"base_token_amount",
|
||||
"quote_token_amount",
|
||||
"quote_expiry",
|
||||
"nonce",
|
||||
"tx_id",
|
||||
"signature",
|
||||
];
|
||||
let mut hashflow_calldata = vec![];
|
||||
for field in &hashflow_fields {
|
||||
let value = signed_quote
|
||||
.quote_attributes
|
||||
.get(*field)
|
||||
.ok_or(EncodingError::FatalError(format!(
|
||||
"Hashflow quote must have a {field} attribute"
|
||||
)))?;
|
||||
hashflow_calldata.extend_from_slice(value);
|
||||
}
|
||||
let args = (
|
||||
(encoding_context.transfer_type as u8).to_be_bytes(),
|
||||
(approval_needed as u8).to_be_bytes(),
|
||||
&hashflow_calldata[..],
|
||||
);
|
||||
Ok(args.abi_encode_packed())
|
||||
}
|
||||
|
||||
fn executor_address(&self) -> &str {
|
||||
&self.executor_address
|
||||
}
|
||||
|
||||
fn clone_box(&self) -> Box<dyn SwapEncoder> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
@@ -1985,7 +2123,7 @@ mod tests {
|
||||
};
|
||||
let bebop_state = MockRFQState {
|
||||
quote_amount_out,
|
||||
quote_calldata: bebop_calldata.clone(),
|
||||
quote_data: vec![("calldata".to_string(), bebop_calldata.clone())],
|
||||
quote_partial_fill_offset: partial_fill_offset,
|
||||
};
|
||||
|
||||
@@ -2037,4 +2175,152 @@ mod tests {
|
||||
assert_eq!(hex_swap, expected_swap + &bebop_calldata.to_string()[2..]);
|
||||
}
|
||||
}
|
||||
|
||||
mod hashflow {
|
||||
use alloy::hex::encode;
|
||||
use num_bigint::BigUint;
|
||||
|
||||
use super::*;
|
||||
use crate::encoding::{
|
||||
evm::testing_utils::MockRFQState,
|
||||
models::{SwapBuilder, TransferType},
|
||||
};
|
||||
|
||||
fn hashflow_config() -> Option<HashMap<String, String>> {
|
||||
Some(HashMap::from([(
|
||||
"hashflow_router_address".to_string(),
|
||||
"0x55084eE0fEf03f14a305cd24286359A35D735151".to_string(),
|
||||
)]))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_hashflow_single_with_user_data() {
|
||||
// Hashflow requires a swap with protocol data, otherwise will return an error
|
||||
let hashflow_component = ProtocolComponent {
|
||||
id: String::from("hashflow-rfq"),
|
||||
protocol_system: String::from("rfq:hashflow"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
|
||||
let token_out = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); // WETH
|
||||
|
||||
let swap = SwapBuilder::new(hashflow_component, token_in.clone(), token_out.clone())
|
||||
.estimated_amount_in(BigUint::from_str("3000000000").unwrap())
|
||||
.build();
|
||||
|
||||
let encoding_context = EncodingContext {
|
||||
receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"),
|
||||
exact_out: false,
|
||||
router_address: Some(Bytes::zero(20)),
|
||||
group_token_in: token_in.clone(),
|
||||
group_token_out: token_out.clone(),
|
||||
transfer_type: TransferType::Transfer,
|
||||
};
|
||||
|
||||
let encoder = HashflowSwapEncoder::new(
|
||||
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
|
||||
Chain::Ethereum,
|
||||
hashflow_config(),
|
||||
)
|
||||
.unwrap();
|
||||
encoder
|
||||
.encode_swap(&swap, &encoding_context)
|
||||
.expect_err("Should returned an error if the swap has no protocol state");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_hashflow_single_with_protocol_state() {
|
||||
// 3000 USDC -> 1 WETH using a mocked RFQ state to get a quote
|
||||
let partial_fill_offset = 12u64;
|
||||
let quote_amount_out = BigUint::from_str("1000000000000000000").unwrap();
|
||||
|
||||
let hashflow_component = ProtocolComponent {
|
||||
id: String::from("hashflow-rfq"),
|
||||
protocol_system: String::from("rfq:hashflow"),
|
||||
..Default::default()
|
||||
};
|
||||
let hashflow_quote_data = vec![
|
||||
(
|
||||
"pool".to_string(),
|
||||
Bytes::from_str("0x031903307c517c11b71f8313d19afde0a4f41cb5").unwrap(),
|
||||
),
|
||||
(
|
||||
"trader".to_string(),
|
||||
Bytes::from_str("0x5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").unwrap(),
|
||||
),
|
||||
("nonce".to_string(), Bytes::from(1755512134141u64.to_be_bytes().to_vec())),
|
||||
(
|
||||
"tx_id".to_string(),
|
||||
Bytes::from_str(
|
||||
"0x125000064000640000001747937188ffffffffffffff00295e467232b36d0000",
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
("signature".to_string(), Bytes::from_str("0xfda99100ffd8adfc818a827e1698c1d1fa2f59f7723ff84bfeba0f80e5298b1077f590d8d99aec6f6801c611eb270b5d89fac02a680ab38f03f3c5d16039c6f11c").unwrap()),
|
||||
("quote_expiry".to_string(), Bytes::from(1755512162u64.to_be_bytes().to_vec())),
|
||||
(
|
||||
"external_account".to_string(),
|
||||
Bytes::from_str("0xbb289bc97591f70d8216462df40ed713011b968a").unwrap(),
|
||||
),
|
||||
(
|
||||
"effective_trader".to_string(),
|
||||
Bytes::from_str("0xcd09f75e2bf2a4d11f3ab23f1389fcc1621c0cc2").unwrap(),
|
||||
),
|
||||
];
|
||||
let hashflow_quote_data_values =
|
||||
hashflow_quote_data
|
||||
.iter()
|
||||
.fold(vec![], |mut acc, (_key, value)| {
|
||||
acc.extend_from_slice(value);
|
||||
acc
|
||||
});
|
||||
let hashflow_calldata = Bytes::from(hashflow_quote_data_values);
|
||||
let hashflow_state = MockRFQState {
|
||||
quote_amount_out,
|
||||
quote_data: hashflow_quote_data,
|
||||
quote_partial_fill_offset: partial_fill_offset,
|
||||
};
|
||||
|
||||
let token_in = Bytes::from("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); // USDC
|
||||
let token_out = Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); // WETH
|
||||
|
||||
let swap = SwapBuilder::new(hashflow_component, token_in.clone(), token_out.clone())
|
||||
.estimated_amount_in(BigUint::from_str("3000000000").unwrap())
|
||||
.protocol_state(&hashflow_state)
|
||||
.build();
|
||||
|
||||
let encoding_context = EncodingContext {
|
||||
receiver: Bytes::from("0xc5564C13A157E6240659fb81882A28091add8670"),
|
||||
exact_out: false,
|
||||
router_address: Some(Bytes::zero(20)),
|
||||
group_token_in: token_in.clone(),
|
||||
group_token_out: token_out.clone(),
|
||||
transfer_type: TransferType::Transfer,
|
||||
};
|
||||
|
||||
let encoder = HashflowSwapEncoder::new(
|
||||
String::from("0x543778987b293C7E8Cf0722BB2e935ba6f4068D4"),
|
||||
Chain::Ethereum,
|
||||
hashflow_config(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let encoded_swap = encoder
|
||||
.encode_swap(&swap, &encoding_context)
|
||||
.unwrap();
|
||||
let hex_swap = encode(&encoded_swap);
|
||||
|
||||
let expected_swap = String::from(concat!(
|
||||
"01", // transfer type
|
||||
"01", // approval needed
|
||||
));
|
||||
assert_eq!(hex_swap, expected_swap + &hashflow_calldata.to_string()[2..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_hashflow_aggregate_with_protocol_state() {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use tycho_common::{
|
||||
#[derive(Debug)]
|
||||
pub struct MockRFQState {
|
||||
pub quote_amount_out: BigUint,
|
||||
pub quote_calldata: Bytes,
|
||||
pub quote_data: Vec<(String, Bytes)>,
|
||||
pub quote_partial_fill_offset: u64,
|
||||
}
|
||||
impl ProtocolSim for MockRFQState {
|
||||
@@ -83,7 +83,9 @@ impl IndicativelyPriced for MockRFQState {
|
||||
params: GetAmountOutParams,
|
||||
) -> Result<SignedQuote, SimulationError> {
|
||||
let mut quote_attributes: HashMap<String, Bytes> = HashMap::new();
|
||||
quote_attributes.insert("calldata".to_string(), self.quote_calldata.clone());
|
||||
for (attr, value) in &self.quote_data {
|
||||
quote_attributes.insert(attr.clone(), value.clone());
|
||||
}
|
||||
quote_attributes.insert(
|
||||
"partial_fill_offset".to_string(),
|
||||
Bytes::from(
|
||||
|
||||
@@ -78,6 +78,9 @@ pub fn get_static_attribute(swap: &Swap, attribute_name: &str) -> Result<Vec<u8>
|
||||
.to_vec())
|
||||
}
|
||||
|
||||
/// Returns the current Tokio runtime handle, or creates a new one if it doesn't exist.
|
||||
/// It also returns the runtime to prevent it from being dropped before use.
|
||||
/// This is required since tycho-execution does not have a pre-existing runtime.
|
||||
pub fn get_runtime() -> Result<(Handle, Option<Arc<Runtime>>), EncodingError> {
|
||||
match Handle::try_current() {
|
||||
Ok(h) => Ok((h, None)),
|
||||
|
||||
Reference in New Issue
Block a user