diff --git a/protocol-testing/src/abi/ERC20.json b/protocol-testing/src/abi/ERC20.json new file mode 100644 index 0000000..71c1ae7 --- /dev/null +++ b/protocol-testing/src/abi/ERC20.json @@ -0,0 +1,19 @@ +[ + { + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/protocol-testing/src/config.rs b/protocol-testing/src/config.rs new file mode 100644 index 0000000..2580f3d --- /dev/null +++ b/protocol-testing/src/config.rs @@ -0,0 +1,71 @@ +use std::collections::HashMap; + +use figment::providers::Format; +use hex::FromHex; +use serde::{Deserialize, Serialize}; + +/// Represents a hexadecimal byte string: Check if we already have a default impl for this +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(from = "String", into = "String")] +pub struct HexBytes(Vec); + +impl From for HexBytes { + fn from(s: String) -> Self { + let s = s + .trim_start_matches("0x") + .to_lowercase(); + HexBytes(Vec::from_hex(s).unwrap_or_default()) + } +} + +impl Into for HexBytes { + fn into(self) -> String { + format!("0x{}", hex::encode(self.0)) + } +} + +/// Represents a ProtocolComponent with its main attributes +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ProtocolComponentExpectation { + pub id: String, + pub tokens: Vec, + #[serde(default)] + pub static_attributes: HashMap, + pub creation_tx: HexBytes, +} + +/// Represents a ProtocolComponent with test configuration +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ProtocolComponentWithTestConfig { + #[serde(flatten)] + pub base: ProtocolComponentExpectation, + #[serde(default = "default_false")] + pub skip_simulation: bool, +} + +fn default_false() -> bool { + false +} + +/// Configuration for an individual test +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct IntegrationTest { + pub name: String, + pub start_block: u64, + pub stop_block: u64, + pub initialized_accounts: Option>, + pub expected_components: Vec, +} + +/// Main integration test configuration +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct IntegrationTestsConfig { + pub substreams_yaml_path: String, + pub adapter_contract: String, + pub adapter_build_signature: Option, + pub adapter_build_args: Option, + pub initialized_accounts: Option>, + pub skip_balance_check: bool, + pub protocol_type_names: Vec, + pub tests: Vec, +} diff --git a/protocol-testing/src/rpc.rs b/protocol-testing/src/rpc.rs new file mode 100644 index 0000000..837af36 --- /dev/null +++ b/protocol-testing/src/rpc.rs @@ -0,0 +1,118 @@ +use alloy::{ + contract::{ContractInstance, Interface}, + dyn_abi::DynSolValue, + eips::eip1898::BlockId, + primitives::{address, Address, U256}, + providers::{Provider, ProviderBuilder}, + transports::http::reqwest::Url, +}; + +const NATIVE_ALIASES: &[Address] = &[ + address!("0x0000000000000000000000000000000000000000"), + address!("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), +]; + +const ERC_20_ABI: &str = r#"[{"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"}]"#; + +struct RPCProvider { + url: Url, +} + +impl RPCProvider { + fn new(url: String) -> RPCProvider { + let url = url.as_str().parse().unwrap(); + RPCProvider { url } + } + + async fn get_token_balance( + self, + token_address: Address, + wallet_address: Address, + block_number: u64, + ) -> U256 { + let provider = ProviderBuilder::new().on_http(self.url); + let block_id: BlockId = BlockId::from(block_number); + + match NATIVE_ALIASES.contains(&token_address) { + true => provider + .get_balance(wallet_address) + .block_id(block_id) + .await + .expect("Failed to fetch token balance"), + false => { + let abi = serde_json::from_str(ERC_20_ABI).expect("invalid ABI"); + + let contract = ContractInstance::new(token_address, provider, Interface::new(abi)); + + let wallet_addr = DynSolValue::from(wallet_address); + + let result_value = contract + .function("balanceOf", &[wallet_addr]) + .expect("Failed to build function call") + .block(block_id) + .call() + .await + .expect("Failed to fetch ERC-20 Balance"); + let result: U256 = result_value + .first() + .unwrap() + .as_uint() + .unwrap() + .0; + result + } + } + } + + async fn get_block_header(self, block_number: u64) { + // TODO: Implement + // let provider = ProviderBuilder::new().on_http(self.url); + // let block_id: BlockId = BlockId::from(block_number); + // + // let block = provider.get_block(block_id) + } +} + +#[cfg(test)] +mod tests { + use std::{env, str::FromStr}; + + use alloy::primitives::address; + + use super::*; + + #[tokio::test] + async fn get_token_balance_native_token() { + let eth_rpc_url = env::var("ETH_RPC_URL").expect("Missing ETH_RPC_URL in environment"); + + let rpc_provider = RPCProvider::new(eth_rpc_url); + let token_address = address!("0x0000000000000000000000000000000000000000"); + let wallet_address = address!("0x787B8840100d9BaAdD7463f4a73b5BA73B00C6cA"); + let block_number = 21998530; + + let balance = rpc_provider + .get_token_balance(token_address, wallet_address, block_number) + .await; + + assert_eq!( + balance, + U256::from_str("1070041574684539264153").expect("Failed to convert ETH value") + ); + } + + #[tokio::test] + async fn get_token_balance_erc20_token() { + let eth_rpc_url = env::var("ETH_RPC_URL").expect("Missing ETH_RPC_URL in environment"); + + let rpc_provider = RPCProvider::new(eth_rpc_url); + let token_address = address!("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"); + let wallet_address = address!("0x787B8840100d9BaAdD7463f4a73b5BA73B00C6cA"); + let block_number = 21998530; + + let balance = rpc_provider + .get_token_balance(token_address, wallet_address, block_number) + .await; + + assert_eq!(balance, U256::from(717250938432_u64)); + } +}