├── README.md └── example_solver ├── .env.example ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src ├── chains ├── ethereum.rs ├── mantis.rs ├── mod.rs └── solana.rs ├── main.rs └── routers ├── jupiter ├── field_as_string.rs ├── field_instruction.rs ├── field_prioritization_fee.rs ├── field_pubkey.rs └── mod.rs ├── mod.rs └── paraswap.rs /README.md: -------------------------------------------------------------------------------- 1 | # 🎉 MANTIS V0: Decentralized Cross-Chain Intents 🎉 2 | 3 | [![License](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) 4 | [![Twitter Follow](https://img.shields.io/twitter/follow/mantis?style=social)](https://x.com/mantis) 5 | [![Website](https://img.shields.io/badge/website-ComposableFoundation-blue)](https://www.composablefoundation.com/) 6 | 7 | MANTIS V0 is a cutting-edge system designed to enable seamless, decentralized interactions across multiple blockchains. It relies on **four key components** to ensure that transactions are executed efficiently, securely, and without the need for a trusted third party. Let's explore these components: 8 | 9 | --- 10 | 11 | ## 1. 🎯 The Auctioneer 12 | The **Auctioneer** is an essential off-chain entity that orchestrates the entire transaction process. It acts as a bridge between users, solvers, and the blockchain networks. The Auctioneer’s primary roles include: 13 | 14 | - **Listening for Intents:** Users submit transaction intents directly on-chain, and the Auctioneer listens for these on-chain events. 15 | - **Broadcasting to Solvers:** The Auctioneer broadcasts these intents to solvers, who compete to execute the transactions. 16 | - **Determining the Winner:** After solvers submit their bids, the Auctioneer selects the best bid based on criteria like speed, cost, and reliability. 17 | - **Updating the Intent:** The Auctioneer updates the on-chain intent with the winning solver and the amount output from the solver. 18 | 19 | --- 20 | 21 | ## 2. 🛠️ The Solvers 22 | **Solvers** are entities capable of executing the transactions described in the intents. They listen for intents emitted as on-chain events and decide whether to participate in the auction. The solvers’ responsibilities include: 23 | 24 | - **Bidding:** Solvers analyze the intents and submit bids to execute the transaction. 25 | - **Executing Transactions:** The winning solver executes the transaction on the destination chain, ensuring the intent is fulfilled as specified. 26 | 27 | --- 28 | 29 | ## 3. 🔐 Smart Contracts on Each Chain 30 | Smart contracts deployed on each blockchain play a pivotal role in the system. These contracts are responsible for: 31 | 32 | - **Escrow Management:** Handling the secure transfer of funds between chains. 33 | - **Execution Logic:** Enforcing the rules that govern how transactions are processed and validated on each chain. 34 | 35 | These smart contracts ensure that transactions are executed in a trustless and secure manner, with no need for intermediaries. 36 | 37 | --- 38 | 39 | ## 4. 🌐 The Rollup: Where MANTIS Runs 40 | The **Rollup** is the backbone of the MANTIS V0 system, providing a scalable and secure environment for processing transactions. It serves several critical functions: 41 | 42 | - **Aggregation:** Collecting and storing multiple transactions in a compressed format. 43 | - **Decentralization:** Maintaining the logic that governs the Auctioneer’s operations, ensuring the entire process remains decentralized. 44 | - **Security:** Ensuring that all actions are transparent and can be independently verified by participants. 45 | 46 | The Rollup enables MANTIS to operate efficiently while preserving the principles of decentralization and trustlessness. 47 | 48 | --- 49 | 50 | # Cross-Chain Domain vs. Single Domain Options 51 | 52 | MANTIS V0 empowers users with two flexible transaction options: **Cross-Chain Domain** and **Single Domain**. Both are designed to ensure secure, efficient, and decentralized operations, but each offers unique capabilities. 53 | 54 | --- 55 | 56 | ## 🌉 Cross-Chain Domain: Connecting the Blockchains 57 | 58 | The **Cross-Chain Domain** lets you traverse different blockchains effortlessly. Currently, we support: 59 | 60 | - 🟣 **Ethereum** 61 | - 🟠 **Solana** 62 | 63 | *(More blockchains are on the horizon!)* 64 | 65 | ### 🔄 How It Works: 66 | In this domain, you can submit intents that involve transactions across chains. Picture this, for example: 67 | 68 | - **🟣 Start on Ethereum:** Swap a token on Ethereum (your source chain). 69 | - **🟠 End on Solana:** Receive the token on Solana (your destination chain). 70 | 71 | ### 🚀 The Role of Solvers: 72 | Solvers are the unsung heroes making these cross-chain journeys possible. They: 73 | 74 | - 🛠️ **Bridge the Gap:** By holding **USDT**, solvers enable swift and secure cross-chain swaps. 75 | - ⏩ **Ensure Speed:** Solvers are positioned in the middle, ensuring that cross-chain intents are completed quickly. 76 | 77 | This option is perfect for users looking to move assets between blockchains seamlessly. 78 | 79 | --- 80 | 81 | ## 🔗 Single Domain: Mastering a Single Chain 82 | 83 | For those who prefer to stay within one blockchain, the **Single Domain** is your go-to. It supports: 84 | 85 | - 🟣 **Ethereum** 86 | - 🟠 **Solana** 87 | 88 | *(And yes, more chains will be available soon!)* 89 | 90 | ### 📈 How It Works: 91 | In the Single Domain, users submit intents and solvers execute them entirely within the same blockchain. Whether you're trading or performing other operations, it all happens within a single chain’s ecosystem. 92 | 93 | ### 🛡️ Security & Efficiency: 94 | Both Single Domain and Cross-Chain Domain options are designed with the highest standards of security and efficiency, ensuring peace of mind for both users and solvers. 95 | 96 | --- 97 | 98 | # 🔄 Interaction Flow 99 | 100 | 1. **👥 User Submits Intents** 101 | - The user submits their intent on-chain, specifying the details of a transaction, including the source chain, destination chain, and other relevant parameters. This submission is the initial step in the process. 102 | 103 | 2. **📣 Auctioneer and Solvers Listen for Intents** 104 | - The Auctioneer and solvers listen for on-chain events emitted after the user submits their intent. Solvers, who are capable of executing the transactions, receive the intent and decide whether to participate in the auction. 105 | 106 | 3. **🤔 Solvers Decide to Participate** 107 | - Solvers receive the intent and determine if they can provide a competitive bid to execute the transaction. 108 | 109 | 4. **🏆 Auctioneer Determines Winning Solver** 110 | - After receiving bids from participating solvers, the Auctioneer selects the winning solver based on criteria such as speed, cost, and reliability. 111 | - The Auctioneer then updates the on-chain intent with the winning solver and the `amount_out`. 112 | 113 | 5. **⚙️ Solver Executes Transaction on Destination Chain** 114 | - The winning solver submits a transaction on the destination chain through the escrow contract to transfer funds to the user. If the source chain and destination chain are different, the solver also sends a cross-chain message as part of the same transaction. This ensures that the transaction is recognized and processed correctly across both chains. 115 | 116 | 6. **📦 Transaction Storage in Rollup** 117 | - Once the transaction is executed, whether it is a cross-chain transaction or a single-domain transaction, it is stored in the rollup. The rollup is a layer that aggregates multiple transactions and stores them securely. It also maintains the logic of how the Auctioneer operates, ensuring that the entire process remains decentralized and trustless. 118 | 119 | 7. **🔐 Decentralization and Trustlessness** 120 | - The rollup is responsible for storing information and executing the logic that governs the Auctioneer's operations. This setup ensures that the system remains decentralized and trustless, meaning that no single entity has control over the process, and all actions can be verified independently by participants in the network. 121 | 122 | --- 123 | 124 | ## 🪙 User - Solver - Token Workflow 125 | 126 | ### 🚀 Single Domain Workflow: 127 | 1. **User Actions**: 128 | - The user escrows `token_in` on the Escrow Contract (`Escrow SC`) within the same domain. 129 | 130 | 2. **Solver Actions**: 131 | - The solver calls `send_funds_to_user()`, which triggers the following actions: 132 | - Sends `token_out` to the user. 133 | - Receives the `token_in` from the escrow in the **SAME** transaction. 134 | 135 | 3. **Smart Contract Role**: 136 | - The `Escrow SC` ensures that everything is decentralized 🕸️ and operates according to the intent information submitted by the user, guaranteeing that both the solver and the user experience the same level of fairness ⚖️. 137 | 138 | ### 🌐 Cross-Domain Workflow: 139 | 1. **User Actions**: 140 | - The user escrows `token_in` on the **source chain** via the Escrow Contract (`Escrow SC`). 141 | 142 | 2. **Solver Actions**: 143 | - The solver calls `send_funds_to_user()` on the **destination chain**. This function does the following: 144 | - Sends `token_out` to the user. 145 | - Sends a cross-chain message 📨 within the **SAME** function, according to the intent info instructions, to ensure fairness for both the solver and the user. 146 | 147 | 3. **Cross-Chain Message**: 148 | - The message contains the necessary information to release the `token_in` on the source chain for the solver, ensuring that everything is handled fairly across domains 🔄. 149 | 150 | --- 151 | 152 | ## 🔑 Message Signing Process 153 | 154 | 1. **Keccak Hashing:** 155 | - The first step is to generate a unique hash of the message. This is done using the Keccak-256 algorithm, which produces a fixed-size 256-bit hash. 156 | 157 | 2. **Signing the Message:** 158 | - The solver then signs this hashed message using their Ethereum private key. This signature is a cryptographic proof that the message was indeed created by the owner of the private key. 159 | 160 | 3. **Verification by Auctioneer:** 161 | - When the auctioneer receives the signed message, it verifies the signature. This is done by comparing the Ethereum address that corresponds to the private key (from which the signature was derived) with the address provided in the `SOLVER_ADDRESSES`. 162 | - If the addresses match, the auctioneer confirms that the message is authentic and that it was sent by the correct solver. 163 | 164 | --- 165 | 166 | # Solver Setup Instructions 167 | ## ⚠️ Important Warnings for Ethereum Solvers 168 | - **⚠️ WARNING:** Modify `send_tx()` on Ethereum for customized gas priority. Make sure you adjust the gas settings accordingly to avoid transaction failures. 169 | - **⚠️ WARNING:** Always use a reliable RPC. Avoid using any unreliable private pools to ensure smooth operations. 170 | - **⚠️ WARNING:** If the Ethereum swap size is **less** than `ETH FLAT_FEE + COMMISSION` or the Solana swap size is **less** than `SOL FLAT_FEE + COMMISSION`, the solver **will not** participate in the auction. 171 | - **⚠️ WARNING:** Solvers need to **approve** USDT to Paraswap on Ethereum using the contract address `0x216b4b4ba9f3e719726886d34a177484278bfcae` **only once**. 172 | - **⚠️ WARNING:** Solvers need to **approve** USDT to Escrow on Ethereum using the contract address `0x64E78873057769a5fd9A2278E6820666ec7e87f9` **only once**. 173 | - **⚠️ WARNING:** Optimize `FLAT_FEES` based on gas consumption and **optimize token approvals** to reduce unnecessary costs. 174 | - **⚠️ WARNING:** The solver's address **must be the same** as the address used to send ETH to the Auctioner. 175 | 176 | ## 🔧 Important Configuration: `SOLVER_ADDRESSES` in `chains/mod.rs` 177 | When setting up as a solver within the MANTIS V0 system, one crucial variable you need to pay attention to is `SOLVER_ADDRESSES` located in the `chains/mod.rs` file. This variable is vital for ensuring that your solver is correctly recognized on the blockchain networks where you are solving intents. 178 | The `SOLVER_ADDRESSES` variable is a static array that holds the addresses your solver uses on the respective blockchains. Each entry in this array corresponds to the specific chain where you will be solving intents. 179 | Here’s how it looks in the code: 180 | ```rust 181 | pub static SOLVER_ADDRESSES: &[&str] = &[ 182 | "0x...", // ethereum, MUST be the pubkey of ETHEREUM_PKEY on .env! 183 | "CM...", // solana 184 | ]; 185 | ``` 186 | ## Step 1: Fill the .env File 187 | The first thing you need to do is fill out the `.env` file. Use the provided `env.example` as a template: 188 | ```bash 189 | ETHEREUM_RPC="" # https 190 | ETHEREUM_PKEY="" # we use this pkey to be the SOLVER_PRIVATE_KEY, MUST be the private key of ethereum SOLVER_ADDRESSES 191 | SOLANA_RPC="" # https 192 | SOLANA_KEYPAIR="" 193 | BRIDGE_TOKEN="USDT" # USDT 194 | COMISSION="10" # if COMISSION == "1"-> 0.01% 195 | SOLVER_ID="" # Given by Composable 196 | COMPOSABLE_ENDPOINT="" # ws IP address Given by Composable 197 | JITO="" # true or false 198 | ``` 199 | ## Step 2: Run the Solver 200 | To run the solver, use the following command: 201 | ```sh 202 | cargo run --release 203 | ``` 204 | this is the kind of messages you want to see if you made things right: 205 | ```rust 206 | Object { 207 | "code": Number(3), 208 | "msg": String("Solver was succesfully registered"), 209 | } 210 | Object { 211 | "code": Number(1), 212 | "msg": Object { 213 | "intent": "...", // intent_info 214 | "intent_id": String("RVcwGSrL"), 215 | }, 216 | } 217 | User wants 20000000, you can provide 95137240 218 | Object { 219 | "code": Number(4), 220 | "msg": Object { 221 | "amount": Number(95137240), 222 | "intent_id": String("RVcwGSrL"), 223 | "msg": String("You won this auction!"), 224 | }, 225 | } 226 | You have win 29.196523 USDT on intent RVcwGSrL 227 | ``` 228 | Inside the `example_solver`, we have two main folders: `routers` and `chains`. 229 | ### Routers 230 | In the `routers` folder, we have Jupiter on Solana and Paraswap on Ethereum mainnet. Feel free to add more routers or your own router system. The `routers` folder doesn't need modifications unless you want to add new routers or your own router. 231 | ### Chains 232 | In the `chains` folder, we have two chains: Ethereum and Solana. The structure is the same for each chain. The important functions are: 233 | - `chain_simulate_swap()` 234 | - `chain_executing()` 235 | #### `chain_simulate_swap()` 236 | This function is used to participate in the auction. Inside this function, you will find the logic to simulate swaps on Jupiter for Solana and Paraswap for Ethereum. Feel free to change this if you want to add more routers. 237 | #### `chain_executing()` 238 | This function is used when the solver wins the auction and is solving the intent. Inside this function, you will find the process to make a swap on Paraswap or Jupiter. Feel free to change this as well. 239 | 240 | ## WS: 241 | **Composable Endpoint:** 242 | `ws://34.78.217.187:8900` 🏗️ upgrading to V1 243 | ### Register Solver Addresses (one address per chain) 244 | ```rust 245 | { 246 | "code": 1, 247 | "msg": { 248 | "solver_id": SOLVER_ID, // Given by Composable 249 | "solver_addresses": SOLVER_ADDRESSES, // vec!(solana address, ethereum address, ...) 250 | "intent_hash": "...", // Keccak256Hash of the intent 251 | "signature": "..." // ECDSA signature of the hash 252 | } 253 | } 254 | ``` 255 | ### Response: 256 | **OK:** 257 | ```rust 258 | { 259 | "code": 3, 260 | "msg": "Solver was successfully registered" 261 | } 262 | ``` 263 | **ERROR:** 264 | ```rust 265 | { 266 | "code": 0, 267 | "msg": msg_error 268 | } 269 | ``` 270 | ### Participate in an Intent Auction: 271 | ```rust 272 | { 273 | "code": 2, 274 | "msg": { 275 | "intent_id": intent_id, // obtained listening to Intents 276 | "solver_id": SOLVER_ID, // Given by Composable 277 | "amount": "...", // off-chain solver setup to get the best quote 278 | "intent_hash": "...", // Keccak256Hash of the intent 279 | "signature": "..." // ECDSA signature of the hash 280 | } 281 | } 282 | ``` 283 | ### Response: 284 | **OK:** 285 | ```rust 286 | { 287 | "code": 4, 288 | "msg": msg // "You won this auction!" 289 | // OR "You lost this auction" 290 | } 291 | ``` 292 | **ERROR:** 293 | ```rust 294 | { 295 | "code": 0, 296 | "msg": msg_error 297 | } 298 | ``` 299 | -------------------------------------------------------------------------------- /example_solver/.env.example: -------------------------------------------------------------------------------- 1 | ETHEREUM_RPC="" # https 2 | ETHEREUM_PKEY="" # we use this pkey to be the SOLVER_PRIVATE_KEY, MUST be the private key of ethereum SOLVER_ADDRESSES 3 | SOLANA_RPC="" # https 4 | MANTIS_RPC="" # https 5 | SOLANA_KEYPAIR="" 6 | BRIDGE_TOKEN="USDT" # USDT 7 | COMISSION="100" # if COMISSION == "1"-> 0.01% 8 | SOLVER_ID="" # Given by Composable 9 | COMPOSABLE_ENDPOINT="" # ws IP address Given by Composable 10 | JITO="" # true or false 11 | -------------------------------------------------------------------------------- /example_solver/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /example_solver/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example_solver" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.71.0" 6 | 7 | [dependencies] 8 | tokio = { version = "1", features = ["full"] } 9 | tokio-tungstenite = "0.23.1" 10 | futures = "0.3" 11 | serde = { version = "1.0", features = ["derive"] } 12 | serde_json = "1.0.117" 13 | reqwest = { version = "0.11", features = ["json"] } 14 | lazy_static = "1.4.0" 15 | ethers = { version = "2.0.14", default-features = true, features = ["ws","abigen"] } 16 | hex = "0.4.3" 17 | spl-associated-token-account = { version = "3.0.2", default-features = false, features = ["no-entrypoint"] } 18 | spl-token = { version = "3.2.0", default-features = false, features = ["no-entrypoint"] } 19 | spl-token-2022 = { version = "0.9.0", features = ["no-entrypoint"] } 20 | solana-client = "1.8.3" 21 | anchor-client = { version = "0.29.0" } 22 | solana-sdk = "1.17.30" 23 | anchor-spl = { version = "0.29.0" } 24 | anchor-lang = { version = "0.29.0" } 25 | anyhow = "1.0.32" 26 | dotenv = "0.15.0" 27 | base64 = { version = "0.22.1", default-features = false, features = ["alloc"] } 28 | thiserror = "1.0.61" 29 | bincode = "1" 30 | num-bigint = "0.4.5" 31 | num-traits = "0.2.19" 32 | strum = { version = "0.26.2", features = ["derive"] } 33 | strum_macros = "0.26.4" 34 | secp256k1 = "0.27.0" 35 | web3 = "0.19.0" 36 | solana-program = "1.17.30" 37 | jito-searcher-client = { git = "https://github.com/dhruvja/searcher-examples" } 38 | jito-protos = { git = "https://github.com/dhruvja/searcher-examples" } 39 | bridge-escrow = { git = "https://github.com/ComposableFi/emulated-light-client.git", branch = "upgrade", package = "bridge-escrow" } 40 | solana-ibc = { git = "https://github.com/ComposableFi/emulated-light-client.git", branch = "fast-bridge", features = ["cpi"] } 41 | lib = { git = "https://github.com/ComposableFi/emulated-light-client.git", branch = "fast-bridge", features = ["solana-program"] } 42 | 43 | [patch.crates-io] 44 | # aes-gcm-siv 0.10.3 and curve25519-dalek 3.x pin zeroize to <1.4 45 | # which conflicts with other dependencies requiring zeroize ^1.5. 46 | # We’re patching both crates to unpin zeroize. 47 | # 48 | # For aes-gcm-siv we’re using the same revision Solana uses in 49 | # an (as of now) unreleased commit, see 50 | # https://github.com/solana-labs/solana/commit/01f1bf27994d9813fadfcd134befd3a449aaa0bd 51 | # 52 | # For curve25519-dalek we’re using commit from a PR, see 53 | # https://github.com/dalek-cryptography/curve25519-dalek/pull/606 54 | aes-gcm-siv = { git = "https://github.com/RustCrypto/AEADs", rev = "6105d7a5591aefa646a95d12b5e8d3f55a9214ef" } 55 | curve25519-dalek = { git = "https://github.com/dalek-cryptography/curve25519-dalek", rev = "8274d5cbb6fc3f38cdc742b4798173895cd2a290" } 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /example_solver/src/chains/ethereum.rs: -------------------------------------------------------------------------------- 1 | pub mod ethereum_chain { 2 | use crate::chains::get_token_info; 3 | use crate::chains::OperationOutput; 4 | use crate::env; 5 | use crate::json; 6 | use crate::routers::paraswap::paraswap_router::simulate_swap_paraswap; 7 | use crate::routers::paraswap::paraswap_router::ParaswapParams; 8 | use crate::OperationInput; 9 | use crate::PostIntentInfo; 10 | use crate::SOLVER_ADDRESSES; 11 | use crate::SOLVER_ID; 12 | use ethers::prelude::abigen; 13 | use ethers::prelude::*; 14 | use ethers::providers::{Http, Provider}; 15 | use num_bigint::BigInt; 16 | use reqwest::Client; 17 | use serde::Deserialize; 18 | use solana_sdk::pubkey::Pubkey; 19 | use spl_associated_token_account::get_associated_token_address; 20 | use std::str::FromStr; 21 | use std::sync::Arc; 22 | use std::thread::sleep; 23 | use std::time::Duration; 24 | 25 | #[derive(Deserialize)] 26 | struct GasPrice { 27 | #[serde(rename = "SafeGasPrice")] 28 | _safe_gas_price: String, 29 | #[serde(rename = "ProposeGasPrice")] 30 | propose_gas_price: String, 31 | #[serde(rename = "FastGasPrice")] 32 | _fast_gas_price: String, 33 | } 34 | 35 | #[derive(Deserialize)] 36 | struct GasResponse { 37 | result: GasPrice, 38 | } 39 | 40 | abigen!( 41 | ERC20, 42 | r#"[{ 43 | "constant": true, 44 | "inputs": [], 45 | "name": "decimals", 46 | "outputs": [{ "name": "", "type": "uint8" }], 47 | "type": "function" 48 | }, 49 | { 50 | "constant": false, 51 | "inputs": [ 52 | { "name": "_to", "type": "address" }, 53 | { "name": "_value", "type": "uint256" } 54 | ], 55 | "name": "transfer", 56 | "outputs": [{ "name": "", "type": "bool" }], 57 | "type": "function" 58 | }, 59 | { 60 | "constant": false, 61 | "inputs": [ 62 | { "name": "_spender", "type": "address" }, 63 | { "name": "_value", "type": "uint256" } 64 | ], 65 | "name": "approve", 66 | "outputs": [{ "name": "", "type": "bool" }], 67 | "type": "function" 68 | },{ 69 | "constant":true, 70 | "inputs":[ 71 | {"name":"account","type":"address"} 72 | ], 73 | "name":"balanceOf", 74 | "outputs":[{"name":"","type":"uint256"}], 75 | "type":"function"} 76 | ]"# 77 | ); 78 | 79 | abigen!( 80 | Escrow, 81 | r#"[{ 82 | "constant": false, 83 | "inputs": [ 84 | { 85 | "components": [ 86 | { "name": "intentId", "type": "string" }, 87 | { "name": "tokenOut", "type": "address" }, 88 | { "name": "amountOut", "type": "uint256" }, 89 | { "name": "dstUser", "type": "address" }, 90 | { "name": "singleDomain", "type": "bool" }, 91 | { "name": "accounts", "type": "string[]"} 92 | ], 93 | "name": "solverTransferData", 94 | "type": "tuple" 95 | } 96 | ], 97 | "name": "sendFundsToUser", 98 | "outputs": [], 99 | "payable": true, 100 | "stateMutability": "payable", 101 | "type": "function" 102 | }]"# 103 | ); 104 | 105 | abigen!( 106 | UsdtContract, 107 | r#"[ 108 | function balanceOf(address owner) view returns (uint256) 109 | ]"# 110 | ); 111 | 112 | pub const ESCROW_SC_ETHEREUM: &str = "0x64E78873057769a5fd9A2278E6820666ec7e87f9"; 113 | pub const PARASWAP: &str = "0x216b4b4ba9f3e719726886d34a177484278bfcae"; 114 | 115 | pub async fn handle_ethereum_execution( 116 | intent: &PostIntentInfo, 117 | intent_id: &str, 118 | amount: &str, 119 | ) -> Result<(), String> { 120 | let usdt_contract_address = "0xdac17f958d2ee523a2206206994597c13d831ec7"; 121 | 122 | let rpc_url = env::var("ETHEREUM_RPC").expect("ETHEREUM_RPC must be set"); 123 | let private_key = env::var("ETHEREUM_PKEY").expect("ETHEREUM_PKEY must be set"); 124 | let target_address: Address = Address::from_str(SOLVER_ADDRESSES.get(0).unwrap()).unwrap(); 125 | 126 | let provider = Arc::new( 127 | Provider::::try_from(&rpc_url) 128 | .map_err(|e| format!("Failed to create Ethereum provider: {}", e))?, 129 | ); 130 | 131 | let usdt_contract = UsdtContract::new( 132 | Address::from_str(usdt_contract_address).unwrap(), 133 | provider.clone(), 134 | ); 135 | 136 | let balance_ant = usdt_contract 137 | .balance_of(target_address) 138 | .call() 139 | .await 140 | .map_err(|e| format!("Failed to get USDT balance: {}", e))?; 141 | 142 | let mut token_in = String::default(); 143 | let mut token_out = String::default(); 144 | let mut amount_in = String::default(); 145 | let mut src_user = String::default(); 146 | let mut dst_user = String::default(); 147 | 148 | if let OperationOutput::SwapTransfer(transfer_output) = &intent.outputs { 149 | token_out = transfer_output.token_out.clone(); 150 | dst_user = transfer_output.dst_chain_user.clone(); 151 | } 152 | if let OperationInput::SwapTransfer(transfer_input) = &intent.inputs { 153 | src_user = transfer_input.src_chain_user.clone(); 154 | token_in = transfer_input.token_in.clone(); 155 | amount_in = transfer_input.amount_in.clone(); 156 | } 157 | 158 | // swap USDT -> token_out 159 | let contract = ERC20::new(Address::from_str(&token_out).unwrap(), provider.clone()); 160 | use ethers::types::U256; 161 | 162 | let balance: U256 = contract 163 | .balance_of(target_address) 164 | .call() 165 | .await 166 | .unwrap_or_else(|_| U256::zero()); 167 | 168 | let mut do_swap = false; 169 | 170 | if balance < U256::from_dec_str(&amount).unwrap() 171 | && !token_out.eq_ignore_ascii_case(usdt_contract_address) 172 | { 173 | if let Err(e) = 174 | ethereum_trasnfer_swap(&intent_id.to_string(), intent.clone(), amount).await 175 | { 176 | return Err(format!( 177 | "Error occurred on Ethereum swap USDT -> token_out (solver must approve USDT to Paraswap 0x216b4b4ba9f3e719726886d34a177484278bfcae first): {}", 178 | e 179 | )); 180 | } 181 | 182 | do_swap = true; 183 | } 184 | 185 | if !token_out.eq_ignore_ascii_case(usdt_contract_address) { 186 | if let Err(e) = approve_erc20( 187 | &rpc_url, 188 | &private_key, 189 | &token_out, 190 | ESCROW_SC_ETHEREUM, 191 | amount, 192 | ) 193 | .await 194 | { 195 | println!("Error approving {token_out} for solver: {e}"); 196 | return Err(e.to_string()); 197 | } 198 | } 199 | 200 | let solver_out = if intent.src_chain == "ethereum" { 201 | SOLVER_ADDRESSES.get(0).unwrap() 202 | } else if intent.src_chain == "solana" { 203 | SOLVER_ADDRESSES.get(1).unwrap() 204 | } else { 205 | panic!("chain not supported, this should't happen"); 206 | }; 207 | 208 | // solver -> token_out -> user | user -> token_in -> solver 209 | let mut value = U256::zero(); 210 | if token_out.eq_ignore_ascii_case("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE") { 211 | value = U256::from_str(&amount).unwrap(); 212 | } 213 | 214 | if let Err(e) = ethereum_send_funds_to_user( 215 | &rpc_url, 216 | &private_key, 217 | ESCROW_SC_ETHEREUM, 218 | &intent_id.to_string(), 219 | &token_in, 220 | Address::from_str(&token_out).unwrap(), 221 | U256::from_dec_str(&amount).unwrap(), 222 | &src_user, 223 | Address::from_str(&dst_user).unwrap(), 224 | intent.src_chain == intent.dst_chain, 225 | value, 226 | &solver_out.to_string(), 227 | ) 228 | .await 229 | { 230 | println!("Error occurred on Ethereum send token_out -> user & user sends token_in -> solver (solver must approve USDT to Escrow SC first): {}", e); 231 | return Err(e.to_string()); 232 | // swap token_in -> USDT 233 | } else if do_swap 234 | && intent.src_chain == intent.dst_chain 235 | && !token_in.eq_ignore_ascii_case(usdt_contract_address) 236 | { 237 | if let Err(e) = 238 | approve_erc20(&rpc_url, &private_key, &token_in, PARASWAP, &amount_in).await 239 | { 240 | println!("Error approving {token_in} for solver: {e}"); 241 | return Err(e.to_string()); 242 | } 243 | 244 | let (token_out, token1_decimals) = match get_token_info("USDT", "ethereum") { 245 | Some((token_out, token1_decimals)) => (token_out.to_string(), token1_decimals), 246 | None => { 247 | println!("Failed to get token info for USDT on Ethereum"); 248 | return Err("Failed to get token info".to_string()); 249 | } 250 | }; 251 | 252 | let token0_decimals = if token_in == "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" { 253 | 18 254 | } else { 255 | get_evm_token_decimals(&ERC20::new( 256 | Address::from_str(&token_in).unwrap(), 257 | provider.clone(), 258 | )) 259 | .await 260 | }; 261 | 262 | let paraswap_params = ParaswapParams { 263 | side: "SELL".to_string(), 264 | chain_id: 1, 265 | amount_in: BigInt::from_str(&amount_in).unwrap(), 266 | token_in: Address::from_str(&token_in).unwrap(), 267 | token_out: Address::from_str(&token_out).unwrap(), 268 | token0_decimals: token0_decimals as u32, 269 | token1_decimals: token1_decimals as u32, 270 | wallet_address: Address::from_str(SOLVER_ADDRESSES.get(0).unwrap()).unwrap(), 271 | receiver_address: Address::from_str(SOLVER_ADDRESSES.get(0).unwrap()).unwrap(), 272 | client_aggregator: Client::new(), 273 | }; 274 | 275 | let (_res_amount, res_data, res_to) = simulate_swap_paraswap(paraswap_params) 276 | .await 277 | .map_err(|e| format!("Error simulating Paraswap swap: {}", e))?; 278 | 279 | if let Err(e) = send_tx(res_to, res_data, 1, 500_000, 0, rpc_url).await { 280 | println!("Error sending transaction on Ethereum: {}", e); 281 | return Err(e.to_string()); 282 | } 283 | } 284 | 285 | if intent.src_chain == intent.dst_chain { 286 | let balance_post = usdt_contract 287 | .balance_of(target_address) 288 | .call() 289 | .await 290 | .map_err(|e| format!("Failed to get post-swap USDT balance: {}", e))?; 291 | 292 | let balance = if balance_post >= balance_ant { 293 | balance_post - balance_ant 294 | } else { 295 | balance_ant - balance_post 296 | }; 297 | 298 | println!( 299 | "You have {} {} USDT on intent {intent_id}", 300 | if balance_post >= balance_ant { 301 | "won" 302 | } else { 303 | "lost" 304 | }, 305 | balance.as_u128() as f64 / 1e6 306 | ); 307 | } 308 | 309 | Ok(()) 310 | } 311 | 312 | pub async fn ethereum_trasnfer_swap( 313 | intent_id: &str, 314 | intent: PostIntentInfo, 315 | amount: &str, 316 | ) -> Result<(), String> { 317 | let client_rpc = 318 | env::var("ETHEREUM_RPC").map_err(|e| format!("ETHEREUM_RPC must be set: {}", e))?; 319 | let mut token_out = String::default(); 320 | 321 | match intent.function_name.as_str() { 322 | "transfer" => { 323 | if let OperationOutput::SwapTransfer(transfer_output) = &intent.outputs { 324 | token_out = transfer_output.token_out.clone(); 325 | } 326 | 327 | match transfer_erc20( 328 | &client_rpc, 329 | &env::var("ETHEREUM_PKEY") 330 | .map_err(|e| format!("ETHEREUM_PKEY must be set: {}", e))?, 331 | &token_out, 332 | SOLVER_ADDRESSES.get(0).unwrap(), 333 | &amount.to_string(), 334 | ) 335 | .await 336 | { 337 | Ok(signature) => { 338 | let msg = json!({ 339 | "code": 1, 340 | "msg": { 341 | "intent_id": intent_id, 342 | "solver_id": SOLVER_ID.to_string(), 343 | "tx_hash": signature, 344 | } 345 | }) 346 | .to_string(); 347 | println!("{}", msg); 348 | Ok(()) 349 | } 350 | Err(err) => { 351 | let msg = json!({ 352 | "code": 0, 353 | "solver_id": 0, 354 | "msg": format!("Transaction failed: {}", err) 355 | }) 356 | .to_string(); 357 | Err(msg) 358 | } 359 | } 360 | } 361 | "swap" => { 362 | let (token_in, token0_decimals) = get_token_info("USDT", "ethereum") 363 | .ok_or_else(|| "Failed to get token info".to_string())?; 364 | 365 | if let OperationOutput::SwapTransfer(transfer_output) = &intent.outputs { 366 | token_out = transfer_output.token_out.clone(); 367 | } 368 | 369 | let provider = Provider::::try_from(client_rpc.replace("wss", "https")) 370 | .map_err(|e| format!("Failed to create provider: {}", e))?; 371 | let provider = Arc::new(provider); 372 | 373 | let token1_decimals = get_evm_token_decimals(&ERC20::new( 374 | Address::from_str(&token_out) 375 | .map_err(|e| format!("Invalid token_out address: {}", e))?, 376 | provider.clone(), 377 | )) 378 | .await; 379 | 380 | let paraswap_params = ParaswapParams { 381 | side: "BUY".to_string(), 382 | chain_id: 1, 383 | amount_in: BigInt::from_str(amount) 384 | .map_err(|e| format!("Invalid amount: {}", e))?, 385 | token_in: Address::from_str(&token_in) 386 | .map_err(|e| format!("Invalid token_in address: {}", e))?, 387 | token_out: Address::from_str(&token_out) 388 | .map_err(|e| format!("Invalid token_out address: {}", e))?, 389 | token0_decimals: token0_decimals as u32, 390 | token1_decimals: token1_decimals as u32, 391 | wallet_address: Address::from_str(SOLVER_ADDRESSES.get(0).unwrap()) 392 | .map_err(|e| format!("Invalid wallet address: {}", e))?, 393 | receiver_address: Address::from_str(SOLVER_ADDRESSES.get(0).unwrap()) 394 | .map_err(|e| format!("Invalid receiver address: {}", e))?, 395 | client_aggregator: Client::new(), 396 | }; 397 | 398 | let (_res_amount, res_data, res_to) = simulate_swap_paraswap(paraswap_params) 399 | .await 400 | .map_err(|e| format!("Failed to simulate swap: {}", e))?; 401 | 402 | let tx_hash = send_tx(res_to, res_data, 1, 500_000, 0, client_rpc).await; 403 | 404 | // since tx_hash is a String, handle error separately if needed 405 | if tx_hash.is_err() { 406 | return Err(format!( 407 | "Transaction failed with tx_hash error: {:?}", 408 | tx_hash 409 | )); 410 | } 411 | 412 | Ok(()) 413 | } 414 | _ => Err("Function not supported".to_string()), 415 | } 416 | } 417 | 418 | async fn transfer_erc20( 419 | provider_url: &str, 420 | private_key: &str, 421 | token_address: &str, 422 | recipient_address: &str, 423 | amount: &str, 424 | ) -> Result> { 425 | let provider = Provider::::try_from(provider_url)?; 426 | let provider = Arc::new(provider); 427 | 428 | let wallet: LocalWallet = private_key.parse()?; 429 | let wallet = wallet.with_chain_id(1u64); // Mainnet 430 | let wallet = Arc::new(SignerMiddleware::new(provider.clone(), wallet)); 431 | 432 | let token_address = token_address.parse::
()?; 433 | let erc20 = ERC20::new(token_address, wallet.clone()); 434 | 435 | let recipient: Address = recipient_address.parse()?; 436 | let amount = U256::from_dec_str(amount).unwrap(); 437 | 438 | let tx = erc20.transfer(recipient, amount); 439 | let tx = tx.send().await?; 440 | 441 | Ok(tx.tx_hash()) 442 | } 443 | 444 | pub async fn send_tx( 445 | to: Address, 446 | data: String, 447 | chain_id: u64, 448 | gas: u64, 449 | value: u128, 450 | url: String, 451 | ) -> Result<(), String> { 452 | let prvk = match env::var("ETHEREUM_PKEY") { 453 | Ok(key) => match secp256k1::SecretKey::from_str(&key) { 454 | Ok(prvk) => prvk, 455 | Err(e) => return Err(format!("Failed to parse private key: {}", e)), 456 | }, 457 | Err(_) => return Err("ETHEREUM_PKEY environment variable is not set".to_string()), 458 | }; 459 | 460 | // Get gas 461 | let response = 462 | reqwest::get("https://api.etherscan.io/api?module=gastracker&action=gasoracle") 463 | .await 464 | .map_err(|e| format!("Failed to fetch gas price: {}", e))?; 465 | 466 | let gas_response: GasResponse = response 467 | .json::() 468 | .await 469 | .map_err(|e| format!("Failed to parse gas response: {}", e))?; 470 | 471 | // Parse the propose gas price as f64 472 | let propose_gas_price_f64: f64 = gas_response 473 | .result 474 | .propose_gas_price 475 | .parse() 476 | .map_err(|e| format!("Failed to parse gas price: {}", e))?; 477 | 478 | // Convert to wei (1 Gwei = 1e9 wei) 479 | let propose_gas_price_wei: u128 = (propose_gas_price_f64 * 1e9) as u128; 480 | 481 | let base_fee_per_gas = propose_gas_price_wei; 482 | let priority_fee_per_gas: u128 = 2_000_000_000; // This is already in wei 483 | let max_fee_per_gas = base_fee_per_gas + priority_fee_per_gas; 484 | 485 | // EIP-1559 transaction 486 | let tx_object = web3::types::TransactionParameters { 487 | to: Some(to), 488 | gas: U256::from(gas), 489 | value: U256::from(value), 490 | data: web3::types::Bytes::from( 491 | hex::decode(&data[2..]).map_err(|e| format!("Failed to decode data: {}", e))?, 492 | ), 493 | chain_id: Some(chain_id), 494 | transaction_type: Some(web3::types::U64::from(2)), 495 | access_list: None, 496 | max_fee_per_gas: Some(U256::from(max_fee_per_gas)), 497 | max_priority_fee_per_gas: Some(U256::from(priority_fee_per_gas)), 498 | ..Default::default() 499 | }; 500 | 501 | let web3_query = web3::Web3::new( 502 | web3::transports::Http::new(&url) 503 | .map_err(|e| format!("Failed to create HTTP transport: {}", e))?, 504 | ); 505 | 506 | let signed = web3_query 507 | .accounts() 508 | .sign_transaction(tx_object, &prvk) 509 | .await 510 | .map_err(|e| format!("Failed to sign transaction: {}", e))?; 511 | 512 | let tx_hash = web3_query 513 | .eth() 514 | .send_raw_transaction(signed.raw_transaction) 515 | .await 516 | .map_err(|e| format!("Failed to send transaction: {}", e))?; 517 | 518 | // println!("Transaction hash: {:?}", tx_hash); 519 | 520 | // Poll for the transaction receipt 521 | loop { 522 | match web3_query.eth().transaction_receipt(tx_hash).await { 523 | Ok(Some(receipt)) => { 524 | if receipt.status == Some(U64::from(1)) { 525 | // println!("Transaction confirmed: {:?}", receipt); 526 | return Ok(()); 527 | } else { 528 | return Err("Transaction failed".to_string()); 529 | } 530 | } 531 | Ok(None) => { 532 | // Receipt is not yet available, continue polling 533 | //println!("Transaction pending..."); 534 | sleep(Duration::from_secs(5)); 535 | } 536 | Err(e) => return Err(format!("Error while fetching transaction receipt: {}", e)), 537 | } 538 | } 539 | } 540 | 541 | pub async fn get_evm_token_decimals(erc20: &ERC20>) -> u8 { 542 | match erc20.decimals().call().await { 543 | Ok(decimals) => decimals, 544 | Err(e) => { 545 | eprintln!("Error getting decimals: {}", e); 546 | 0 547 | } 548 | } 549 | } 550 | 551 | pub async fn ethereum_simulate_swap( 552 | token_in: &str, 553 | amount_in: &str, 554 | token_out: &str, 555 | ) -> BigInt { 556 | let rpc_url = env::var("ETHEREUM_RPC").expect("ETHEREUM_RPC must be set"); 557 | let provider = Provider::::try_from(rpc_url) 558 | .map_err(|e| e.to_string()) 559 | .unwrap(); 560 | let provider = Arc::new(provider); 561 | 562 | let token_in = Address::from_str(token_in).unwrap(); 563 | let token_out = Address::from_str(token_out).unwrap(); 564 | let token0_decimals = get_evm_token_decimals(&ERC20::new(token_in, provider.clone())).await; 565 | let token1_decimals = 566 | get_evm_token_decimals(&ERC20::new(token_out, provider.clone())).await; 567 | 568 | let paraswap_params = ParaswapParams { 569 | side: "SELL".to_string(), 570 | chain_id: 1, 571 | amount_in: BigInt::from_str(amount_in).unwrap(), 572 | token_in: token_in, 573 | token_out: token_out, 574 | token0_decimals: token0_decimals as u32, 575 | token1_decimals: token1_decimals as u32, 576 | wallet_address: Address::from_str(SOLVER_ADDRESSES.get(0).unwrap()).unwrap(), 577 | receiver_address: Address::from_str(SOLVER_ADDRESSES.get(0).unwrap()).unwrap(), 578 | client_aggregator: Client::new(), 579 | }; 580 | 581 | let (_res_amount, _, _) = simulate_swap_paraswap(paraswap_params).await.unwrap(); 582 | _res_amount 583 | } 584 | 585 | pub async fn ethereum_send_funds_to_user( 586 | provider_url: &str, 587 | private_key: &str, 588 | contract_address: &str, 589 | intent_id: &String, 590 | token_in: &String, 591 | token_out: Address, 592 | amount_out: U256, 593 | src_user: &String, 594 | dst_user: Address, 595 | single_domain: bool, 596 | value_in_wei: U256, 597 | solver_out: &String, 598 | ) -> Result> { 599 | let provider = Provider::::try_from(provider_url)?; 600 | let provider = Arc::new(provider); 601 | 602 | let wallet: LocalWallet = private_key.parse()?; 603 | let wallet = wallet.with_chain_id(1u64); // Mainnet 604 | let wallet = Arc::new(SignerMiddleware::new(provider.clone(), wallet)); 605 | 606 | let contract_address = contract_address.parse::
()?; 607 | let contract = Escrow::new(contract_address, wallet.clone()); 608 | 609 | let accounts = if single_domain { 610 | vec![] 611 | } else { 612 | let auctioneer_state = Pubkey::find_program_address( 613 | &[b"auctioneer"], 614 | &Pubkey::from_str(&bridge_escrow::ID.to_string()).unwrap(), 615 | ) 616 | .0; 617 | let token_in = Pubkey::from_str(&token_in).unwrap(); 618 | 619 | let escrow_token_account = get_associated_token_address(&auctioneer_state, &token_in); 620 | let solver_token_account = 621 | get_associated_token_address(&Pubkey::from_str(&solver_out).unwrap(), &token_in); 622 | 623 | let intent_state = Pubkey::find_program_address( 624 | &[b"intent", intent_id.as_bytes()], 625 | &bridge_escrow::ID, 626 | ) 627 | .0; 628 | 629 | vec![ 630 | auctioneer_state.to_string(), 631 | token_in.to_string(), 632 | escrow_token_account.to_string(), 633 | solver_token_account.to_string(), 634 | solana_program::sysvar::instructions::ID.to_string(), 635 | anchor_spl::token::ID.to_string(), 636 | intent_state.to_string(), 637 | src_user.to_string(), 638 | ] 639 | }; 640 | 641 | let solver_transfer_data = ( 642 | intent_id.to_string(), 643 | token_out, 644 | amount_out, 645 | dst_user, 646 | single_domain, 647 | accounts, 648 | ); 649 | 650 | // let gas_price = provider.get_gas_price().await.unwrap(); 651 | let contract = contract 652 | .send_funds_to_user(solver_transfer_data) 653 | .value(value_in_wei); 654 | // .gas_price(gas_price); // Set the gas price 655 | 656 | let pending_tx = contract.send().await?; 657 | 658 | let tx_receipt = pending_tx 659 | .await? 660 | .expect("Failed to fetch transaction receipt"); 661 | 662 | Ok(tx_receipt) 663 | } 664 | 665 | pub async fn approve_erc20( 666 | provider_url: &str, 667 | private_key: &str, 668 | token_address: &str, 669 | spender_address: &str, 670 | amount: &str, 671 | ) -> Result<(), String> { 672 | let provider = Provider::::try_from(provider_url) 673 | .map_err(|e| format!("Failed to create provider: {}", e))?; 674 | let provider = Arc::new(provider); 675 | 676 | let wallet: LocalWallet = private_key 677 | .parse() 678 | .map_err(|e| format!("Failed to parse private key: {}", e))?; 679 | let wallet = wallet.with_chain_id(1u64); // Mainnet 680 | let wallet = Arc::new(SignerMiddleware::new(provider.clone(), wallet)); 681 | 682 | let token_address = token_address 683 | .parse::
() 684 | .map_err(|e| format!("Failed to parse token address: {}", e))?; 685 | let erc20 = ERC20::new(token_address, wallet.clone()); 686 | 687 | let spender: Address = spender_address 688 | .parse::
() 689 | .map_err(|e| format!("Failed to parse spender address: {}", e))?; 690 | let amount = 691 | U256::from_dec_str(amount).map_err(|e| format!("Failed to parse amount: {}", e))?; 692 | 693 | let tx = erc20.approve(spender, amount); 694 | let pending_tx = tx 695 | .send() 696 | .await 697 | .map_err(|e| format!("Failed to send transaction: {}", e))?; 698 | 699 | pending_tx 700 | .await 701 | .map_err(|e| format!("Transaction failed: {}", e))?; 702 | 703 | Ok(()) 704 | } 705 | } 706 | -------------------------------------------------------------------------------- /example_solver/src/chains/mantis.rs: -------------------------------------------------------------------------------- 1 | pub mod mantis_chain { 2 | use crate::chains::solana::solana_chain::solana_send_funds_to_user; 3 | use crate::chains::*; 4 | use crate::PostIntentInfo; 5 | use serde::{Deserialize, Serialize}; 6 | use solana_sdk::pubkey::Pubkey; 7 | use std::env; 8 | use std::str::FromStr; 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | struct SwapData { 12 | pub user_account: String, 13 | pub token_in: String, 14 | pub token_out: String, 15 | pub amount: u64, 16 | pub slippage_bps: u64, 17 | } 18 | 19 | #[derive(Debug, Serialize, Deserialize)] 20 | struct Memo { 21 | tx_hash: String, 22 | intent_id: String, 23 | params: Vec, 24 | } 25 | 26 | pub async fn handle_mantis_execution( 27 | intent_info: &PostIntentInfo, 28 | intent_id: &str, 29 | amount: &str, 30 | ) -> Result<(), String> { 31 | let rpc_url = env::var("MANTIS_RPC").expect("MANTIS_RPC must be set"); 32 | 33 | let mut user = String::default(); 34 | let mut token_in = String::default(); 35 | let mut token_out = String::default(); 36 | 37 | if let OperationOutput::SwapTransfer(transfer_output) = &intent_info.outputs { 38 | user = transfer_output.dst_chain_user.clone(); 39 | token_out = transfer_output.token_out.clone(); 40 | } 41 | if let OperationInput::SwapTransfer(transfer_input) = &intent_info.inputs { 42 | token_in = transfer_input.token_in.clone(); 43 | } 44 | 45 | let solver_out = if intent_info.src_chain == "ethereum" { 46 | SOLVER_ADDRESSES.get(0).unwrap() 47 | } else if intent_info.src_chain == "solana" || intent_info.src_chain == "mantis" { 48 | SOLVER_ADDRESSES.get(1).unwrap() 49 | } else { 50 | panic!("chain not supported, this should't happen"); 51 | }; 52 | 53 | // solver -> token_out -> user | user -> token_in -> solver 54 | if let Err(e) = solana_send_funds_to_user( 55 | intent_id, 56 | &token_in, 57 | &token_out, 58 | &user, 59 | solver_out.to_string(), 60 | intent_info.src_chain == intent_info.dst_chain, 61 | rpc_url, 62 | Pubkey::from_str("61beRZG1h3SvPgGYh9tXhx42jABkMjbMQWpgqUqXw2hw").unwrap(), 63 | amount.parse::().unwrap(), 64 | ) 65 | .await 66 | { 67 | return Err(format!( 68 | "Error occurred on send token_out -> user & user sends token_in -> solver: {}", 69 | e 70 | )); 71 | } else { 72 | println!("solver succesfully solve intent: {}", intent_id); 73 | } 74 | 75 | Ok(()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /example_solver/src/chains/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ethereum; 2 | pub mod mantis; 3 | pub mod solana; 4 | 5 | use crate::env; 6 | use ethers::prelude::*; 7 | use ethers::signers::LocalWallet; 8 | use ethers::utils::hash_message; 9 | use ethers::utils::keccak256; 10 | use lazy_static::lazy_static; 11 | use serde::{Deserialize, Serialize}; 12 | use serde_json::Value; 13 | use solana::solana_chain; 14 | use std::collections::HashMap; 15 | use std::error::Error; 16 | use std::str::FromStr; 17 | use std::sync::Arc; 18 | use strum_macros::EnumString; 19 | use tokio::sync::RwLock; 20 | 21 | lazy_static! { 22 | // 23 | pub static ref INTENTS: Arc>> = { 24 | let m = HashMap::new(); 25 | Arc::new(RwLock::new(m)) 26 | }; 27 | } 28 | 29 | #[derive(Debug, Serialize, Deserialize, Clone)] 30 | pub struct SwapTransferInput { 31 | pub token_in: String, 32 | pub amount_in: String, 33 | pub src_chain_user: String, 34 | pub timeout: String, 35 | } 36 | 37 | #[derive(Debug, Serialize, Deserialize, Clone)] 38 | pub struct SwapTransferOutput { 39 | pub token_out: String, 40 | pub amount_out: String, 41 | pub dst_chain_user: String, 42 | } 43 | 44 | #[derive(Debug, Serialize, Deserialize, Clone)] 45 | pub struct LendInput { 46 | // TO DO 47 | } 48 | 49 | #[derive(Debug, Serialize, Deserialize, Clone)] 50 | pub struct LendOutput { 51 | // TO DO 52 | } 53 | 54 | #[derive(Debug, Serialize, Deserialize, Clone)] 55 | pub struct BorrowInput { 56 | // TO DO 57 | } 58 | 59 | #[derive(Debug, Serialize, Deserialize, Clone)] 60 | pub struct BorrowOutput { 61 | // TO DO 62 | } 63 | 64 | #[derive(Debug, Serialize, Deserialize, Clone)] 65 | pub enum OperationInput { 66 | SwapTransfer(SwapTransferInput), 67 | Lend(LendInput), 68 | Borrow(BorrowInput), 69 | } 70 | 71 | #[derive(Debug, Serialize, Deserialize, Clone)] 72 | pub enum OperationOutput { 73 | SwapTransfer(SwapTransferOutput), 74 | Lend(LendOutput), 75 | Borrow(BorrowOutput), 76 | } 77 | 78 | #[derive(Debug, Serialize, Deserialize, Clone)] 79 | pub struct PostIntentInfo { 80 | pub function_name: String, 81 | pub src_chain: String, 82 | pub dst_chain: String, 83 | pub inputs: OperationInput, 84 | pub outputs: OperationOutput, 85 | } 86 | 87 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Serialize, Deserialize)] 88 | #[strum(serialize_all = "lowercase")] 89 | enum Blockchain { 90 | Ethereum, 91 | Solana, 92 | } 93 | 94 | #[derive(Debug, PartialEq, Eq, Hash, EnumString, Serialize, Deserialize)] 95 | #[strum(serialize_all = "UPPERCASE")] 96 | enum Token { 97 | USDT, 98 | } 99 | 100 | #[derive(Debug)] 101 | struct TokenInfo { 102 | address: HashMap, 103 | decimals: u32, 104 | } 105 | 106 | pub static SOLVER_ADDRESSES: &[&str] = &[ 107 | "0x0362110922F923B57b7EfF68eE7A51827b2dF4b4", // ethereum 108 | "6zYgJTTuHZZ3G7qNje7RbCSnNtVtGKsxN5YKopPP6cqL", // solana 109 | ]; 110 | 111 | lazy_static! { 112 | static ref TOKEN_INFO: HashMap = { 113 | let mut m = HashMap::new(); 114 | 115 | let mut usdt_addresses = HashMap::new(); 116 | usdt_addresses.insert( 117 | Blockchain::Ethereum, 118 | "0xdAC17F958D2ee523a2206206994597C13D831ec7", 119 | ); 120 | usdt_addresses.insert(Blockchain::Solana, solana_chain::USDT_ADDRESS); 121 | m.insert( 122 | Token::USDT, 123 | TokenInfo { 124 | address: usdt_addresses, 125 | decimals: 6, 126 | }, 127 | ); 128 | 129 | m 130 | }; 131 | pub static ref SOLVER_ID: String = env::var("SOLVER_ID").unwrap_or_else(|_| String::from("")); 132 | pub static ref SOLVER_PRIVATE_KEY: String = 133 | env::var("ETHEREUM_PKEY").unwrap_or_else(|_| String::from("")); 134 | } 135 | 136 | pub fn get_token_info(token: &str, blockchain: &str) -> Option<(&'static str, u32)> { 137 | let token_enum = Token::from_str(token).ok()?; 138 | let blockchain_enum = Blockchain::from_str(blockchain).ok()?; 139 | let info = TOKEN_INFO.get(&token_enum)?; 140 | let address = info.address.get(&blockchain_enum)?; 141 | Some((address, info.decimals)) 142 | } 143 | 144 | pub async fn create_keccak256_signature( 145 | json_data: &mut Value, 146 | private_key: String, 147 | ) -> Result<(), Box> { 148 | let json_str = json_data.to_string(); 149 | let json_bytes = json_str.as_bytes(); 150 | 151 | let hash = keccak256(json_bytes); 152 | let hash_hex = hex::encode(hash); 153 | 154 | let wallet: LocalWallet = private_key.parse().unwrap(); 155 | let eth_message_hash = hash_message(hash); 156 | 157 | let signature: Signature = wallet.sign_hash(H256::from(eth_message_hash)).unwrap(); 158 | let signature_hex = signature.to_string(); 159 | 160 | if let Some(msg) = json_data.get_mut("msg") { 161 | msg["hash"] = Value::String(hash_hex); 162 | msg["signature"] = Value::String(signature_hex); 163 | } 164 | 165 | Ok(()) 166 | } 167 | -------------------------------------------------------------------------------- /example_solver/src/chains/solana.rs: -------------------------------------------------------------------------------- 1 | pub mod solana_chain { 2 | use crate::chains::*; 3 | use crate::routers::jupiter::jupiter_swap; 4 | use crate::routers::jupiter::quote; 5 | use crate::routers::jupiter::Memo as Jup_Memo; 6 | use crate::routers::jupiter::QuoteConfig; 7 | use crate::routers::jupiter::SwapMode; 8 | use crate::PostIntentInfo; 9 | use anchor_client::Cluster; 10 | use jito_protos::searcher::SubscribeBundleResultsRequest; 11 | use num_bigint::BigInt; 12 | use serde::{Deserialize, Serialize}; 13 | use serde_json::json; 14 | use solana_client::nonblocking::rpc_client::RpcClient; 15 | use solana_sdk::commitment_config::CommitmentConfig; 16 | use solana_sdk::compute_budget::ComputeBudgetInstruction; 17 | use solana_sdk::instruction::Instruction; 18 | use solana_sdk::message::{v0, VersionedMessage}; 19 | use solana_sdk::pubkey; 20 | use solana_sdk::pubkey::Pubkey; 21 | use solana_sdk::signature::Signature; 22 | use solana_sdk::signature::{Keypair, Signer}; 23 | use solana_sdk::system_instruction; 24 | use solana_sdk::system_program; 25 | use solana_sdk::transaction::{Transaction, VersionedTransaction}; 26 | use spl_associated_token_account::get_associated_token_address; 27 | use spl_associated_token_account::instruction; 28 | use spl_token::instruction::transfer; 29 | use std::env; 30 | use std::str::FromStr; 31 | use std::sync::atomic::{AtomicBool, Ordering}; 32 | use std::sync::Arc; 33 | use std::time::Duration; 34 | use strum_macros::{Display, IntoStaticStr}; 35 | 36 | pub const JITO_ADDRESS: Pubkey = pubkey!("96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5"); 37 | pub const AUCTIONEER_ADDRESS: Pubkey = pubkey!("5zCZ3jk8EZnJyG7fhDqD6tmqiYTLZjik5HUpGMnHrZfC"); 38 | pub const JITO_TIP_AMOUNT: u64 = 100_000; 39 | pub const JITO_BLOCK_ENGINE_URL: &str = "https://mainnet.block-engine.jito.wtf"; 40 | pub const MAX_RETRIES: u8 = 1; 41 | pub const WSOL_ADDRESS: &str = "So11111111111111111111111111111111111111112"; 42 | pub const USDT_ADDRESS: &str = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"; 43 | pub static SUBMIT_THROUGH_JITO: AtomicBool = AtomicBool::new(option_env!("JITO").is_some()); 44 | const AUCTIONEER_URL: &str = "http://34.78.217.187:8080"; 45 | 46 | /// The Errors that may occur while using this module 47 | #[derive(thiserror::Error, Debug)] 48 | pub enum Error { 49 | #[error("Soalana client error: {0}")] 50 | SolanaClient(#[from] solana_client::client_error::ClientError), 51 | 52 | #[error("Reqwest HTTP error: {0}")] 53 | Reqwest(#[from] reqwest::Error), 54 | 55 | #[error("Jito client error: {0}")] 56 | JitoClient(#[from] jito_searcher_client::BlockEngineConnectionError), 57 | 58 | #[error("Message compile error: {0}")] 59 | MessageCompile(#[from] solana_sdk::message::CompileError), 60 | 61 | #[error("Signer error: {0}")] 62 | Signer(#[from] solana_sdk::signature::SignerError), 63 | 64 | #[error("Program error: {0}")] 65 | Program(#[from] solana_program::program_error::ProgramError), 66 | 67 | #[error("Failed to parse {name}: {source}")] 68 | ParseInt { 69 | name: String, 70 | #[source] 71 | source: std::num::ParseIntError, 72 | }, 73 | 74 | #[error("Failed to parse {name}: {source}")] 75 | ParsePubkey { 76 | name: String, 77 | #[source] 78 | source: solana_program::pubkey::ParsePubkeyError, 79 | }, 80 | 81 | #[error("Env var must be set: {name} {source}")] 82 | EnvVar { 83 | name: String, 84 | #[source] 85 | source: std::env::VarError, 86 | }, 87 | 88 | #[error("{0}")] 89 | Message(String), 90 | 91 | #[error("{context}: {source}")] 92 | WithContext { 93 | context: String, 94 | #[source] 95 | source: Box, 96 | }, 97 | } 98 | 99 | impl Error { 100 | pub fn with_context(context: &str, source: E) -> Self 101 | where 102 | E: std::error::Error + Send + Sync + 'static, 103 | { 104 | Error::WithContext { 105 | context: context.to_string(), 106 | source: Box::new(source), 107 | } 108 | } 109 | } 110 | 111 | type Result = std::result::Result; 112 | 113 | #[allow(clippy::upper_case_acronyms)] 114 | #[derive(Debug, Clone, Copy, Default, EnumString, Display, IntoStaticStr)] 115 | #[strum(serialize_all = "snake_case")] 116 | pub enum TxSendMethod { 117 | #[default] 118 | JITO, 119 | RPC, 120 | } 121 | 122 | // DUMMY MANTIS = 78grvu3nEsQsx3tdMB8BqedJF2hyJx1GPgjGQZWDrDTS 123 | 124 | #[derive(Debug, Serialize, Deserialize)] 125 | struct SwapData { 126 | pub user_account: String, 127 | pub token_in: String, 128 | pub token_out: String, 129 | pub amount: u64, 130 | pub slippage_bps: u64, 131 | } 132 | 133 | #[derive(Debug, Serialize, Deserialize)] 134 | struct Memo { 135 | tx_hash: String, 136 | intent_id: String, 137 | params: Vec, 138 | } 139 | 140 | pub async fn handle_solana_execution( 141 | intent: &PostIntentInfo, 142 | intent_id: &str, 143 | amount: &str, 144 | ) -> Result<()> { 145 | let solver_keypair = Arc::new(Keypair::from_base58_string( 146 | env::var("SOLANA_KEYPAIR") 147 | .map_err(|e| Error::EnvVar { 148 | name: "SOLANA_KEYPAIR".into(), 149 | source: e, 150 | })? 151 | .as_str(), 152 | )); 153 | let rpc_url = env::var("SOLANA_RPC").map_err(|e| Error::EnvVar { 154 | name: "SOLANA_RPC".into(), 155 | source: e, 156 | })?; 157 | let client = RpcClient::new_with_commitment(rpc_url.clone(), CommitmentConfig::confirmed()); 158 | 159 | // let solver_usdt_ata = get_associated_token_address( 160 | // &solver_keypair.pubkey(), 161 | // &Pubkey::from_str(USDT_ADDRESS).unwrap(), 162 | // ); 163 | 164 | // let balance_ant = client 165 | // .get_token_account_balance(&solver_usdt_ata) 166 | // .await 167 | // .map_err(|e| Error::with_context("Failed to get solver USDT token account balance", e))? 168 | // .ui_amount 169 | // .unwrap(); 170 | 171 | let mut user_account = String::default(); 172 | let mut token_in = String::default(); 173 | let mut amount_in = String::default(); 174 | let mut token_out = String::default(); 175 | 176 | if let OperationOutput::SwapTransfer(transfer_output) = &intent.outputs { 177 | user_account = transfer_output.dst_chain_user.clone(); 178 | token_out = transfer_output.token_out.clone(); 179 | 180 | if token_out == system_program::ID.to_string() { 181 | token_out = WSOL_ADDRESS.to_string(); 182 | } 183 | } 184 | if let OperationInput::SwapTransfer(transfer_input) = &intent.inputs { 185 | token_in = transfer_input.token_in.clone(); 186 | amount_in = transfer_input.amount_in.clone(); 187 | } 188 | 189 | // swap USDT -> token_out 190 | let solver_token_out_account = get_associated_token_address( 191 | &solver_keypair.pubkey(), 192 | &Pubkey::from_str(&token_out).map_err(|e| Error::ParsePubkey { 193 | name: "token_out".into(), 194 | source: e, 195 | })?, 196 | ); 197 | 198 | let solver_token_out_balance = client 199 | .get_token_account_balance(&solver_token_out_account) 200 | .await 201 | .map(|result| result.amount) 202 | .unwrap_or_else(|_| "0".to_string()) 203 | .parse::() 204 | .map_err(|e| Error::ParseInt { 205 | name: "balance".into(), 206 | source: e, 207 | })?; 208 | 209 | let token_out_amount = amount.parse::().map_err(|e| Error::ParseInt { 210 | name: "amount".into(), 211 | source: e, 212 | })?; 213 | 214 | let mut setup_instructions: Vec = vec![]; 215 | let mut bundle_instructions: Vec> = vec![]; 216 | 217 | if solver_token_out_balance < token_out_amount 218 | && !token_out.eq_ignore_ascii_case(USDT_ADDRESS) 219 | { 220 | let ts_instructions = 221 | solana_transfer_swap(intent.clone(), amount) 222 | .await 223 | .map_err(|e| { 224 | Error::with_context( 225 | "Error occurred during swap from USDT to token_out (manual swap required)", 226 | e, 227 | ) 228 | })?; 229 | setup_instructions.extend(ts_instructions.setup_instructions); 230 | if !ts_instructions.swap_instructions.is_empty() { 231 | bundle_instructions.push(ts_instructions.swap_instructions); 232 | } else if !ts_instructions.transfer_instructions.is_empty() { 233 | bundle_instructions.push(ts_instructions.transfer_instructions); 234 | } 235 | } 236 | 237 | let solver_out = if intent.src_chain == "ethereum" { 238 | SOLVER_ADDRESSES.get(0).unwrap() 239 | } else if intent.src_chain == "solana" { 240 | SOLVER_ADDRESSES.get(1).unwrap() 241 | } else { 242 | panic!("Chain not supported, this should't happen"); 243 | }; 244 | 245 | // solver -> token_out -> user | user -> token_in -> solver 246 | let sftu_instructions = solana_send_funds_to_user( 247 | intent_id, 248 | &token_in, 249 | &token_out, 250 | &user_account, 251 | solver_out.to_string(), 252 | intent.src_chain == intent.dst_chain, 253 | rpc_url, 254 | bridge_escrow::ID, 255 | token_out_amount, 256 | ) 257 | .await 258 | .map_err(|e| Error::with_context("Error occurred during solana_send_funds_to_user", e))?; 259 | 260 | let mut sftu_tx_index: Option = None; 261 | setup_instructions.extend(sftu_instructions.setup_instructions); 262 | if !sftu_instructions.send_funds_to_user_instructions.is_empty() { 263 | bundle_instructions.push(sftu_instructions.send_funds_to_user_instructions); 264 | // We record the SendFundsToUser transaction index to later find the correct signature. 265 | sftu_tx_index = Some(bundle_instructions.len()); 266 | } 267 | 268 | // swap token_in -> USDT 269 | if intent.src_chain == intent.dst_chain && !token_in.eq_ignore_ascii_case(USDT_ADDRESS) { 270 | let mut amount_in = amount_in.parse::().map_err(|e| Error::ParseInt { 271 | name: "amount_in".into(), 272 | source: e, 273 | })?; 274 | // The bridge escrow contract takes a 0.1% fee. 275 | amount_in -= amount_in / 1000; 276 | 277 | let memo = format!( 278 | r#"{{"user_account": "{}","token_in": "{}","token_out": "{}","amount": {},"slippage_bps": {}}}"#, 279 | SOLVER_ADDRESSES.get(1).unwrap(), 280 | token_in, 281 | USDT_ADDRESS, 282 | amount_in, 283 | 1000 284 | ); 285 | 286 | let js_instructions = jupiter_swap( 287 | &memo, 288 | &client, 289 | solver_keypair.clone(), 290 | SwapMode::ExactIn, 291 | true, 292 | ) 293 | .await 294 | .map_err(|e| Error::Message(format!("Failed to swap token_in to USDT: {e}")))?; 295 | 296 | setup_instructions.extend(js_instructions.setup_instructions); 297 | if !js_instructions.swap_instructions.is_empty() { 298 | bundle_instructions.push(js_instructions.swap_instructions); 299 | } 300 | } 301 | 302 | // if intent.src_chain == intent.dst_chain { 303 | // let mut balance_post = client 304 | // .get_token_account_balance(&solver_usdt_ata) 305 | // .await 306 | // .unwrap() 307 | // .ui_amount 308 | // .unwrap(); 309 | 310 | // let balance = if balance_post > balance_ant { 311 | // balance_post - balance_ant 312 | // } else if balance_post < balance_ant { 313 | // balance_ant - balance_post 314 | // } else { 315 | // std::thread::sleep(Duration::from_secs(5)); 316 | // balance_post = client 317 | // .get_token_account_balance(&solver_usdt_ata) 318 | // .await 319 | // .unwrap() 320 | // .ui_amount 321 | // .unwrap(); 322 | 323 | // balance_post - balance_ant 324 | // }; 325 | 326 | // println!( 327 | // "You have {} {} USDT on intent {intent_id}", 328 | // if balance_post >= balance_ant { 329 | // "won" 330 | // } else { 331 | // "lost" 332 | // }, 333 | // balance 334 | // ); 335 | // } 336 | 337 | match submit_through_jito( 338 | &client, 339 | solver_keypair, 340 | setup_instructions, 341 | bundle_instructions, 342 | JITO_TIP_AMOUNT, 343 | ) 344 | .await 345 | { 346 | Ok(bundle_signatures) => { 347 | if let Some(index) = sftu_tx_index { 348 | let sftu_tx_hash = bundle_signatures[index]; 349 | println!( 350 | "Sending SendFundsToUser transaction hash to auctioneer: {}", 351 | sftu_tx_hash 352 | ); 353 | send_tx_hash_to_auctioneer(AUCTIONEER_URL, sftu_tx_hash) 354 | .await 355 | .map_err(|e| { 356 | Error::with_context("Failed to send transaction hash to auctioneer", e) 357 | })?; 358 | } else { 359 | return Err(Error::Message( 360 | "SendFundsToUser signature index missing".into(), 361 | )); 362 | } 363 | println!("Intent {intent_id} solved successfully"); 364 | Ok(()) 365 | } 366 | Err(error) => Err(Error::with_context( 367 | "Failed to submit intent transactions", 368 | error, 369 | )), 370 | } 371 | } 372 | 373 | #[derive(Debug, Default)] 374 | pub struct TransferSwapInstructions { 375 | pub setup_instructions: Vec, 376 | pub transfer_instructions: Vec, 377 | pub swap_instructions: Vec, 378 | } 379 | 380 | pub async fn solana_transfer_swap( 381 | intent: PostIntentInfo, 382 | amount: &str, 383 | ) -> Result { 384 | let rpc_url = env::var("SOLANA_RPC").map_err(|e| Error::EnvVar { 385 | name: "SOLANA_RPC".into(), 386 | source: e, 387 | })?; 388 | 389 | let solver_keypair = Arc::new(Keypair::from_base58_string( 390 | env::var("SOLANA_KEYPAIR") 391 | .map_err(|e| Error::EnvVar { 392 | name: "SOLANA_KEYPAIR".into(), 393 | source: e, 394 | })? 395 | .as_str(), 396 | )); 397 | 398 | let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed()); 399 | 400 | let mut ts_instructions = TransferSwapInstructions::default(); 401 | 402 | match intent.function_name.as_str() { 403 | "transfer" => { 404 | let mut user_account = String::default(); 405 | let mut token_out = String::default(); 406 | let mut parsed_amount = 0u64; 407 | 408 | if let OperationOutput::SwapTransfer(transfer_output) = &intent.outputs { 409 | user_account = transfer_output.dst_chain_user.clone(); 410 | token_out = transfer_output.token_out.clone(); 411 | parsed_amount = 412 | transfer_output 413 | .amount_out 414 | .parse::() 415 | .map_err(|e| Error::ParseInt { 416 | name: "amount_out".into(), 417 | source: e, 418 | })?; 419 | } 420 | 421 | let instructions = transfer_spl20( 422 | &client, 423 | solver_keypair.clone(), 424 | &Pubkey::from_str(&user_account).map_err(|e| Error::ParsePubkey { 425 | name: "user_account".into(), 426 | source: e, 427 | })?, 428 | &Pubkey::from_str(&token_out).map_err(|e| Error::ParsePubkey { 429 | name: "token_out".into(), 430 | source: e, 431 | })?, 432 | parsed_amount, 433 | ) 434 | .await 435 | .map_err(|e| Error::with_context("Token transfer failed", e))?; 436 | 437 | ts_instructions.setup_instructions = instructions.setup_instructions; 438 | ts_instructions.transfer_instructions = instructions.transfer_instructions; 439 | } 440 | "swap" => { 441 | let mut token_out = String::default(); 442 | 443 | if let OperationOutput::SwapTransfer(transfer_output) = &intent.outputs { 444 | token_out = transfer_output.token_out.clone(); 445 | } 446 | 447 | let memo = format!( 448 | r#"{{"user_account": "{}","token_in": "{}","token_out": "{}","amount": {},"slippage_bps": {}}}"#, 449 | SOLVER_ADDRESSES.get(1).unwrap(), 450 | USDT_ADDRESS, 451 | token_out, 452 | amount, 453 | 200 454 | ); 455 | 456 | let js_instructions = jupiter_swap( 457 | &memo, 458 | &client, 459 | solver_keypair.clone(), 460 | SwapMode::ExactOut, 461 | true, 462 | ) 463 | .await 464 | .map_err(|e| Error::Message(format!("Jupiter swap failed: {e}")))?; 465 | 466 | ts_instructions.setup_instructions = js_instructions.setup_instructions; 467 | ts_instructions.swap_instructions = js_instructions.swap_instructions; 468 | } 469 | _ => { 470 | return Err(Error::Message("Function not supported".into())); 471 | } 472 | }; 473 | 474 | Ok(ts_instructions) 475 | } 476 | 477 | #[derive(Debug, Default)] 478 | struct TransferSPL20Instructions { 479 | setup_instructions: Vec, 480 | transfer_instructions: Vec, 481 | } 482 | 483 | async fn transfer_spl20( 484 | client: &RpcClient, 485 | sender_keypair: Arc, 486 | recipient_wallet: &Pubkey, 487 | token_mint: &Pubkey, 488 | amount: u64, 489 | ) -> Result { 490 | let sender_wallet = &sender_keypair.pubkey(); 491 | let sender_token_account = get_associated_token_address(sender_wallet, token_mint); 492 | let recipient_token_account = get_associated_token_address(recipient_wallet, token_mint); 493 | 494 | if client.get_account(&sender_token_account).await.is_err() { 495 | eprintln!("Sender's associated token account does not exist"); 496 | return Err(Error::Message( 497 | "Sender's associated token account does not exist".into(), 498 | )); 499 | } 500 | 501 | let mut setup_instructions: Vec = vec![]; 502 | if client.get_account(&recipient_token_account).await.is_err() { 503 | let token_program_id = get_token_program_id(client, token_mint).await?; 504 | setup_instructions.push(instruction::create_associated_token_account_idempotent( 505 | &sender_keypair.pubkey(), 506 | recipient_wallet, 507 | token_mint, 508 | &token_program_id, 509 | )); 510 | } 511 | 512 | let mut transfer_instructions: Vec = vec![]; 513 | let recent_blockhash = client.get_latest_blockhash().await?; 514 | let transfer_instruction = transfer( 515 | &spl_token::id(), 516 | &sender_token_account, 517 | &recipient_token_account, 518 | &sender_keypair.pubkey(), 519 | &[], 520 | amount, 521 | )?; 522 | transfer_instructions.push(transfer_instruction.clone()); 523 | 524 | let transaction = Transaction::new_signed_with_payer( 525 | &[transfer_instruction], 526 | Some(&sender_keypair.pubkey()), 527 | &[&*sender_keypair], 528 | recent_blockhash, 529 | ); 530 | 531 | let simulation_result = client.simulate_transaction(&transaction).await?; 532 | if simulation_result.value.err.is_some() { 533 | eprintln!( 534 | "Transaction simulation failed: {:?}", 535 | simulation_result.value.err 536 | ); 537 | return Err(Error::Message("Transaction simulation failed".into())); 538 | } 539 | 540 | Ok(TransferSPL20Instructions { 541 | setup_instructions, 542 | transfer_instructions, 543 | }) 544 | } 545 | 546 | pub async fn _get_solana_token_decimals(token_address: &str) -> Result { 547 | let rpc_url = env::var("SOLANA_RPC").map_err(|e| Error::EnvVar { 548 | name: "SOLANA_RPC".into(), 549 | source: e, 550 | })?; 551 | let client = reqwest::Client::new(); 552 | let request_body = json!({ 553 | "jsonrpc": "2.0", 554 | "id": 1, 555 | "method": "getTokenSupply", 556 | "params": [ 557 | token_address 558 | ] 559 | }); 560 | 561 | let response = client 562 | .post(rpc_url) 563 | .json(&request_body) 564 | .send() 565 | .await? 566 | .json::() 567 | .await?; 568 | 569 | if let Some(decimals) = response["result"]["value"]["decimals"].as_u64() { 570 | Ok(decimals as u8) 571 | } else { 572 | Err(Error::Message("Token information not available.".into())) 573 | } 574 | } 575 | 576 | async fn get_token_program_id(rpc_client: &RpcClient, token_mint: &Pubkey) -> Result { 577 | let mint_account = rpc_client 578 | .get_account(token_mint) 579 | .await 580 | .map_err(|e| Error::with_context("Failed to get token mint account", e))?; 581 | 582 | if mint_account.owner == spl_token_2022::ID { 583 | Ok(spl_token_2022::ID) 584 | } else if mint_account.owner == spl_token::ID { 585 | Ok(spl_token::ID) 586 | } else { 587 | Err(Error::Message( 588 | "Token mint is not owned by Token or Token2022 program".into(), 589 | )) 590 | } 591 | } 592 | 593 | pub async fn solana_simulate_swap( 594 | dst_chain_user: &str, 595 | token_in: &str, 596 | mut token_out: &str, 597 | amount_in: u64, 598 | ) -> String { 599 | if token_out == system_program::ID.to_string() { 600 | token_out = WSOL_ADDRESS; 601 | } 602 | 603 | let memo_json = json!({ 604 | "user_account": dst_chain_user, 605 | "token_in": token_in, 606 | "token_out": token_out, 607 | "amount": amount_in, 608 | "slippage_bps": 100 609 | }); 610 | 611 | let memo = match Jup_Memo::from_json(&memo_json.to_string()) { 612 | Ok(memo) => memo, 613 | Err(_) => return "0".to_string(), 614 | }; 615 | 616 | let quote_config = QuoteConfig { 617 | only_direct_routes: false, 618 | swap_mode: Some(SwapMode::ExactIn), 619 | slippage_bps: Some(memo.slippage_bps), 620 | ..QuoteConfig::default() 621 | }; 622 | 623 | let quotes = match quote(memo.token_in, memo.token_out, memo.amount, quote_config).await { 624 | Ok(quotes) => quotes, 625 | Err(_) => return "0".to_string(), 626 | }; 627 | 628 | BigInt::from(quotes.out_amount).to_string() 629 | } 630 | 631 | #[derive(Debug, Default)] 632 | pub struct SendFundsToUserInstructions { 633 | pub setup_instructions: Vec, 634 | pub send_funds_to_user_instructions: Vec, 635 | } 636 | 637 | #[allow(clippy::too_many_arguments)] 638 | pub async fn solana_send_funds_to_user( 639 | intent_id: &str, 640 | token_in_mint: &str, 641 | token_out_mint: &str, 642 | user: &str, 643 | solver_out: String, 644 | single_domain: bool, 645 | rpc_url: String, 646 | program_id: Pubkey, 647 | amount_out_cross_chain: u64, 648 | ) -> Result { 649 | let solver_keypair = Arc::new(Keypair::from_base58_string( 650 | env::var("SOLANA_KEYPAIR") 651 | .map_err(|e| Error::EnvVar { 652 | name: "SOLANA_KEYPAIR".into(), 653 | source: e, 654 | })? 655 | .as_str(), 656 | )); 657 | 658 | // Clone the necessary variables for the task 659 | let solver_clone = Arc::clone(&solver_keypair); 660 | let intent_id = intent_id.to_string(); 661 | let token_in_mint = Pubkey::from_str(token_in_mint).map_err(|e| Error::ParsePubkey { 662 | name: "token_in_mint".into(), 663 | source: e, 664 | })?; 665 | let token_out_mint = Pubkey::from_str(token_out_mint).map_err(|e| Error::ParsePubkey { 666 | name: "token_out_mint".into(), 667 | source: e, 668 | })?; 669 | let user = Pubkey::from_str(user).map_err(|e| Error::ParsePubkey { 670 | name: "user".into(), 671 | source: e, 672 | })?; 673 | 674 | let rpc_client = 675 | RpcClient::new_with_commitment(rpc_url.clone(), CommitmentConfig::confirmed()); 676 | 677 | let solver_token_out_ata = get_associated_token_address( 678 | &user, // careful with cross-chain 679 | &token_out_mint, 680 | ); 681 | 682 | let mut sftu_instructions = SendFundsToUserInstructions::default(); 683 | 684 | if single_domain { 685 | let solver_token_in_ata = 686 | get_associated_token_address(&solver_clone.pubkey(), &token_in_mint); 687 | 688 | if rpc_client.get_account(&solver_token_in_ata).await.is_err() { 689 | let token_program_id = get_token_program_id(&rpc_client, &token_in_mint).await?; 690 | sftu_instructions.setup_instructions.push( 691 | instruction::create_associated_token_account_idempotent( 692 | &solver_keypair.pubkey(), 693 | &solver_clone.pubkey(), 694 | &token_in_mint, 695 | &token_program_id, 696 | ), 697 | ); 698 | } 699 | } 700 | 701 | if rpc_client.get_account(&solver_token_out_ata).await.is_err() { 702 | let token_program_id = get_token_program_id(&rpc_client, &token_out_mint).await?; 703 | sftu_instructions.setup_instructions.push( 704 | instruction::create_associated_token_account_idempotent( 705 | &solver_keypair.pubkey(), 706 | &user, 707 | &token_out_mint, 708 | &token_program_id, 709 | ), 710 | ); 711 | } 712 | 713 | // Spawn a blocking task to construct the transaction instructions 714 | sftu_instructions.send_funds_to_user_instructions = tokio::task::block_in_place(|| { 715 | let client = anchor_client::Client::new_with_options( 716 | Cluster::Custom(rpc_url.clone(), rpc_url), 717 | solver_clone.clone(), 718 | CommitmentConfig::processed(), 719 | ); 720 | 721 | let program = client 722 | .program(program_id) 723 | .map_err(|e| Error::with_context("Failed to access bridge_escrow program", e))?; 724 | 725 | let user_token_out_addr = get_associated_token_address(&user, &token_out_mint); 726 | 727 | let intent_state = 728 | Pubkey::find_program_address(&[b"intent", intent_id.as_bytes()], &program_id).0; 729 | 730 | let auctioneer_state = Pubkey::find_program_address(&[b"auctioneer"], &program_id).0; 731 | 732 | let solver_token_out_addr = 733 | get_associated_token_address(&solver_clone.pubkey(), &token_out_mint); 734 | 735 | let (_storage, _bump_storage) = Pubkey::find_program_address( 736 | &[solana_ibc::SOLANA_IBC_STORAGE_SEED], 737 | &solana_ibc::ID, 738 | ); 739 | 740 | let (_trie, _bump_trie) = 741 | Pubkey::find_program_address(&[solana_ibc::TRIE_SEED], &solana_ibc::ID); 742 | 743 | let (_chain, _bump_chain) = 744 | Pubkey::find_program_address(&[solana_ibc::CHAIN_SEED], &solana_ibc::ID); 745 | 746 | let (_mint_authority, _bump_mint_authority) = 747 | Pubkey::find_program_address(&[solana_ibc::MINT_ESCROW_SEED], &solana_ibc::ID); 748 | 749 | let _dummy_token_mint = Pubkey::find_program_address(&[b"dummy"], &program_id).0; 750 | 751 | let _hashed_full_denom = 752 | lib::hash::CryptoHash::digest(_dummy_token_mint.to_string().as_bytes()); 753 | 754 | let (_escrow_account, _bump_escrow_account) = Pubkey::find_program_address( 755 | &[solana_ibc::ESCROW, _hashed_full_denom.as_slice()], 756 | &solana_ibc::ID, 757 | ); 758 | 759 | let _receiver_token_account = 760 | get_associated_token_address(&solver_keypair.pubkey(), &_dummy_token_mint); 761 | 762 | let (_fee_collector, _bump_fee_collector) = 763 | Pubkey::find_program_address(&[solana_ibc::FEE_SEED], &solana_ibc::ID); 764 | 765 | let storage; 766 | let trie; 767 | let chain; 768 | let mint_authority; 769 | let dummy_token_mint = Some(_dummy_token_mint); 770 | let escrow_account; 771 | let receiver_token_account; 772 | let fee_collector; 773 | 774 | if !single_domain { 775 | storage = Some(_storage); 776 | trie = Some(_trie); 777 | chain = Some(_chain); 778 | mint_authority = Some(_mint_authority); 779 | escrow_account = Some(_escrow_account); 780 | receiver_token_account = Some(_receiver_token_account); 781 | fee_collector = Some(_fee_collector); 782 | 783 | program 784 | .request() 785 | .instruction(ComputeBudgetInstruction::set_compute_unit_limit(1_000_000)) 786 | .instruction(ComputeBudgetInstruction::request_heap_frame(128 * 1024)) 787 | .accounts(bridge_escrow::accounts::SplTokenTransferCrossChain { 788 | auctioneer_state, 789 | solver: solver_clone.pubkey(), 790 | auctioneer: AUCTIONEER_ADDRESS, 791 | token_in: None, 792 | token_out: token_out_mint, 793 | auctioneer_token_in_account: None, 794 | solver_token_in_account: None, 795 | solver_token_out_account: solver_token_out_addr, 796 | user_token_out_account: user_token_out_addr, 797 | token_program: anchor_spl::token::ID, 798 | associated_token_program: anchor_spl::associated_token::ID, 799 | system_program: anchor_lang::solana_program::system_program::ID, 800 | ibc_program: Some(solana_ibc::ID), 801 | receiver: Some(user), 802 | storage: storage, 803 | trie: trie, 804 | chain: chain, 805 | mint_authority: mint_authority, 806 | token_mint: dummy_token_mint, 807 | escrow_account: escrow_account, 808 | receiver_token_account: receiver_token_account, 809 | fee_collector: fee_collector, 810 | }) 811 | .args(bridge_escrow::instruction::SendFundsToUserCrossChain { 812 | intent_id: intent_id.clone(), 813 | amount_out: amount_out_cross_chain, 814 | solver_out: solver_out.clone(), 815 | }) 816 | .payer(solver_clone.clone()) 817 | .instructions() 818 | .map_err(|e| { 819 | Error::with_context( 820 | "Failed to create instructions for SendFundsToUserCrossChain", 821 | e, 822 | ) 823 | }) 824 | } else { 825 | storage = Some(_storage); 826 | trie = Some(_trie); 827 | chain = Some(_chain); 828 | mint_authority = Some(_mint_authority); 829 | escrow_account = Some(_escrow_account); 830 | receiver_token_account = Some(_receiver_token_account); 831 | fee_collector = Some(_fee_collector); 832 | let receiver = Some(user); 833 | let token_in_escrow_addr = 834 | get_associated_token_address(&auctioneer_state, &token_in_mint); 835 | let solver_token_in_addr = 836 | get_associated_token_address(&solver_clone.pubkey(), &token_in_mint); 837 | let auctioneer_token_in_account = Some(token_in_escrow_addr); 838 | let solver_token_in_account = Some(solver_token_in_addr); 839 | 840 | program 841 | .request() 842 | .instruction(ComputeBudgetInstruction::set_compute_unit_limit(1_000_000)) 843 | .instruction(ComputeBudgetInstruction::request_heap_frame(128 * 1024)) 844 | .accounts(bridge_escrow::accounts::SplTokenTransfer { 845 | solver: solver_clone.pubkey(), 846 | intent: Some(intent_state), 847 | intent_owner: receiver.unwrap(), 848 | auctioneer_state, 849 | auctioneer: AUCTIONEER_ADDRESS, 850 | token_in: Some(token_in_mint), 851 | token_out: token_out_mint, 852 | auctioneer_token_in_account: auctioneer_token_in_account, 853 | solver_token_in_account: solver_token_in_account, 854 | solver_token_out_account: solver_token_out_addr, 855 | user_token_out_account: user_token_out_addr, 856 | token_program: anchor_spl::token::ID, 857 | associated_token_program: anchor_spl::associated_token::ID, 858 | system_program: anchor_lang::solana_program::system_program::ID, 859 | ibc_program: Some(solana_ibc::ID), 860 | receiver: receiver, 861 | storage: storage, 862 | trie: trie, 863 | chain: chain, 864 | mint_authority: mint_authority, 865 | token_mint: dummy_token_mint, 866 | escrow_account: escrow_account, 867 | receiver_token_account: receiver_token_account, 868 | fee_collector: fee_collector, 869 | }) 870 | .args(bridge_escrow::instruction::SendFundsToUser { 871 | intent_id: intent_id.to_string(), 872 | }) 873 | .payer(solver_clone.clone()) 874 | .instructions() 875 | .map_err(|e| { 876 | Error::with_context("Failed to create instructions for SendFundsToUser", e) 877 | }) 878 | } 879 | })?; 880 | Ok(sftu_instructions) 881 | } 882 | 883 | pub async fn submit( 884 | rpc_client: &RpcClient, 885 | fee_payer: Arc, 886 | instructions: Vec, 887 | jito_tip: u64, 888 | ) -> Result { 889 | let tx_send_method: TxSendMethod = match SUBMIT_THROUGH_JITO.load(Ordering::Relaxed) { 890 | true => TxSendMethod::JITO, 891 | false => TxSendMethod::RPC, 892 | }; 893 | match tx_send_method { 894 | TxSendMethod::JITO => { 895 | submit_through_jito(rpc_client, fee_payer, vec![], vec![instructions], jito_tip) 896 | .await 897 | .map(|signatures| signatures.first().cloned().unwrap()) 898 | } 899 | TxSendMethod::RPC => submit_default(rpc_client, fee_payer, instructions, true).await, 900 | } 901 | } 902 | 903 | pub async fn submit_default( 904 | rpc_client: &RpcClient, 905 | fee_payer: Arc, 906 | instructions: Vec, 907 | legacy_transaction: bool, 908 | ) -> Result { 909 | let mut retries = 0; 910 | loop { 911 | let send_result; 912 | if legacy_transaction { 913 | let transaction = Transaction::new_signed_with_payer( 914 | &instructions, 915 | Some(&fee_payer.pubkey()), 916 | &[&*fee_payer], 917 | rpc_client.get_latest_blockhash().await?, 918 | ); 919 | 920 | send_result = rpc_client 921 | .send_and_confirm_transaction_with_spinner(&transaction) 922 | .await; 923 | } else { 924 | let message = v0::Message::try_compile( 925 | &fee_payer.pubkey(), 926 | &instructions, 927 | &[], 928 | rpc_client.get_latest_blockhash().await?, 929 | )?; 930 | 931 | let transaction = 932 | VersionedTransaction::try_new(VersionedMessage::V0(message), &[&fee_payer])?; 933 | 934 | send_result = rpc_client 935 | .send_and_confirm_transaction_with_spinner(&transaction) 936 | .await; 937 | } 938 | 939 | match send_result { 940 | Ok(signature) => return Ok(signature), 941 | Err(error) if error.to_string().contains("unable to confirm transaction") => { 942 | if retries == MAX_RETRIES { 943 | return Err(Error::with_context( 944 | "Reached maximum retries for transaction", 945 | error, 946 | )); 947 | } 948 | eprintln!("Sending transaction failed: {}", error); 949 | println!( 950 | "Retrying transaction {} more time(s)", 951 | MAX_RETRIES - retries 952 | ); 953 | retries += 1; 954 | std::thread::sleep(Duration::from_secs(1)); 955 | } 956 | Err(error) => { 957 | return Err(Error::with_context( 958 | "Transaction failed due to a non-retryable error", 959 | error, 960 | )); 961 | } 962 | } 963 | } 964 | } 965 | 966 | pub async fn submit_through_jito( 967 | rpc_client: &RpcClient, 968 | fee_payer: Arc, 969 | setup_instructions: Vec, 970 | bundle_instructions: Vec>, 971 | jito_tip: u64, 972 | ) -> Result> { 973 | let recent_blockhash = rpc_client.get_latest_blockhash().await?; 974 | 975 | let mut setup_with_tip_instructions = setup_instructions.clone(); 976 | 977 | let tip_instruction = 978 | system_instruction::transfer(&fee_payer.pubkey(), &JITO_ADDRESS, jito_tip); 979 | 980 | setup_with_tip_instructions.push(tip_instruction); 981 | 982 | let setup_with_tip_message = v0::Message::try_compile( 983 | &fee_payer.pubkey(), 984 | &setup_with_tip_instructions, 985 | &[], 986 | recent_blockhash, 987 | )?; 988 | 989 | let setup_with_tip_transaction = VersionedTransaction::try_new( 990 | VersionedMessage::V0(setup_with_tip_message), 991 | &[&fee_payer], 992 | )?; 993 | 994 | let mut bundle_transactions: Vec = vec![setup_with_tip_transaction]; 995 | 996 | // Create a VersionedTransaction from each instruction group in the bundle. 997 | for instructions in bundle_instructions.iter() { 998 | let message = 999 | v0::Message::try_compile(&fee_payer.pubkey(), instructions, &[], recent_blockhash)?; 1000 | let transaction = 1001 | VersionedTransaction::try_new(VersionedMessage::V0(message), &[&fee_payer])?; 1002 | bundle_transactions.push(transaction); 1003 | } 1004 | 1005 | let mut client = 1006 | jito_searcher_client::get_searcher_client(JITO_BLOCK_ENGINE_URL, &fee_payer).await?; 1007 | 1008 | let mut bundle_results_subscription = client 1009 | .subscribe_bundle_results(SubscribeBundleResultsRequest {}) 1010 | .await 1011 | .map_err(|e| Error::with_context("Failed to subscribe to bundle results", e))? 1012 | .into_inner(); 1013 | 1014 | let jito_bundle_result = if bundle_transactions.len() > 5 { 1015 | Err(Error::Message("Bundle has more than 5 transactions".into())) 1016 | } else { 1017 | let mut bundle_tx_signtures: Vec = vec![]; 1018 | bundle_transactions 1019 | .iter() 1020 | .for_each(|tx| bundle_tx_signtures.extend(tx.signatures.clone())); 1021 | println!( 1022 | "Sending Jito bundle with {} transactions: {:?}", 1023 | bundle_transactions.len(), 1024 | bundle_tx_signtures 1025 | ); 1026 | 1027 | jito_searcher_client::send_bundle_with_confirmation( 1028 | &bundle_transactions, 1029 | rpc_client, 1030 | &mut client, 1031 | &mut bundle_results_subscription, 1032 | ) 1033 | .await 1034 | .map_err(|e| Error::Message(e.to_string())) 1035 | }; 1036 | 1037 | match jito_bundle_result { 1038 | Err(error) => { 1039 | eprintln!("Failed to send the Jito bundle: {error}"); 1040 | let mut rpc_signatures: Vec = vec![]; 1041 | for (i, tx_instructions) in bundle_instructions.iter().enumerate() { 1042 | println!( 1043 | "Sending transaction {} of {} via RPC", 1044 | i + 1, 1045 | bundle_instructions.len() 1046 | ); 1047 | let signature = submit_default( 1048 | rpc_client, 1049 | fee_payer.clone(), 1050 | tx_instructions.to_vec(), 1051 | false, 1052 | ) 1053 | .await 1054 | .map_err(|e| { 1055 | Error::Message(format!( 1056 | "Failed to submit transaction {} of {} via RPC: {e}", 1057 | i + 1, 1058 | bundle_instructions.len() 1059 | )) 1060 | })?; 1061 | rpc_signatures.push(signature); 1062 | } 1063 | Ok(rpc_signatures) 1064 | } 1065 | Ok(jito_signatures) => Ok(jito_signatures), 1066 | } 1067 | } 1068 | 1069 | async fn send_tx_hash_to_auctioneer(auctioneer_url: &str, tx_hash: Signature) -> Result<()> { 1070 | let response = reqwest::Client::new() 1071 | .post(format!("{auctioneer_url}/solana_tx_proof")) 1072 | .body(tx_hash.to_string()) 1073 | .send() 1074 | .await?; 1075 | println!("Auctioneer response: {}", response.status()); 1076 | Ok(()) 1077 | } 1078 | } 1079 | -------------------------------------------------------------------------------- /example_solver/src/main.rs: -------------------------------------------------------------------------------- 1 | mod chains; 2 | mod routers; 3 | 4 | use crate::chains::ethereum::ethereum_chain::handle_ethereum_execution; 5 | use crate::chains::mantis::mantis_chain::handle_mantis_execution; 6 | use crate::chains::solana::solana_chain::{handle_solana_execution, SUBMIT_THROUGH_JITO}; 7 | use crate::chains::OperationInput; 8 | use crate::chains::OperationOutput; 9 | use crate::chains::PostIntentInfo; 10 | use crate::chains::INTENTS; 11 | use crate::chains::SOLVER_ADDRESSES; 12 | use crate::chains::SOLVER_ID; 13 | use crate::chains::SOLVER_PRIVATE_KEY; 14 | use crate::routers::get_simulate_swap_intent; 15 | use chains::create_keccak256_signature; 16 | use ethers::types::U256; 17 | use futures::stream::SplitSink; 18 | use futures::{SinkExt, StreamExt}; 19 | use serde_json::json; 20 | use serde_json::Value; 21 | use spl_associated_token_account::get_associated_token_address; 22 | use std::env; 23 | use std::sync::Arc; 24 | use std::time::{SystemTime, UNIX_EPOCH}; 25 | use tokio::net::TcpStream; 26 | use tokio::sync::RwLock; 27 | use tokio_tungstenite::connect_async; 28 | use tokio_tungstenite::tungstenite::protocol::Message; 29 | use tokio_tungstenite::MaybeTlsStream; 30 | use tokio_tungstenite::WebSocketStream; 31 | 32 | #[tokio::main] 33 | async fn main() { 34 | dotenv::dotenv().ok(); 35 | let is_jito_enabled = env::var("JITO").map(|x| x == "true").unwrap_or(false); 36 | SUBMIT_THROUGH_JITO.store(is_jito_enabled, std::sync::atomic::Ordering::SeqCst); 37 | let server_addr = env::var("COMPOSABLE_ENDPOINT").unwrap_or_else(|_| String::from("")); 38 | 39 | // Connect to WebSocket 40 | let (ws_stream, _) = connect_async(server_addr).await.expect("Failed to connect"); 41 | let (ws_sender, mut ws_receiver) = ws_stream.split(); 42 | 43 | // Wrap the sender in an Arc for thread-safe sharing 44 | let ws_sender = Arc::new(RwLock::new(ws_sender)); 45 | 46 | // Initial authentication message 47 | let mut json_data = json!({ 48 | "code": 1, 49 | "msg": { 50 | "solver_id": SOLVER_ID.to_string(), 51 | "solver_addresses": SOLVER_ADDRESSES, 52 | } 53 | }); 54 | 55 | create_keccak256_signature(&mut json_data, SOLVER_PRIVATE_KEY.to_string()) 56 | .await 57 | .expect("Failed to sign message"); 58 | 59 | if json_data.get("code").unwrap() == "0" { 60 | println!("Authentication failed: {:#?}", json_data); 61 | return; 62 | } 63 | 64 | // Send the initial message 65 | { 66 | let mut ws_sender_locked = ws_sender.write().await; 67 | ws_sender_locked 68 | .send(Message::Text(json_data.to_string())) 69 | .await 70 | .expect("Failed to send initial message"); 71 | } 72 | 73 | // Handle incoming messages 74 | while let Some(msg) = ws_receiver.next().await { 75 | let ws_sender = Arc::clone(&ws_sender); 76 | tokio::spawn(async move { 77 | match msg { 78 | Ok(Message::Text(text)) => { 79 | let parsed: Value = serde_json::from_str(&text).unwrap(); 80 | let code = parsed.get("code").unwrap().as_u64().unwrap(); 81 | 82 | println!("{:#?}", parsed); 83 | 84 | if code == 1 { 85 | handle_auction_message(parsed, ws_sender).await; 86 | } else if code == 4 { 87 | handle_result_message(parsed).await; 88 | } 89 | } 90 | Ok(Message::Close(_)) | Err(_) => { 91 | println!("WebSocket closed or error occurred."); 92 | } 93 | _ => {} 94 | } 95 | }); 96 | } 97 | 98 | println!("Auctioneer went down, please reconnect"); 99 | } 100 | 101 | /// Handle "participate auction" messages (code 1) 102 | async fn handle_auction_message( 103 | parsed: Value, 104 | ws_sender: Arc>, Message>>>, 105 | ) { 106 | let intent_id = parsed 107 | .get("msg") 108 | .unwrap() 109 | .get("intent_id") 110 | .and_then(Value::as_str) 111 | .unwrap(); 112 | let intent_str = parsed 113 | .get("msg") 114 | .unwrap() 115 | .get("intent") 116 | .unwrap() 117 | .to_string(); 118 | let intent_value: Value = serde_json::from_str(&intent_str).unwrap(); 119 | let intent_info: PostIntentInfo = serde_json::from_value(intent_value).unwrap(); 120 | 121 | if let OperationInput::SwapTransfer(swap_input) = &intent_info.inputs { 122 | let timeout = swap_input.timeout.parse::().unwrap(); 123 | let current_time = SystemTime::now() 124 | .duration_since(UNIX_EPOCH) 125 | .expect("Time went backwards") 126 | .as_secs(); 127 | 128 | let ok = if current_time >= timeout { 129 | println!("current_time >= intent.timestamp: impossible to solve {intent_id}"); 130 | false 131 | } else if intent_info.src_chain == intent_info.dst_chain { 132 | if timeout - current_time < 30 { 133 | println!("timeout - current_time < 30: not willing to solve {intent_id}"); 134 | false 135 | } else { 136 | true 137 | } 138 | } else { 139 | if timeout - current_time < 1800 { 140 | println!("timeout - current_time < 30mins on cross-chain: not willing to solve {intent_id}"); 141 | false 142 | } else { 143 | true 144 | } 145 | }; 146 | 147 | if ok { 148 | let final_amount = get_simulate_swap_intent( 149 | &intent_info, 150 | &intent_info.src_chain, 151 | &intent_info.dst_chain, 152 | &String::from("USDT"), 153 | ) 154 | .await; 155 | 156 | let mut amount_out_min = U256::zero(); 157 | if let OperationOutput::SwapTransfer(transfer_output) = &intent_info.outputs { 158 | amount_out_min = U256::from_dec_str(&transfer_output.amount_out).unwrap(); 159 | } 160 | 161 | let final_amount = U256::from_dec_str(&final_amount).unwrap(); 162 | 163 | println!( 164 | "User wants {amount_out_min} token_out, you can provide {final_amount} token_out (after FLAT_FEES + COMMISSION)" 165 | ); 166 | 167 | if final_amount > amount_out_min { 168 | let mut json_data = json!({ 169 | "code": 2, 170 | "msg": { 171 | "intent_id": intent_id, 172 | "solver_id": SOLVER_ID.to_string(), 173 | "amount": final_amount.to_string() 174 | } 175 | }); 176 | 177 | create_keccak256_signature(&mut json_data, SOLVER_PRIVATE_KEY.to_string()) 178 | .await 179 | .unwrap(); 180 | 181 | { 182 | let mut ws_sender_locked = ws_sender.write().await; 183 | ws_sender_locked 184 | .send(Message::text(json_data.to_string())) 185 | .await 186 | .expect("Failed to send message"); 187 | drop(ws_sender_locked); 188 | } 189 | 190 | { 191 | let mut intents = INTENTS.write().await; 192 | intents.insert(intent_id.to_string(), intent_info); 193 | drop(intents); 194 | } 195 | } 196 | } 197 | } 198 | } 199 | 200 | async fn handle_result_message(parsed: Value) { 201 | let intent_id = parsed 202 | .get("msg") 203 | .and_then(|msg| msg.get("intent_id")) 204 | .and_then(Value::as_str) 205 | .map(|s| s.to_string()) 206 | .expect("intent_id not found"); 207 | let amount = parsed 208 | .get("msg") 209 | .and_then(|msg| msg.get("amount")) 210 | .and_then(Value::as_str) 211 | .map(|s| s.to_string()); 212 | 213 | if let Some(amount) = amount { 214 | let msg = parsed 215 | .get("msg") 216 | .and_then(|msg| msg.get("msg")) 217 | .and_then(Value::as_str) 218 | .map(|s| s.to_string()) 219 | .expect("msg not found"); 220 | 221 | if msg.contains("won") { 222 | // Clone the necessary data for the closure 223 | let cloned_intent_id = intent_id.clone(); 224 | let cloned_amount = amount.clone(); 225 | 226 | let intent; 227 | { 228 | let intents = INTENTS.read().await; 229 | intent = intents.get(&intent_id).cloned().expect("Intent not found"); 230 | drop(intents); 231 | } 232 | 233 | let result = if intent.dst_chain == "solana" { 234 | tokio::task::spawn_blocking(move || { 235 | tokio::runtime::Handle::current() 236 | .block_on(handle_solana_execution( 237 | &intent, 238 | &cloned_intent_id, 239 | &cloned_amount, 240 | )) 241 | .map_err(|e| e.to_string()) 242 | }) 243 | .await 244 | } else if intent.dst_chain == "ethereum" { 245 | tokio::task::spawn_blocking(move || { 246 | tokio::runtime::Handle::current().block_on(handle_ethereum_execution( 247 | &intent, 248 | &cloned_intent_id, 249 | &cloned_amount, 250 | )) 251 | }) 252 | .await 253 | } else if intent.dst_chain == "mantis" { 254 | tokio::task::spawn_blocking(move || { 255 | tokio::runtime::Handle::current().block_on(handle_mantis_execution( 256 | &intent, 257 | &cloned_intent_id, 258 | &cloned_amount, 259 | )) 260 | }) 261 | .await 262 | } else { 263 | Ok(Ok(())) 264 | }; 265 | 266 | // Log errors if any 267 | match result { 268 | Err(e) => eprintln!("Error spawning chain handler: {:?}", e), 269 | Ok(Err(e)) => eprintln!("Error during chain handler execution: {}", e), 270 | _ => {} 271 | } 272 | 273 | // Update INTENTS after execution 274 | { 275 | let mut intents = INTENTS.write().await; 276 | intents.remove(&intent_id); 277 | } 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /example_solver/src/routers/jupiter/field_as_string.rs: -------------------------------------------------------------------------------- 1 | use { 2 | serde::{de, Deserializer, Serializer}, 3 | serde::{Deserialize, Serialize}, 4 | std::str::FromStr, 5 | }; 6 | 7 | pub fn serialize(t: &T, serializer: S) -> Result 8 | where 9 | T: ToString, 10 | S: Serializer, 11 | { 12 | t.to_string().serialize(serializer) 13 | } 14 | 15 | pub fn deserialize<'de, T, D>(deserializer: D) -> Result 16 | where 17 | T: FromStr, 18 | D: Deserializer<'de>, 19 | ::Err: std::fmt::Debug, 20 | { 21 | let s: String = String::deserialize(deserializer)?; 22 | s.parse() 23 | .map_err(|e| de::Error::custom(format!("Parse error: {e:?}"))) 24 | } 25 | -------------------------------------------------------------------------------- /example_solver/src/routers/jupiter/field_instruction.rs: -------------------------------------------------------------------------------- 1 | // Deserialize Instruction with a custom function 2 | pub mod instruction { 3 | use serde::{Deserialize, Deserializer}; 4 | use solana_sdk::{instruction::AccountMeta, instruction::Instruction, pubkey::Pubkey}; 5 | use std::str::FromStr; 6 | 7 | pub fn deserialize<'de, D>(deserializer: D) -> Result 8 | where 9 | D: Deserializer<'de>, 10 | { 11 | #[derive(Deserialize)] 12 | #[serde(rename_all = "camelCase")] 13 | struct InstructionFields { 14 | accounts: Vec, 15 | data: String, 16 | program_id: String, 17 | } 18 | 19 | #[derive(Deserialize)] 20 | #[serde(rename_all = "camelCase")] 21 | struct AccountMetaFields { 22 | pubkey: String, 23 | is_signer: bool, 24 | is_writable: bool, 25 | } 26 | 27 | let fields = InstructionFields::deserialize(deserializer)?; 28 | let program_id = Pubkey::from_str(&fields.program_id) 29 | .map_err(|e| serde::de::Error::custom(format!("Error parsing programId: {}", e)))?; 30 | 31 | let accounts = fields 32 | .accounts 33 | .into_iter() 34 | .map(|acc| { 35 | let pubkey = Pubkey::from_str(&acc.pubkey).map_err(|e| { 36 | serde::de::Error::custom(format!("Error parsing pubkey: {}", e)) 37 | })?; 38 | Ok(AccountMeta { 39 | pubkey, 40 | is_signer: acc.is_signer, 41 | is_writable: acc.is_writable, 42 | }) 43 | }) 44 | .collect::, _>>()?; 45 | 46 | let instruction = Instruction { 47 | program_id, 48 | accounts, 49 | #[allow(deprecated)] 50 | data: base64::decode(&fields.data) 51 | .map_err(|e| serde::de::Error::custom(format!("Error decoding data: {}", e)))?, 52 | }; 53 | 54 | Ok(instruction) 55 | } 56 | } 57 | 58 | // Deserialize Option with a custom function 59 | pub mod option_instruction { 60 | use serde::{Deserialize, Deserializer}; 61 | use solana_sdk::instruction::Instruction; 62 | 63 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 64 | where 65 | D: Deserializer<'de>, 66 | { 67 | let value: serde_json::Value = Deserialize::deserialize(deserializer)?; 68 | 69 | match value { 70 | serde_json::Value::Null => Ok(None), 71 | _ => crate::routers::jupiter::field_instruction::instruction::deserialize(value) 72 | .map_err(|e| { 73 | serde::de::Error::custom(format!( 74 | "Error deserialize optional instruction: {}", 75 | e 76 | )) 77 | }) 78 | .map(Some), 79 | } 80 | } 81 | } 82 | 83 | // Deserialize Vec with a custom function 84 | pub mod vec_instruction { 85 | use serde::{Deserialize, Deserializer}; 86 | use solana_sdk::instruction::Instruction; 87 | 88 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 89 | where 90 | D: Deserializer<'de>, 91 | { 92 | let values: Vec = Deserialize::deserialize(deserializer)?; 93 | let mut instructions = Vec::new(); 94 | 95 | for value in values { 96 | let instruction: Instruction = 97 | crate::routers::jupiter::field_instruction::instruction::deserialize(value) 98 | .map_err(|e| { 99 | serde::de::Error::custom(format!( 100 | "Error deserialize vec instruction: {}", 101 | e 102 | )) 103 | })?; 104 | instructions.push(instruction); 105 | } 106 | 107 | Ok(instructions) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /example_solver/src/routers/jupiter/field_prioritization_fee.rs: -------------------------------------------------------------------------------- 1 | use {crate::routers::jupiter::PrioritizationFeeLamports, serde::Serialize, serde::Serializer}; 2 | 3 | pub fn serialize( 4 | prioritization_fee_lamports: &PrioritizationFeeLamports, 5 | serializer: S, 6 | ) -> Result 7 | where 8 | S: Serializer, 9 | { 10 | match prioritization_fee_lamports { 11 | PrioritizationFeeLamports::Auto => "auto".serialize(serializer), 12 | PrioritizationFeeLamports::Exact { lamports } => lamports.serialize(serializer), 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example_solver/src/routers/jupiter/field_pubkey.rs: -------------------------------------------------------------------------------- 1 | pub mod vec { 2 | use { 3 | serde::{de, Deserializer, Serializer}, 4 | serde::{Deserialize, Serialize}, 5 | solana_sdk::pubkey::Pubkey, 6 | std::str::FromStr, 7 | }; 8 | 9 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 10 | where 11 | D: Deserializer<'de>, 12 | { 13 | let vec_str: Vec = Vec::deserialize(deserializer)?; 14 | let mut vec_pubkey = Vec::new(); 15 | for s in vec_str { 16 | let pubkey = Pubkey::from_str(&s).map_err(de::Error::custom)?; 17 | vec_pubkey.push(pubkey); 18 | } 19 | Ok(vec_pubkey) 20 | } 21 | 22 | #[allow(dead_code)] 23 | pub fn serialize(vec_pubkey: &[Pubkey], serializer: S) -> Result 24 | where 25 | S: Serializer, 26 | { 27 | let vec_str: Vec = vec_pubkey.iter().map(|pubkey| pubkey.to_string()).collect(); 28 | vec_str.serialize(serializer) 29 | } 30 | } 31 | 32 | pub mod option { 33 | use { 34 | serde::{Serialize, Serializer}, 35 | solana_sdk::pubkey::Pubkey, 36 | }; 37 | 38 | pub fn serialize(t: &Option, serializer: S) -> Result 39 | where 40 | S: Serializer, 41 | { 42 | match t { 43 | Some(pubkey) => pubkey.to_string().serialize(serializer), 44 | None => serializer.serialize_none(), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example_solver/src/routers/jupiter/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod field_as_string; 2 | pub mod field_instruction; 3 | pub mod field_prioritization_fee; 4 | pub mod field_pubkey; 5 | 6 | use solana_sdk::transaction::VersionedTransaction; 7 | use std::sync::Arc; 8 | use std::{env, fmt, str::FromStr}; 9 | 10 | use serde_json::Value; 11 | use solana_sdk::pubkey; 12 | use solana_sdk::signer::keypair::Keypair; 13 | use spl_associated_token_account::get_associated_token_address; 14 | use spl_associated_token_account::instruction; 15 | use { 16 | serde::{Deserialize, Serialize}, 17 | solana_client::nonblocking::rpc_client::RpcClient, 18 | solana_sdk::{ 19 | instruction::Instruction, 20 | pubkey::{ParsePubkeyError, Pubkey}, 21 | signature::Signer, 22 | }, 23 | std::collections::HashMap, 24 | }; 25 | 26 | /// A `Result` alias where the `Err` case is `jup_ag::Error`. 27 | pub type Result = std::result::Result; 28 | 29 | // Reference: https://quote-api.jup.ag/v4/docs/static/index.html 30 | fn quote_api_url() -> String { 31 | env::var("QUOTE_API_URL").unwrap_or_else(|_| "https://quote-api.jup.ag/v6".to_string()) 32 | } 33 | 34 | // Reference: https://quote-api.jup.ag/docs/static/index.html 35 | fn _price_api_url() -> String { 36 | env::var("PRICE_API_URL").unwrap_or_else(|_| "https://price.jup.ag/v1".to_string()) 37 | } 38 | 39 | /// The Errors that may occur while using this crate 40 | #[derive(thiserror::Error, Debug)] 41 | pub enum Error { 42 | #[error("reqwest: {0}")] 43 | Reqwest(#[from] reqwest::Error), 44 | 45 | #[error("invalid pubkey in response data: {0}")] 46 | ParsePubkey(#[from] ParsePubkeyError), 47 | 48 | #[error("bincode: {0}")] 49 | Bincode(#[from] bincode::Error), 50 | 51 | #[error("Jupiter API: {0}")] 52 | JupiterApi(String), 53 | 54 | #[error("serde_json: {0}")] 55 | SerdeJson(#[from] serde_json::Error), 56 | 57 | #[error("parse SwapMode: Invalid value `{value}`")] 58 | ParseSwapMode { value: String }, 59 | } 60 | 61 | #[derive(Clone, Debug, Deserialize)] 62 | #[serde(rename_all = "camelCase")] 63 | pub struct Price { 64 | #[allow(dead_code)] 65 | #[serde(with = "field_as_string", rename = "id")] 66 | pub input_mint: Pubkey, 67 | #[allow(dead_code)] 68 | #[serde(rename = "mintSymbol")] 69 | pub input_symbol: String, 70 | #[allow(dead_code)] 71 | #[serde(with = "field_as_string", rename = "vsToken")] 72 | pub output_mint: Pubkey, 73 | #[allow(dead_code)] 74 | #[serde(rename = "vsTokenSymbol")] 75 | pub output_symbol: String, 76 | #[allow(dead_code)] 77 | pub price: f64, 78 | } 79 | 80 | #[derive(Clone, Debug, Deserialize, Serialize)] 81 | #[serde(rename_all = "camelCase")] 82 | pub struct Quote { 83 | #[serde(with = "field_as_string")] 84 | pub input_mint: Pubkey, 85 | #[serde(with = "field_as_string")] 86 | pub in_amount: u64, 87 | #[serde(with = "field_as_string")] 88 | pub output_mint: Pubkey, 89 | #[serde(with = "field_as_string")] 90 | pub out_amount: u64, 91 | #[serde(with = "field_as_string")] 92 | pub other_amount_threshold: u64, 93 | pub swap_mode: String, 94 | pub slippage_bps: u64, 95 | pub platform_fee: Option, 96 | #[serde(with = "field_as_string")] 97 | pub price_impact_pct: f64, 98 | pub route_plan: Vec, 99 | pub context_slot: Option, 100 | pub time_taken: Option, 101 | } 102 | 103 | #[derive(Clone, Debug, Deserialize, Serialize)] 104 | #[serde(rename_all = "camelCase")] 105 | pub struct PlatformFee { 106 | #[serde(with = "field_as_string")] 107 | pub amount: u64, 108 | pub fee_bps: u64, 109 | } 110 | 111 | #[derive(Clone, Debug, Deserialize, Serialize)] 112 | #[serde(rename_all = "camelCase")] 113 | pub struct RoutePlan { 114 | pub swap_info: SwapInfo, 115 | pub percent: u8, 116 | } 117 | 118 | #[derive(Clone, Debug, Deserialize, Serialize)] 119 | #[serde(rename_all = "camelCase")] 120 | pub struct SwapInfo { 121 | #[serde(with = "field_as_string")] 122 | pub amm_key: Pubkey, 123 | pub label: Option, 124 | #[serde(with = "field_as_string")] 125 | pub input_mint: Pubkey, 126 | #[serde(with = "field_as_string")] 127 | pub output_mint: Pubkey, 128 | #[serde(with = "field_as_string")] 129 | pub in_amount: u64, 130 | #[serde(with = "field_as_string")] 131 | pub out_amount: u64, 132 | #[serde(with = "field_as_string")] 133 | pub fee_amount: u64, 134 | #[serde(with = "field_as_string")] 135 | pub fee_mint: Pubkey, 136 | } 137 | 138 | #[derive(Clone, Debug, Deserialize, Serialize)] 139 | #[serde(rename_all = "camelCase")] 140 | pub struct FeeInfo { 141 | #[serde(with = "field_as_string")] 142 | pub amount: u64, 143 | #[serde(with = "field_as_string")] 144 | pub mint: Pubkey, 145 | pub pct: f64, 146 | } 147 | 148 | /// Partially signed transactions required to execute a swap 149 | #[derive(Clone, Debug)] 150 | pub struct Swap { 151 | pub swap_transaction: VersionedTransaction, 152 | #[allow(dead_code)] 153 | pub last_valid_block_height: u64, 154 | } 155 | 156 | /// Swap instructions 157 | #[derive(Clone, Debug, Deserialize)] 158 | #[serde(rename_all = "camelCase")] 159 | pub struct SwapInstructions { 160 | #[allow(dead_code)] 161 | #[serde(with = "field_instruction::option_instruction")] 162 | pub token_ledger_instruction: Option, 163 | #[allow(dead_code)] 164 | #[serde(with = "field_instruction::vec_instruction")] 165 | pub compute_budget_instructions: Vec, 166 | #[allow(dead_code)] 167 | #[serde(with = "field_instruction::vec_instruction")] 168 | pub setup_instructions: Vec, 169 | #[allow(dead_code)] 170 | #[serde(with = "field_instruction::instruction")] 171 | pub swap_instruction: Instruction, 172 | #[allow(dead_code)] 173 | #[serde(with = "field_instruction::option_instruction")] 174 | pub cleanup_instruction: Option, 175 | #[allow(dead_code)] 176 | #[serde(with = "field_pubkey::vec")] 177 | pub address_lookup_table_addresses: Vec, 178 | #[allow(dead_code)] 179 | pub prioritization_fee_lamports: u64, 180 | } 181 | 182 | /// Hashmap of possible swap routes from input mint to an array of output mints 183 | #[allow(dead_code)] 184 | pub type RouteMap = HashMap>; 185 | 186 | fn maybe_jupiter_api_error(value: serde_json::Value) -> Result 187 | where 188 | T: serde::de::DeserializeOwned, 189 | { 190 | #[derive(Deserialize)] 191 | struct ErrorResponse { 192 | error: String, 193 | } 194 | if let Ok(ErrorResponse { error }) = serde_json::from_value::(value.clone()) { 195 | Err(Error::JupiterApi(error)) 196 | } else { 197 | serde_json::from_value(value).map_err(|err| err.into()) 198 | } 199 | } 200 | 201 | /// Get simple price for a given input mint, output mint, and amount 202 | pub async fn _price(input_mint: Pubkey, output_mint: Pubkey, ui_amount: f64) -> Result { 203 | let url = format!( 204 | "{base_url}/price?id={input_mint}&vsToken={output_mint}&amount={ui_amount}", 205 | base_url = _price_api_url(), 206 | ); 207 | maybe_jupiter_api_error(reqwest::get(url).await?.json().await?) 208 | } 209 | 210 | #[derive(Serialize, Deserialize, Default, PartialEq, Clone, Debug)] 211 | pub enum SwapMode { 212 | #[default] 213 | ExactIn, 214 | ExactOut, 215 | } 216 | 217 | impl FromStr for SwapMode { 218 | type Err = Error; 219 | 220 | fn from_str(s: &str) -> Result { 221 | match s { 222 | "ExactIn" => Ok(Self::ExactIn), 223 | "ExactOut" => Ok(Self::ExactOut), 224 | _ => Err(Error::ParseSwapMode { value: s.into() }), 225 | } 226 | } 227 | } 228 | 229 | impl fmt::Display for SwapMode { 230 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 231 | match *self { 232 | Self::ExactIn => write!(f, "ExactIn"), 233 | Self::ExactOut => write!(f, "ExactOut"), 234 | } 235 | } 236 | } 237 | 238 | #[derive(Default)] 239 | pub struct QuoteConfig { 240 | pub slippage_bps: Option, 241 | pub swap_mode: Option, 242 | pub dexes: Option>, 243 | pub exclude_dexes: Option>, 244 | pub only_direct_routes: bool, 245 | pub as_legacy_transaction: Option, 246 | pub platform_fee_bps: Option, 247 | pub max_accounts: Option, 248 | } 249 | 250 | /// Get quote for a given input mint, output mint, and amount 251 | pub async fn quote( 252 | input_mint: Pubkey, 253 | output_mint: Pubkey, 254 | amount: u64, 255 | quote_config: QuoteConfig, 256 | ) -> Result { 257 | let url = format!( 258 | "{base_url}/quote?inputMint={input_mint}&outputMint={output_mint}&amount={amount}&onlyDirectRoutes={}&{}{}{}{}{}{}{}", 259 | quote_config.only_direct_routes, 260 | quote_config 261 | .as_legacy_transaction 262 | .map(|as_legacy_transaction| format!("&asLegacyTransaction={as_legacy_transaction}")) 263 | .unwrap_or_default(), 264 | quote_config 265 | .swap_mode 266 | .map(|swap_mode| format!("&swapMode={swap_mode}")) 267 | .unwrap_or_default(), 268 | quote_config 269 | .slippage_bps 270 | .map(|slippage_bps| format!("&slippageBps={slippage_bps}")) 271 | .unwrap_or_default(), 272 | quote_config 273 | .platform_fee_bps 274 | .map(|platform_fee_bps| format!("&feeBps={platform_fee_bps}")) 275 | .unwrap_or_default(), 276 | quote_config 277 | .dexes 278 | .map(|dexes| format!("&dexes={:?}", dexes)) 279 | .unwrap_or_default(), 280 | quote_config 281 | .exclude_dexes 282 | .map(|exclude_dexes| format!("&excludeDexes={:?}", exclude_dexes)) 283 | .unwrap_or_default(), 284 | quote_config 285 | .max_accounts 286 | .map(|max_accounts| format!("&maxAccounts={max_accounts}")) 287 | .unwrap_or_default(), 288 | base_url=quote_api_url(), 289 | ); 290 | 291 | maybe_jupiter_api_error(reqwest::get(url).await?.json().await?) 292 | } 293 | 294 | #[derive(Debug)] 295 | pub enum PrioritizationFeeLamports { 296 | #[allow(dead_code)] 297 | Auto, 298 | Exact { 299 | lamports: u64, 300 | }, 301 | } 302 | 303 | #[derive(Debug, Serialize)] 304 | #[serde(rename_all = "camelCase")] 305 | #[allow(non_snake_case)] 306 | pub struct SwapRequest { 307 | #[serde(with = "field_as_string")] 308 | pub user_public_key: Pubkey, 309 | pub wrap_and_unwrap_sol: Option, 310 | pub use_shared_accounts: Option, 311 | #[serde(with = "field_pubkey::option")] 312 | pub fee_account: Option, 313 | #[deprecated = "please use SwapRequest::prioritization_fee_lamports instead"] 314 | pub compute_unit_price_micro_lamports: Option, 315 | #[serde(with = "field_prioritization_fee")] 316 | pub prioritization_fee_lamports: PrioritizationFeeLamports, 317 | pub as_legacy_transaction: Option, 318 | pub use_token_ledger: Option, 319 | #[serde(with = "field_pubkey::option")] 320 | pub destination_token_account: Option, 321 | pub quote_response: Quote, 322 | } 323 | 324 | impl SwapRequest { 325 | /// Creates new SwapRequest with the given and default values 326 | pub fn new( 327 | user_public_key: Pubkey, 328 | quote_response: Quote, 329 | destination_account: Pubkey, 330 | ) -> Self { 331 | #[allow(deprecated)] 332 | SwapRequest { 333 | user_public_key, 334 | wrap_and_unwrap_sol: Some(true), 335 | use_shared_accounts: Some(true), 336 | fee_account: None, 337 | compute_unit_price_micro_lamports: None, 338 | prioritization_fee_lamports: PrioritizationFeeLamports::Exact { lamports: 200_000 }, 339 | as_legacy_transaction: Some(false), 340 | use_token_ledger: Some(false), 341 | destination_token_account: Some(destination_account), 342 | quote_response, 343 | } 344 | } 345 | } 346 | 347 | #[derive(Debug, Deserialize)] 348 | #[serde(rename_all = "camelCase")] 349 | struct SwapResponse { 350 | pub swap_transaction: String, 351 | pub last_valid_block_height: u64, 352 | } 353 | 354 | /// Get swap serialized transactions for a quote 355 | pub async fn swap(swap_request: SwapRequest) -> Result { 356 | let url = format!("{}/swap", quote_api_url()); 357 | 358 | let response = maybe_jupiter_api_error::( 359 | reqwest::Client::builder() 360 | .build()? 361 | .post(url) 362 | .header("Accept", "application/json") 363 | .json(&swap_request) 364 | .send() 365 | .await? 366 | .error_for_status()? 367 | .json() 368 | .await?, 369 | )?; 370 | 371 | fn decode(base64_transaction: String) -> Result { 372 | #[allow(deprecated)] 373 | bincode::deserialize(&base64::decode(base64_transaction).unwrap()).map_err(|err| err.into()) 374 | } 375 | 376 | Ok(Swap { 377 | swap_transaction: decode(response.swap_transaction)?, 378 | last_valid_block_height: response.last_valid_block_height, 379 | }) 380 | } 381 | 382 | /// Get swap serialized transaction instructions for a quote 383 | pub async fn swap_instructions(swap_request: SwapRequest) -> Result { 384 | let url = format!("{}/swap-instructions", quote_api_url()); 385 | 386 | let response = reqwest::Client::builder() 387 | .build()? 388 | .post(url) 389 | .header("Accept", "application/json") 390 | .json(&swap_request) 391 | .send() 392 | .await?; 393 | 394 | if !response.status().is_success() { 395 | return Err(Error::JupiterApi(response.text().await?)); 396 | } 397 | 398 | Ok(response.json::().await?) 399 | } 400 | 401 | /// Returns a hash map, input mint as key and an array of valid output mint as values 402 | pub async fn _route_map() -> Result { 403 | let url = format!( 404 | "{}/indexed-route-map?onlyDirectRoutes=false", 405 | quote_api_url() 406 | ); 407 | 408 | #[derive(Debug, Deserialize)] 409 | #[serde(rename_all = "camelCase")] 410 | struct IndexedRouteMap { 411 | _mint_keys: Vec, 412 | _indexed_route_map: HashMap>, 413 | } 414 | 415 | let response = reqwest::get(url).await?.json::().await?; 416 | 417 | let mint_keys = response 418 | ._mint_keys 419 | .into_iter() 420 | .map(|x| x.parse::().map_err(|err| err.into())) 421 | .collect::>>()?; 422 | 423 | let mut route_map = HashMap::new(); 424 | for (from_index, to_indices) in response._indexed_route_map { 425 | route_map.insert( 426 | mint_keys[from_index], 427 | to_indices.into_iter().map(|i| mint_keys[i]).collect(), 428 | ); 429 | } 430 | 431 | Ok(route_map) 432 | } 433 | 434 | #[derive(PartialEq, Debug, Default, Serialize, Deserialize)] 435 | pub struct Memo { 436 | pub user_account: pubkey::Pubkey, 437 | pub token_in: pubkey::Pubkey, 438 | pub token_out: pubkey::Pubkey, 439 | pub amount: u64, 440 | pub slippage_bps: u64, 441 | } 442 | 443 | impl Memo { 444 | pub fn from_json(json_str: &str) -> Result { 445 | let parsed_json: Value = serde_json::from_str(json_str).unwrap(); 446 | let mut memo = Memo::default(); 447 | 448 | memo.user_account = 449 | Pubkey::from_str(&parsed_json["user_account"].to_string().trim_matches('"')).unwrap(); 450 | memo.token_in = 451 | Pubkey::from_str(&parsed_json["token_in"].to_string().trim_matches('"')).unwrap(); 452 | memo.token_out = 453 | Pubkey::from_str(&parsed_json["token_out"].to_string().trim_matches('"')).unwrap(); 454 | memo.amount = parsed_json["amount"].as_u64().unwrap_or_default(); 455 | memo.slippage_bps = parsed_json["slippage_bps"].as_u64().unwrap_or_default(); 456 | 457 | Ok(memo) 458 | } 459 | } 460 | 461 | #[derive(Debug, Default)] 462 | pub struct JupiterSwapInstructions { 463 | pub setup_instructions: Vec, 464 | pub swap_instructions: Vec, 465 | } 466 | 467 | pub async fn jupiter_swap( 468 | memo: &str, 469 | rpc_client: &RpcClient, 470 | fee_payer: Arc, 471 | swap_mode: SwapMode, 472 | legacy_transaction: bool, 473 | ) -> core::result::Result { 474 | let memo = Memo::from_json(memo).map_err(|e| format!("Failed to parse memo: {}", e))?; 475 | 476 | let only_direct_routes = false; 477 | let quotes = quote( 478 | memo.token_in, 479 | memo.token_out, 480 | memo.amount, 481 | QuoteConfig { 482 | only_direct_routes, 483 | as_legacy_transaction: Some(legacy_transaction), 484 | swap_mode: Some(swap_mode), 485 | slippage_bps: Some(memo.slippage_bps), 486 | ..QuoteConfig::default() 487 | }, 488 | ) 489 | .await 490 | .map_err(|e| format!("Failed to get quotes: {}", e))?; 491 | 492 | let mut setup_instructions: Vec = vec![]; 493 | let user_token_out_ata = get_associated_token_address(&memo.user_account, &memo.token_out); 494 | 495 | // Check if the user token account exists, and create it if necessary 496 | if rpc_client.get_account(&user_token_out_ata).await.is_err() { 497 | let token_program_id = get_token_program_id(rpc_client, &memo.token_out).await?; 498 | setup_instructions.push(instruction::create_associated_token_account_idempotent( 499 | &fee_payer.pubkey(), 500 | &memo.user_account, 501 | &memo.token_out, 502 | &token_program_id, 503 | )) 504 | } 505 | 506 | let swap_request = SwapRequest::new(fee_payer.pubkey(), quotes.clone(), user_token_out_ata); 507 | let swap_response = swap_instructions(swap_request) 508 | .await 509 | .map_err(|e| format!("Swap creation failed: {}", e))?; 510 | 511 | // Add token ledger instruction if present 512 | let mut swap_instructions: Vec = vec![]; 513 | if let Some(token_ledger_instruction) = swap_response.token_ledger_instruction { 514 | swap_instructions.push(token_ledger_instruction); 515 | } 516 | 517 | // Add compute budget instructions 518 | swap_instructions.extend(swap_response.compute_budget_instructions); 519 | 520 | // Add setup instructions 521 | swap_instructions.extend(swap_response.setup_instructions); 522 | 523 | // Add the swap instruction 524 | swap_instructions.push(swap_response.swap_instruction); 525 | 526 | // Add cleanup instruction if present 527 | if let Some(cleanup_instruction) = swap_response.cleanup_instruction { 528 | swap_instructions.push(cleanup_instruction); 529 | } 530 | 531 | Ok(JupiterSwapInstructions { 532 | setup_instructions, 533 | swap_instructions, 534 | }) 535 | } 536 | 537 | async fn get_token_program_id( 538 | rpc_client: &RpcClient, 539 | token_mint: &Pubkey, 540 | ) -> core::result::Result { 541 | let mint_account = rpc_client 542 | .get_account(token_mint) 543 | .await 544 | .map_err(|e| format!("Failed to get token mint account: {}", e))?; 545 | 546 | if mint_account.owner == spl_token_2022::ID { 547 | Ok(spl_token_2022::ID) 548 | } else if mint_account.owner == spl_token::ID { 549 | Ok(spl_token::ID) 550 | } else { 551 | Err("Token mint is not owned by Token or Token2022 program".into()) 552 | } 553 | } 554 | -------------------------------------------------------------------------------- /example_solver/src/routers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod jupiter; 2 | pub mod paraswap; 3 | 4 | // use ethers::providers::Middleware; 5 | // use serde_json::Value; 6 | use crate::chains::*; 7 | use crate::PostIntentInfo; 8 | use ethereum::ethereum_chain::ethereum_simulate_swap; 9 | use lazy_static::lazy_static; 10 | use num_bigint::BigInt; 11 | use num_traits::ToPrimitive; 12 | use num_traits::Zero; 13 | use solana::solana_chain::solana_simulate_swap; 14 | use solana_sdk::system_program; 15 | use std::collections::HashMap; 16 | use std::env; 17 | use std::str::FromStr; 18 | use std::sync::Arc; 19 | use std::thread::sleep; 20 | use tokio::sync::RwLock; 21 | 22 | lazy_static! { 23 | // <(src_chain, dst_chain), (src_chain_cost, dst_chain_cost)> // cost in USDT 24 | pub static ref FLAT_FEES: Arc>> = { 25 | let mut m = HashMap::new(); 26 | m.insert(("ethereum".to_string(), "ethereum".to_string()), (0, 0)); // 0$ 3$ 27 | m.insert(("solana".to_string(), "solana".to_string()), (0, 0)); // 0$ 0.2$ 28 | m.insert(("ethereum".to_string(), "solana".to_string()), (0, 0)); // 1$ 0.1$ 29 | m.insert(("solana".to_string(), "ethereum".to_string()), (0, 0)); // 0.1$ 2$ 30 | Arc::new(RwLock::new(m)) 31 | }; 32 | 33 | // 34 | pub static ref MANTIS_TOKENS: Arc>> = { 35 | let mut m = HashMap::new(); 36 | m.insert("CpHLZarS6tobQTDQSKtnXCQWd1YcfSDL7UMgmjcVNjTb".to_string(), "7BgBvyjrZX1YKz4oh9mjb8ZScatkkwb8DzFx7LoiVkM3".to_string()); // SLERF (test, not the IBC-SLERF) 37 | m.insert("9fJw9rQdMi8QEJnBsybVKU7XTXBUTXVKpinDaYMsVSUS".to_string(), "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB".to_string()); // USDT (test, not the IBC-USDT) 38 | Arc::new(RwLock::new(m)) 39 | }; 40 | } 41 | 42 | pub async fn get_simulate_swap_intent( 43 | intent_info: &PostIntentInfo, 44 | mut src_chain: &str, 45 | mut dst_chain: &str, 46 | bridge_token: &String, 47 | ) -> String { 48 | // Extracting values from OperationInput 49 | let (mut token_in, amount_in, src_chain_user) = match &intent_info.inputs { 50 | OperationInput::SwapTransfer(input) => ( 51 | input.token_in.clone(), 52 | input.amount_in.clone(), 53 | input.src_chain_user.clone(), 54 | ), 55 | OperationInput::Lend(_) => todo!(), 56 | OperationInput::Borrow(_) => todo!(), 57 | }; 58 | 59 | let (dst_chain_user, mut token_out, _) = match &intent_info.outputs { 60 | OperationOutput::SwapTransfer(output) => ( 61 | output.dst_chain_user.clone(), 62 | output.token_out.clone(), 63 | output.amount_out.clone(), 64 | ), 65 | OperationOutput::Lend(_) => todo!(), 66 | OperationOutput::Borrow(_) => todo!(), 67 | }; 68 | 69 | if token_out == system_program::ID.to_string() { 70 | sleep(tokio::time::Duration::from_secs(2)); 71 | return String::from("0"); 72 | } 73 | 74 | if src_chain == "mantis" { 75 | let tokens = MANTIS_TOKENS.read().await; 76 | 77 | if let Some(token_mantis) = tokens.get(&token_in) { 78 | token_in = token_mantis.clone(); 79 | } else { 80 | println!("Token {token_in} not supported, please include it on MANTIS_TOKENS"); 81 | } 82 | 83 | src_chain = "solana"; 84 | } 85 | 86 | if dst_chain == "mantis" { 87 | let tokens = MANTIS_TOKENS.read().await; 88 | 89 | if let Some(token_mantis) = tokens.get(&token_out) { 90 | token_out = token_mantis.clone(); 91 | } else { 92 | println!("Token {token_out} not supported, please include it on MANTIS_TOKENS"); 93 | } 94 | 95 | dst_chain = "solana"; 96 | } 97 | 98 | let (bridge_token_address_src, _) = get_token_info(bridge_token, src_chain).unwrap(); 99 | let mut amount_out_src_chain = BigInt::from_str(&amount_in).unwrap(); 100 | 101 | if !bridge_token_address_src.eq_ignore_ascii_case(&token_in) { 102 | // simulate token_in -> USDT 103 | if src_chain == "ethereum" { 104 | amount_out_src_chain = 105 | ethereum_simulate_swap(&token_in, &amount_in, bridge_token_address_src).await; 106 | } else if src_chain == "solana" || src_chain == "mantis" { 107 | if src_chain == "mantis" { 108 | let tokens = MANTIS_TOKENS.read().await; 109 | 110 | match tokens.get(&token_in) { 111 | Some(_token_in) => { 112 | token_in = _token_in.clone(); 113 | } 114 | None => { 115 | amount_out_src_chain = BigInt::zero(); 116 | eprintln!("Update MANTIS_TOKENS global variable "); 117 | } 118 | } 119 | } 120 | 121 | if !amount_out_src_chain.is_zero() { 122 | amount_out_src_chain = BigInt::from_str( 123 | &solana_simulate_swap( 124 | &src_chain_user, 125 | &token_in, 126 | &bridge_token_address_src, 127 | BigInt::from_str(&amount_in).unwrap().to_u64().unwrap(), 128 | ) 129 | .await, 130 | ) 131 | .unwrap(); 132 | } 133 | } 134 | } 135 | 136 | let (bridge_token_address_dst, _) = get_token_info(bridge_token, dst_chain).unwrap(); 137 | 138 | // get flat fees 139 | let flat_fees; 140 | { 141 | let fees = FLAT_FEES.read().await; 142 | flat_fees = fees 143 | .get(&(src_chain.to_string(), dst_chain.to_string())) 144 | .unwrap() 145 | .clone(); 146 | drop(fees); 147 | } 148 | 149 | // get comission 150 | let comission = env::var("COMISSION") 151 | .expect("COMISSION must be set") 152 | .parse::() 153 | .unwrap(); 154 | 155 | if amount_out_src_chain 156 | < BigInt::from(flat_fees.0 + flat_fees.1 + (&amount_out_src_chain * comission) / 100) 157 | { 158 | return String::from("0"); 159 | } 160 | 161 | // we substract the flat fees and the solver comission in USD 162 | let amount_in_dst_chain = amount_out_src_chain.clone() 163 | - (BigInt::from(flat_fees.0) 164 | + BigInt::from(flat_fees.1) 165 | + (amount_out_src_chain * BigInt::from(comission) / BigInt::from(100_000))); 166 | 167 | let mut final_amount_out = amount_in_dst_chain.to_string(); 168 | 169 | if !amount_in_dst_chain.is_zero() && !bridge_token_address_dst.eq_ignore_ascii_case(&token_out) 170 | { 171 | // simulate USDT -> token_out 172 | if dst_chain == "ethereum" { 173 | final_amount_out = 174 | ethereum_simulate_swap(bridge_token_address_dst, &final_amount_out, &token_out) 175 | .await 176 | .to_string(); 177 | } else if dst_chain == "solana" || dst_chain == "mantis" { 178 | if src_chain == "mantis" { 179 | let tokens = MANTIS_TOKENS.read().await; 180 | 181 | match tokens.get(&token_out) { 182 | Some(_token_out) => { 183 | token_out = _token_out.clone(); 184 | } 185 | None => { 186 | final_amount_out = "0".to_string(); 187 | eprintln!("Update MANTIS_TOKENS global variable "); 188 | } 189 | } 190 | } 191 | 192 | if final_amount_out != "0" { 193 | final_amount_out = solana_simulate_swap( 194 | &dst_chain_user, 195 | bridge_token_address_dst, 196 | &token_out, 197 | amount_in_dst_chain.to_u64().unwrap(), 198 | ) 199 | .await; 200 | } 201 | } 202 | } 203 | 204 | final_amount_out 205 | } 206 | 207 | // Calculation ethereum gas fees 208 | // let url = "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd"; 209 | // let response: Value = reqwest::get(url).await.unwrap().json().await.unwrap(); 210 | // let eth_price = response["ethereum"]["usd"].as_f64().unwrap().round(); 211 | // let gas_price = provider.get_gas_price().await.unwrap().as_u128() as f64; 212 | // let gas = 295000f64; 213 | // let flat_fees = (eth_price * ((gas * gas_price) / 1e18)) as f64; 214 | 215 | // let profit = (amount_out_src_chain.to_f64().unwrap() / 10f64.powi(bridge_token_dec_src as i32)) 216 | // - (amount_in_dst_chain.to_f64().unwrap() / 10f64.powi(bridge_token_dec_dst as i32)); 217 | -------------------------------------------------------------------------------- /example_solver/src/routers/paraswap.rs: -------------------------------------------------------------------------------- 1 | pub mod paraswap_router { 2 | use ethers::prelude::Address; 3 | use num_bigint::BigInt; 4 | use reqwest::Client; 5 | use serde_json::Value; 6 | use std::str::FromStr; 7 | 8 | #[derive(Debug)] 9 | pub struct ParaswapParams { 10 | pub side: String, 11 | pub chain_id: u16, 12 | pub amount_in: BigInt, 13 | pub token_in: Address, 14 | pub token_out: Address, 15 | pub token0_decimals: u32, 16 | pub token1_decimals: u32, 17 | pub wallet_address: Address, 18 | pub receiver_address: Address, 19 | pub client_aggregator: Client, 20 | } 21 | 22 | pub async fn simulate_swap_paraswap( 23 | params: ParaswapParams, 24 | ) -> Result<(BigInt, String, Address), String> { 25 | let mut res_amount = BigInt::from(0); 26 | let mut res_data = String::from("None"); 27 | let mut res_to = Address::zero(); 28 | 29 | let url = format!("https://apiv5.paraswap.io/prices?srcToken=0x{:x}&srcDecimals={}&destToken=0x{:x}&destDecimals={}&amount={}&side={}&network={}&maxImpact=10", 30 | params.token_in, params.token0_decimals, params.token_out, params.token1_decimals, params.amount_in, params.side, params.chain_id); 31 | 32 | let res = params 33 | .client_aggregator 34 | .get(url) 35 | .send() 36 | .await 37 | .map_err(|err| err.to_string())?; 38 | 39 | let body = res.text().await.map_err(|err| err.to_string())?; 40 | let json_value = serde_json::from_str::(&body).map_err(|err| err.to_string())?; 41 | 42 | match json_value.get("priceRoute") { 43 | Some(json) => { 44 | let (amount_in, amount_out, mode) = if params.side == "SELL".to_string() { 45 | (params.amount_in.clone(), BigInt::from(1), "destAmount") 46 | } else { 47 | let agg_amount = json 48 | .get("srcAmount") 49 | .ok_or("Failed to get destination srcAmount")? 50 | .to_string() 51 | .trim_matches('"') 52 | .to_string(); 53 | ( 54 | BigInt::from_str(&agg_amount).unwrap() * BigInt::from(2), 55 | params.amount_in.clone(), 56 | "srcAmount", 57 | ) 58 | }; 59 | 60 | let dest_amount = json.get(mode).ok_or("Failed to get destination amount")?; 61 | 62 | res_amount = 63 | BigInt::from_str(&format!("{}", dest_amount.to_string().trim_matches('"'))) 64 | .map_err(|err| err.to_string())?; 65 | 66 | let url = format!( 67 | "https://apiv5.paraswap.io/transactions/{}?gasPrice=50000000000&ignoreChecks=true&ignoreGasEstimate=true&onlyParams=false", params.chain_id 68 | ); 69 | 70 | let body_0 = serde_json::json!({ 71 | "srcToken": format!("0x{:x}", params.token_in), 72 | "destToken": format!("0x{:x}", params.token_out), 73 | "srcAmount": format!("{}", amount_in), 74 | "destAmount": format!("{}", amount_out), 75 | "priceRoute": json, 76 | "userAddress": format!("0x{:x}", params.wallet_address), 77 | "txOrigin": format!("0x{:x}", params.receiver_address), 78 | //"receiver": format!("0x{:x}", *MY_SC), 79 | "partner": "paraswap.io", 80 | "srcDecimals": params.token0_decimals, 81 | "destDecimals": params.token1_decimals 82 | }); 83 | 84 | let res = params 85 | .client_aggregator 86 | .post(url) 87 | .json(&body_0) 88 | .send() 89 | .await 90 | .map_err(|err| err.to_string())?; 91 | 92 | let body = res.text().await.map_err(|err| err.to_string())?; 93 | 94 | let json_value = serde_json::from_str::(&body) 95 | .map_err(|err| err.to_string())?; 96 | 97 | match json_value.get("to") { 98 | Some(address) => { 99 | res_to = 100 | Address::from_str(address.as_str().ok_or("Failed to get address")?) 101 | .map_err(|err| err.to_string())?; 102 | let data = json_value.get("data").ok_or("Failed to get data")?; 103 | res_data = format!("{}", data.to_string().trim_matches('"')); 104 | } 105 | None => { 106 | println!( 107 | "Failed getting calldata in Paraswap (weird): {:#}", 108 | json_value 109 | ); 110 | } 111 | } 112 | } 113 | None => { 114 | println!( 115 | "Failed getting price in Paraswap (maybe token doesn't exist): {:#}", 116 | body 117 | ); 118 | } 119 | } 120 | 121 | Ok((res_amount, res_data, res_to)) 122 | } 123 | } 124 | --------------------------------------------------------------------------------