Skip to main content

odos_sdk/
v3_router.rs

1// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{fmt::Debug, marker::PhantomData};
6
7use alloy_contract::CallBuilder;
8use alloy_network::{Ethereum, Network};
9use alloy_primitives::{Address, Bytes, U256};
10use alloy_provider::Provider;
11use alloy_sol_types::{sol, SolInterface};
12
13use crate::SwapInputs;
14
15// Import generated types after sol! macro
16use IOdosRouterV3::{inputTokenInfo, outputTokenInfo, swapReferralInfo, swapTokenInfo};
17use OdosV3Router::{swapCall, OdosV3RouterCalls, OdosV3RouterInstance, Swap, SwapMulti};
18
19/// The V3 SOR Router contract.
20///
21/// This router is generic over the network type, allowing it to work with both standard
22/// Ethereum networks and OP-stack networks (Optimism, Base, Fraxtal).
23///
24/// # Type Parameters
25///
26/// - `N`: The network type (defaults to `Ethereum`). Use `op_alloy_network::Optimism` for OP-stack chains.
27/// - `P`: The provider type.
28///
29/// # Example
30///
31/// ```rust,ignore
32/// use odos_sdk::V3Router;
33/// use alloy_network::Ethereum;
34/// use alloy_provider::ProviderBuilder;
35///
36/// // Standard Ethereum usage
37/// let provider = ProviderBuilder::new().connect_http("https://eth.llamarpc.com".parse()?);
38/// let router: V3Router<Ethereum, _> = V3Router::new(address, provider);
39///
40/// // OP-stack usage
41/// use op_alloy_network::Optimism;
42/// let op_provider = ProviderBuilder::new()
43///     .network::<Optimism>()
44///     .connect_http("https://mainnet.base.org".parse()?);
45/// let op_router: V3Router<Optimism, _> = V3Router::new(address, op_provider);
46/// ```
47#[derive(Debug, Clone)]
48pub struct V3Router<N: Network = Ethereum, P: Provider<N> = alloy_provider::RootProvider<N>> {
49    instance: OdosV3RouterInstance<P, N>,
50}
51
52impl<N: Network, P: Provider<N>> V3Router<N, P> {
53    /// Creates a new V3 router instance.
54    pub fn new(address: Address, provider: P) -> Self {
55        Self {
56            instance: OdosV3RouterInstance::new(address, provider),
57        }
58    }
59
60    /// Returns the contract owner address.
61    pub async fn owner(&self) -> Result<Address, alloy_contract::Error> {
62        self.instance.owner().call().await
63    }
64
65    /// Returns the liquidator address.
66    pub async fn liquidator_address(&self) -> Result<Address, alloy_contract::Error> {
67        self.instance.liquidatorAddress().call().await
68    }
69
70    /// Builds a swap call using router funds.
71    pub fn build_swap_router_funds_call(
72        &self,
73        input_token_info: inputTokenInfo,
74        output_token_info: outputTokenInfo,
75        inputs: &SwapInputs,
76        from: Address,
77    ) -> CallBuilder<&P, PhantomData<OdosV3Router::swapRouterFundsCall>, N> {
78        self.instance
79            .swapRouterFunds(
80                vec![input_token_info],
81                vec![output_token_info],
82                inputs.path_definition().clone(),
83                inputs.executor(),
84            )
85            .from(from)
86    }
87
88    /// Transfers router funds to a recipient.
89    pub fn transfer_router_funds(
90        &self,
91        from: Address,
92        token: Address,
93        amount: U256,
94        output_recipient: Address,
95    ) -> CallBuilder<&P, PhantomData<OdosV3Router::transferRouterFundsCall>, N> {
96        self.instance
97            .transferRouterFunds(vec![token], vec![amount], output_recipient)
98            .from(from)
99    }
100
101    /// Returns the calldata for a transfer router funds call.
102    pub fn transfer_router_funds_calldata(
103        &self,
104        from: Address,
105        token: Address,
106        amount: U256,
107        output_recipient: Address,
108    ) -> Vec<u8> {
109        self.transfer_router_funds(from, token, amount, output_recipient)
110            .calldata()
111            .to_vec()
112    }
113}
114
115// codegen the odos_v3_router contract
116sol!(
117    #[allow(clippy::too_many_arguments)]
118    #[allow(missing_docs)]
119    #[sol(rpc)]
120    OdosV3Router,
121    "abis/v3.json"
122);
123
124impl Debug for OdosV3Router::swapRouterFundsReturn {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        write!(f, "amountsOut: {:?}", self.amountsOut)
127    }
128}
129
130impl Debug for inputTokenInfo {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        f.debug_struct("inputTokenInfo")
133            .field("tokenAddress", &self.tokenAddress)
134            .field("amountIn", &self.amountIn)
135            .field("receiver", &self.receiver)
136            .finish()
137    }
138}
139
140impl Debug for outputTokenInfo {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        f.debug_struct("outputTokenInfo")
143            .field("tokenAddress", &self.tokenAddress)
144            .field("amountQuote", &self.amountQuote)
145            .field("amountMin", &self.amountMin)
146            .field("receiver", &self.receiver)
147            .finish()
148    }
149}
150
151impl Debug for swapCall {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        f.debug_struct("swapCall")
154            .field("executor", &self.executor)
155            .field("pathDefinition", &self.pathDefinition)
156            .field("referralInfo", &self.referralInfo)
157            .field("tokenInfo", &self.tokenInfo)
158            .finish()
159    }
160}
161
162impl Debug for swapReferralInfo {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        f.debug_struct("swapReferralInfo")
165            .field("code", &self.code)
166            .field("fee", &self.fee)
167            .field("feeRecipient", &self.feeRecipient)
168            .finish()
169    }
170}
171
172impl Debug for SwapMulti {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        f.debug_struct("SwapMulti")
175            .field("sender", &self.sender)
176            .field("amountsIn", &self.amountsIn)
177            .field("tokensIn", &self.tokensIn)
178            .field("amountsOut", &self.amountsOut)
179            .field("tokensOut", &self.tokensOut)
180            .finish()
181    }
182}
183
184impl Debug for Swap {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        f.debug_struct("Swap")
187            .field("sender", &self.sender)
188            .field("inputAmount", &self.inputAmount)
189            .field("inputToken", &self.inputToken)
190            .field("amountOut", &self.amountOut)
191            .field("outputToken", &self.outputToken)
192            .field("slippage", &self.slippage)
193            .field("referralCode", &self.referralCode)
194            .finish()
195    }
196}
197
198impl Debug for swapTokenInfo {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        f.debug_struct("swapTokenInfo")
201            .field("inputToken", &self.inputToken)
202            .field("inputAmount", &self.inputAmount)
203            .field("inputReceiver", &self.inputReceiver)
204            .field("outputToken", &self.outputToken)
205            .field("outputQuote", &self.outputQuote)
206            .field("outputMin", &self.outputMin)
207            .field("outputReceiver", &self.outputReceiver)
208            .finish()
209    }
210}
211
212impl TryFrom<&Bytes> for OdosV3RouterCalls {
213    type Error = alloy_sol_types::Error;
214
215    fn try_from(input: &Bytes) -> Result<Self, Self::Error> {
216        OdosV3RouterCalls::abi_decode(input)
217    }
218}