diff --git a/Cargo.lock b/Cargo.lock index 6c9ba4c..c8ba58d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -49,6 +58,7 @@ dependencies = [ "alloy-network", "alloy-provider", "alloy-rpc-client", + "alloy-rpc-types", "alloy-serde", "alloy-transport", "alloy-transport-http", @@ -332,6 +342,18 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "alloy-rpc-types" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea9bf1abdd506f985a53533f5ac01296bcd6102c5e139bbc5d40bc468d2c916" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + [[package]] name = "alloy-rpc-types-eth" version = "0.5.4" @@ -1247,6 +1269,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -2260,12 +2288,41 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "reqwest" version = "0.12.12" @@ -2326,6 +2383,36 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "rstest" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", + "rustc_version 0.4.1", +] + +[[package]] +name = "rstest_macros" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version 0.4.1", + "syn 2.0.96", + "unicode-ident", +] + [[package]] name = "ruint" version = "1.12.4" @@ -3049,6 +3136,7 @@ dependencies = [ "lazy_static", "num-bigint", "num-traits", + "rstest", "serde", "serde_json", "thiserror 1.0.69", diff --git a/Cargo.toml b/Cargo.toml index 7f672b6..85e9b5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,11 +14,14 @@ serde_json = "1.0.135" thiserror = "1.0.69" tokio = { version = "1.38.0", features = ["full"] } -alloy = { version = "0.5.4", features = ["providers"], optional = true } +alloy = { version = "0.5.4", features = ["providers", "rpc-types-eth"], optional = true } alloy-sol-types = { version = "0.8.14", optional = true } alloy-primitives = { version = "0.8.9", optional = true } tycho-core = { git = "https://github.com/propeller-heads/tycho-indexer.git", package = "tycho-core", tag = "0.46.0" } +[dev-dependencies] +rstest = "0.24.0" + [features] default = ["evm"] evm = ["alloy", "alloy-sol-types", "alloy-primitives"] diff --git a/src/encoding/evm/approvals/protocol_approvals_manager.rs b/src/encoding/evm/approvals/protocol_approvals_manager.rs index fde5442..eaa976b 100644 --- a/src/encoding/evm/approvals/protocol_approvals_manager.rs +++ b/src/encoding/evm/approvals/protocol_approvals_manager.rs @@ -1,49 +1,98 @@ use std::{env, sync::Arc}; use alloy::{ - providers::{ProviderBuilder, RootProvider}, + providers::{Provider, ProviderBuilder, RootProvider}, + rpc::types::{TransactionInput, TransactionRequest}, transports::BoxTransport, }; -use alloy_primitives::Address; +use alloy_primitives::{Address, Bytes, TxKind, U256}; +use alloy_sol_types::SolValue; use dotenv::dotenv; +use tokio::runtime::Runtime; + +use crate::encoding::{errors::EncodingError, evm::utils::encode_input}; #[allow(dead_code)] pub struct ProtocolApprovalsManager { client: Arc>, + runtime: Runtime, } impl ProtocolApprovalsManager { pub fn new() -> Self { - Self { client: get_client() } + let runtime = Runtime::new().expect("Failed to create runtime"); + let client = runtime.block_on(get_client()); + Self { client, runtime } } - pub async fn approval_needed( + pub fn approval_needed( &self, - _token: Address, - _spender_address: Address, - _router_address: Address, - ) -> bool { - todo!() - // should be something like - // let allowance = self - // .client - // .call(token, "allowance(address,address)(uint256)", (router_address, - // spender_address)) .await; - // - // allowance == U256::ZERO // If allowance is 0, approval is needed + token: Address, + owner_address: Address, + spender_address: Address, + ) -> Result { + let args = (owner_address, spender_address); + let data = encode_input("allowance(address,address)", args.abi_encode()); + let tx = TransactionRequest { + to: Some(TxKind::from(token)), + input: TransactionInput { input: Some(Bytes::from(data)), data: None }, + ..Default::default() + }; + + let output = self + .runtime + .block_on(async { self.client.call(&tx).await }); + match output { + Ok(response) => { + let allowance: U256 = U256::abi_decode(&response, true).map_err(|_| { + EncodingError::FatalError("Failed to decode response for allowance".to_string()) + })?; + + Ok(allowance.is_zero()) + } + Err(err) => { + Err(EncodingError::RecoverableError(format!("Call failed with error: {:?}", err))) + } + } } } -pub fn get_client() -> Arc> { +pub async fn get_client() -> Arc> { dotenv().ok(); let eth_rpc_url = env::var("ETH_RPC_URL").expect("Missing ETH_RPC_URL in environment"); - let runtime = tokio::runtime::Handle::try_current() - .is_err() - .then(|| tokio::runtime::Runtime::new().unwrap()) - .unwrap(); - let client = runtime.block_on(async { - ProviderBuilder::new() - .on_builtin(ð_rpc_url) - .await - .unwrap() - }); + let client = ProviderBuilder::new() + .on_builtin(ð_rpc_url) + .await + .expect("Failed to build provider"); Arc::new(client) } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use rstest::rstest; + + use super::*; + #[rstest] + #[case::approval_not_needed( + "0xba12222222228d8ba445958a75a0704d566bf2c8", + "0x2c6a3cd97c6283b95ac8c5a4459ebb0d5fd404f4", + false + )] + #[case::approval_needed( + "0x2c6a3cd97c6283b95ac8c5a4459ebb0d5fd404f4", + "0xba12222222228d8ba445958a75a0704d566bf2c8", + true + )] + fn test_approval_needed(#[case] spender: &str, #[case] owner: &str, #[case] expected: bool) { + let manager = ProtocolApprovalsManager::new(); + + let token = Address::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(); + let spender = Address::from_str(spender).unwrap(); + let owner = Address::from_str(owner).unwrap(); + + let result = manager + .approval_needed(token, owner, spender) + .unwrap(); + assert_eq!(result, expected); + } +} diff --git a/src/encoding/evm/swap_encoder/encoders.rs b/src/encoding/evm/swap_encoder/encoders.rs index a397be3..da1b3aa 100644 --- a/src/encoding/evm/swap_encoder/encoders.rs +++ b/src/encoding/evm/swap_encoder/encoders.rs @@ -53,17 +53,10 @@ impl SwapEncoder for BalancerV2SwapEncoder { encoding_context: EncodingContext, ) -> Result, EncodingError> { let token_approvals_manager = ProtocolApprovalsManager::new(); - let runtime = tokio::runtime::Handle::try_current() - .is_err() - .then(|| tokio::runtime::Runtime::new().unwrap()) - .unwrap(); let token = bytes_to_address(&swap.token_in)?; let router_address = bytes_to_address(&encoding_context.address_for_approvals)?; - let approval_needed = runtime.block_on(async { - token_approvals_manager - .approval_needed(token, self.vault_address, router_address) - .await - }); + let approval_needed = + token_approvals_manager.approval_needed(token, router_address, self.vault_address)?; // should we return gas estimation here too?? if there is an approval needed, gas will be // higher. let args = (