rig_onchain_kit/cross_chain/
tools.rs

1use anyhow::{anyhow, Result};
2use blockhash_cache::{inject_blockhash_into_encoded_tx, BLOCKHASH_CACHE};
3use rig_tool_macro::tool;
4
5use crate::common::wrap_unsafe;
6use crate::signer::SignerContext;
7
8use lifi::LiFi;
9
10// TODO support sponsored transactions here
11// it would save a lot of gas if we could drip on any chain,
12// fees are substantially higher if the user has an empty wallet on the dest chain
13
14#[tool(description = "
15Get a quote for a multichain swap (or bridge).
16
17This might be required in case the user wonders how much it would cost to
18perform a swap or bridge. It is also good in case you would like to validate the
19token addresses and other params with the user before executing
20
21The from_token_address and to_token_address can either be a solana public key, evm
22address or a symbol, try to prioritize the address over the symbol
23
24The amount has to be a string to avoid precision loss. The amount is accounting
25for decimals, e.g. 1e6 for 1 USDC but 1e18 for 1 SOL.
26
27Note that sometimes the quote will return a transaction request, with an address that might require approval.
28In that case, you can use the approve_token tool to approve the token.
29
30Supported from_chains:
31- solana: 1151111081099710
32- arbitrum: 42161
33- base: 8453
34
35Supported to_chains:
36- sol: 1151111081099710
37- arb: 42161
38- base: 8453
39
40if a user hits you with a chain you cannot support, let them know
41")]
42pub async fn get_quote(
43    from_token_address: String,
44    to_token_address: String,
45    amount: String,
46    from_chain: String,
47    to_chain: String,
48) -> Result<serde_json::Value> {
49    let signer = SignerContext::current().await;
50    let lifi = LiFi::new(None);
51
52    let from_address = if from_chain == "1151111081099710"
53        || from_chain.to_lowercase() == "sol"
54    {
55        signer.pubkey()
56    } else {
57        signer.address()
58    };
59
60    let to_address = if to_chain == "1151111081099710"
61        || to_chain.to_lowercase() == "sol"
62    {
63        signer.pubkey()
64    } else {
65        signer.address()
66    };
67
68    let quote = lifi
69        .get_quote(
70            &from_chain,
71            &to_chain,
72            &from_token_address,
73            &to_token_address,
74            &from_address,
75            &to_address,
76            &amount,
77        )
78        .await
79        .map_err(|e| {
80            anyhow!(
81                "{:#?}",
82                e.to_string().chars().take(300).collect::<String>()
83            )
84        })?;
85
86    Ok(quote.summary())
87}
88
89#[tool(description = "
90Multichain swap (or bridge).
91
92This can be used for any swap, solana to solana, evm to evm, solana to evm,
93evm to solana, etc.
94
95Use this in case of the user trying to swap any tokens that exist on two remote
96chains, or would like to bridge the tokens
97
98Don't use this in case you are not certain about all of the params, use the
99get_multichain_quote tool instead to validate the params in that case.
100
101The from_token_address and to_token_address can either be a solana public key, evm
102address or a symbol, try to prioritize the address over the symbol
103
104The amount has to be a string to avoid precision loss. The amount is accounting
105for decimals, e.g. 1e6 for 1 USDC but 1e18 for 1 SOL.
106
107Supported from_chains:
108- solana: 1151111081099710
109- arbitrum: 42161
110- base: 8453
111
112Supported to_chains:
113- sol: 1151111081099710
114- arb: 42161
115- base: 8453
116
117if a user hits you with a chain you cannot support, let them know
118")]
119pub async fn swap(
120    from_token_address: String,
121    to_token_address: String,
122    amount: String,
123    from_chain: String,
124    to_chain: String,
125) -> Result<String> {
126    let signer = SignerContext::current().await;
127    let lifi = LiFi::new(None);
128
129    let from_address = if from_chain == "1151111081099710"
130        || from_chain.to_lowercase() == "sol"
131    {
132        signer.pubkey()
133    } else {
134        signer.address()
135    };
136
137    let to_address = if to_chain == "1151111081099710"
138        || to_chain.to_lowercase() == "sol"
139    {
140        signer.pubkey()
141    } else {
142        signer.address()
143    };
144
145    let quote = lifi
146        .get_quote(
147            &from_chain,
148            &to_chain,
149            &from_token_address,
150            &to_token_address,
151            &from_address,
152            &to_address,
153            &amount,
154        )
155        .await
156        .map_err(|e| {
157            anyhow!(
158                "{:#?}",
159                e.to_string().chars().take(300).collect::<String>()
160            )
161        })?;
162
163    match quote.transaction_request {
164        Some(transaction_request) => {
165            wrap_unsafe(move || async move {
166                if transaction_request.is_solana() {
167                    let latest_blockhash =
168                        BLOCKHASH_CACHE.get_blockhash().await?.to_string();
169                    let encoded_tx = inject_blockhash_into_encoded_tx(
170                        &transaction_request.data,
171                        &latest_blockhash,
172                    )?;
173                    signer
174                        .sign_and_send_encoded_solana_transaction(encoded_tx)
175                        .await
176                } else {
177                    signer
178                        .sign_and_send_json_evm_transaction(
179                            transaction_request.to_json_rpc()?,
180                        )
181                        .await
182                }
183            })
184            .await
185        }
186        None => Err(anyhow!("No transaction request")),
187    }
188}
189
190#[tool(description = "
191Check if a token has enough approval for a spender.
192
193token_address is the ERC20 token contract address
194spender_address is the address that needs approval
195amount is the amount to check approval for (in token decimals)
196
197Returns 'true' if approved, 'false' if not approved
198")]
199pub async fn check_approval(
200    token_address: String,
201    spender_address: String,
202    amount: String,
203    from_chain_caip2: String,
204) -> Result<String> {
205    let signer = SignerContext::current().await;
206    let owner_address = signer.address();
207
208    let allowance = evm_approvals::get_allowance(
209        &token_address,
210        &owner_address,
211        &spender_address,
212        evm_approvals::caip2_to_chain_id(&from_chain_caip2)?,
213    )
214    .await?;
215    let amount = amount
216        .parse::<u128>()
217        .map_err(|_| anyhow!("Invalid amount"))?;
218
219    Ok((allowance >= amount).to_string())
220}
221
222#[tool(description = "
223Approve a token for a spender.
224
225token_address is the ERC20 token contract address
226spender_address is the address that needs approval
227amount is the amount to approve (in token decimals)
228")]
229pub async fn approve_token(
230    token_address: String,
231    spender_address: String,
232    from_chain_caip2: String,
233) -> Result<String> {
234    let signer = SignerContext::current().await;
235    let owner_address = signer.address();
236
237    let transaction = evm_approvals::create_approval_transaction(
238        &token_address,
239        &spender_address,
240        &owner_address,
241        evm_approvals::caip2_to_chain_id(&from_chain_caip2)?,
242    )
243    .await?;
244
245    wrap_unsafe(move || async move {
246        signer
247            .sign_and_send_json_evm_transaction(transaction)
248            .await
249            .map_err(|e| anyhow!(e.to_string()))
250    })
251    .await?;
252
253    Ok("Approved".to_string())
254}