diff --git a/programs/soroswap/Cargo.toml b/programs/soroswap/Cargo.toml index 50b98ea..1166e2b 100644 --- a/programs/soroswap/Cargo.toml +++ b/programs/soroswap/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] zephyr-sdk = { version = "0.2.1" } stellar-strkey = "0.0.8" +thiserror = "1.0" soroban-sdk = { version = "21.5.0" } [lib] diff --git a/programs/soroswap/src/lib.rs b/programs/soroswap/src/lib.rs index a49810c..f491329 100644 --- a/programs/soroswap/src/lib.rs +++ b/programs/soroswap/src/lib.rs @@ -1,10 +1,10 @@ - - use zephyr_sdk::{prelude::*, soroban_sdk::{xdr::{ScVal, Hash }, String as SorobanString, Address}, PrettyContractEvent, EnvClient, DatabaseDerive}; +use std::env; pub mod router; pub mod factory; pub mod pairs; +pub mod providers; #[derive(DatabaseDerive, Clone)] #[with_name("ssw_rt_ev")] @@ -38,6 +38,43 @@ struct ReservesChangeTable { timestamp: ScVal, } +#[derive(DatabaseDerive, Clone)] +#[with_name("ssw_providers")] +pub struct ProvidersTable { + provider_address: ScVal, + pool_address: ScVal, + shares: ScVal, + share_percentage: f64, + token_a_amount: ScVal, + token_b_amount: ScVal, + timestamp: ScVal, + action: String, + tx_hash: ScVal, +} + +impl ProvidersTable { + pub fn put(&self, env: &EnvClient) { + let default_limits = soroban_sdk::xdr::Limits::none(); + + env.db_write( + "ssw_providers", + &[ + "provider_address", "pool_address", "shares", "share_percentage", "token_a_amount", "token_b_amount", "timestamp", "action", "tx_hash", + ], + &[ + &self.provider_address.to_xdr(default_limits.clone()).unwrap_or_default(), + &self.pool_address.to_xdr(default_limits.clone()).unwrap_or_default(), + &self.shares.to_xdr(default_limits.clone()).unwrap_or_default(), + &self.share_percentage.to_le_bytes(), + &self.token_a_amount.to_xdr(default_limits.clone()).unwrap_or_default(), + &self.token_b_amount.to_xdr(default_limits.clone()).unwrap_or_default(), + &self.timestamp.to_xdr(default_limits.clone()).unwrap_or_default(), + &self.action.as_bytes(), + &self.tx_hash.to_xdr(default_limits.clone()).unwrap_or_default(), + ], + ).unwrap(); + } +} #[test] fn test() { let factory_address_str: &'static str = env!("SOROSWAP_FACTORY"); @@ -50,22 +87,44 @@ fn test() { pub extern "C" fn on_close() { let env = EnvClient::new(); - let factory_address_str: &'static str = env!("SOROSWAP_FACTORY"); - let router_address_str: &'static str = env!("SOROSWAP_ROUTER"); - - env.log().debug(format!("Indexing SSW Factory: {:?}", &factory_address_str), None); - env.log().debug(format!("Indexing SSW Router: {:?}", &router_address_str), None); - - let factory_address_bytes: [u8; 32]=stellar_strkey::Contract::from_string(factory_address_str).unwrap().0; - let router_address_bytes: [u8; 32]=stellar_strkey::Contract::from_string(router_address_str).unwrap().0; - - let contract_events_with_txhash: Vec<(PrettyContractEvent, [u8; 32])> = env.reader().pretty().soroban_events_and_txhash(); - - let filtered_router_events_with_txhash: Vec<(PrettyContractEvent, [u8; 32])> = contract_events_with_txhash.clone().into_iter() - .filter(|(event, _txhash)| event.raw.contract_id == Some(Hash(router_address_bytes))).collect(); - - let filtered_factory_events_with_txhash: Vec<(PrettyContractEvent, [u8; 32])> = contract_events_with_txhash.clone().into_iter() - .filter(|(event, _txhash)| event.raw.contract_id == Some(Hash(factory_address_bytes))).collect(); + let factory_address_str = + env::var("SOROSWAP_FACTORY").expect("SOROSWAP_FACTORY environment variable not set"); + let router_address_str = + env::var("SOROSWAP_ROUTER").expect("SOROSWAP_ROUTER environment variable not set"); + + env.log().debug( + format!("Indexing SSW Factory: {:?}", &factory_address_str), + None, + ); + env.log().debug( + format!("Indexing SSW Router: {:?}", &router_address_str), + None, + ); + + let factory_address_bytes: [u8; 32] = + stellar_strkey::Contract::from_string(&factory_address_str) + .unwrap() + .0; + let router_address_bytes: [u8; 32] = stellar_strkey::Contract::from_string(&router_address_str) + .unwrap() + .0; + + let contract_events_with_txhash: Vec<(PrettyContractEvent, [u8; 32])> = + env.reader().pretty().soroban_events_and_txhash(); + + let filtered_router_events_with_txhash: Vec<(PrettyContractEvent, [u8; 32])> = + contract_events_with_txhash + .clone() + .into_iter() + .filter(|(event, _txhash)| event.raw.contract_id == Some(Hash(router_address_bytes))) + .collect(); + + let filtered_factory_events_with_txhash: Vec<(PrettyContractEvent, [u8; 32])> = + contract_events_with_txhash + .clone() + .into_iter() + .filter(|(event, _txhash)| event.raw.contract_id == Some(Hash(factory_address_bytes))) + .collect(); //handle the events from the router contract router::events::handle_contract_events(&env, filtered_router_events_with_txhash); @@ -75,35 +134,45 @@ pub extern "C" fn on_close() { // At this point our PairsTable is up to date. Now we can handle the events from the Pairs let pairs_rows = env.read::(); - - env.log().debug(format!("Before pairs_rows iteration"), None); - for pair in pairs_rows { + env.log() + .debug(format!("Before pairs_rows iteration"), None); + for pair in pairs_rows { let pair_address: Address = env.from_scval(&pair.address); - env.log().debug(format!("Checking pair: {:?}", &pair_address), None); - - + env.log() + .debug(format!("Checking pair: {:?}", &pair_address), None); // We filter to see if there is any event related to our Pair - let pair_contract_events: Vec<(PrettyContractEvent, [u8; 32])> = contract_events_with_txhash.clone().into_iter() - .filter(|(event, _txhash)| { - let contract_id_str = SorobanString::from_str(&env.soroban(), &stellar_strkey::Contract(event.raw.contract_id.as_ref().unwrap().0).to_string()); - contract_id_str == pair_address.to_string() - }) - .collect(); - - env.log().debug(format!("We identified a number of events: {:?}", &pair_contract_events.len()), None); - env.log().debug(format!("pair_contract_events: {:?}", &pair_contract_events), None); - + let pair_contract_events: Vec<(PrettyContractEvent, [u8; 32])> = + contract_events_with_txhash + .clone() + .into_iter() + .filter(|(event, _txhash)| { + let contract_id_str = SorobanString::from_str( + &env.soroban(), + &stellar_strkey::Contract(event.raw.contract_id.as_ref().unwrap().0) + .to_string(), + ); + contract_id_str == pair_address.to_string() + }) + .collect(); + + env.log().debug( + format!( + "We identified a number of events: {:?}", + &pair_contract_events.len() + ), + None, + ); + env.log().debug( + format!("pair_contract_events: {:?}", &pair_contract_events), + None, + ); + // Handle the Event pairs::events::handle_contract_events(&env, pair_contract_events, pair); - } env.log().debug(format!("After pairs_rows iteration"), None); - - -} - - +} \ No newline at end of file diff --git a/programs/soroswap/src/providers/events.rs b/programs/soroswap/src/providers/events.rs new file mode 100644 index 0000000..f4f7b8e --- /dev/null +++ b/programs/soroswap/src/providers/events.rs @@ -0,0 +1,270 @@ +use zephyr_sdk::{ + soroban_sdk::{xdr::ScVal, Address, Symbol}, + PrettyContractEvent, EnvClient, +}; +use thiserror::Error; +use crate::ProvidersTable; +use crate::providers::types::{MintEvent, BurnEvent}; + +#[derive(Error, Debug)] +pub enum ProviderError { + #[error("Invalid liquidity value: {0}")] + InvalidLiquidity(String), + #[error("Invalid total supply: {0}")] + InvalidTotalSupply(String), + #[error("Failed to convert address: {0}")] + AddressConversionError(String), + #[error("Failed to process event data: {0}")] + EventDataError(String), + #[error("Failed to process action: {0}")] + ActionProcessingError(String), +} + +#[allow(dead_code)] +type Result = std::result::Result; + +/// Validates and processes provider data +#[allow(dead_code)] +fn validate_provider_data(liquidity: i128, total_supply: i128) -> Result { + if liquidity < 0 { + return Err(ProviderError::InvalidLiquidity( + "Liquidity cannot be negative".to_string(), + )); + } + + if total_supply <= 0 { + return Err(ProviderError::InvalidTotalSupply( + "Total supply must be positive".to_string(), + )); + } + + let share_percentage = (liquidity as f64 / total_supply as f64) * 100.0; + Ok(share_percentage) +} + +#[allow(dead_code)] +pub(crate) fn get_provider_from_mint( + env: &EnvClient, + data: &ScVal, + address: ScVal, + pool_address: ScVal, + total_supply: i128, + tx_hash: [u8; 32], +) -> Result { + let values: Result = Ok(env.from_scval::(data)); + let values = match values { + Ok(v) => v, + Err(e) => return Err(ProviderError::EventDataError(format!("Failed to deserialize mint event: {}", e))), + }; + + let share_percentage = validate_provider_data(values.liquidity, total_supply)?; + + let provider = ProvidersTable { + provider_address: address, + pool_address, + shares: env.to_scval(values.liquidity), + share_percentage, + token_a_amount: env.to_scval(values.amount_0), + token_b_amount: env.to_scval(values.amount_1), + timestamp: env.to_scval(env.reader().ledger_timestamp()), + action: "MINT".to_string(), + tx_hash: env.to_scval(tx_hash.to_vec()), + }; + + Ok(provider) +} + +#[allow(dead_code)] +pub(crate) fn get_provider_from_burn( + env: &EnvClient, + data: &ScVal, + address: ScVal, + pool_address: ScVal, + total_supply: i128, + tx_hash: [u8; 32], +) -> Result { + let values: Result = Ok(env.from_scval::(data)); + let values = match values { + Ok(v) => v, + Err(e) => return Err(ProviderError::EventDataError(format!("Failed to deserialize burn event: {}", e))), + }; + + let share_percentage = validate_provider_data(values.liquidity, total_supply)?; + + let provider = ProvidersTable { + provider_address: address, + pool_address, + shares: env.to_scval(values.liquidity), + share_percentage, + token_a_amount: env.to_scval(values.amount_0), + token_b_amount: env.to_scval(values.amount_1), + timestamp: env.to_scval(env.reader().ledger_timestamp()), + action: "BURN".to_string(), + tx_hash: env.to_scval(tx_hash.to_vec()), + }; + + Ok(provider) +} + +#[allow(dead_code)] +pub(crate) fn handle_contract_events( + env: &EnvClient, + contract_events: Vec<(PrettyContractEvent, [u8; 32])>, + pool_address: ScVal, + total_supply: i128, +) { + for (event, tx_hash) in contract_events { + match process_event(env, event, &pool_address, total_supply, tx_hash) { + Ok(_) => { + env.log().debug("Successfully processed event", None); + } + Err(e) => { + env.log().error( + format!("Failed to process event: {}", e), + Some(format!("{:?}", e).into()), + ); + continue; + } + } + } +} + +#[allow(dead_code)] +fn process_event( + env: &EnvClient, + event: PrettyContractEvent, + pool_address: &ScVal, + total_supply: i128, + tx_hash: [u8; 32], +) -> Result<()> { + let action = env + .try_from_scval::(&event.topics[1]) + .map_err(|e| ProviderError::ActionProcessingError(format!("Invalid action: {}", e)))?; + + let action_str = action.to_string(); // Convert Symbol to String + + let provider_address = match event.topics.get(2) { + Some(address_scval) => env + .try_from_scval::
(address_scval) + .map_err(|e| ProviderError::AddressConversionError(format!("Invalid address: {}", e)))?, + None => { + return Err(ProviderError::AddressConversionError( + "Missing provider address".to_string(), + )) + } + }; + + let data = &event.data; + let provider_scval = env.to_scval(provider_address); + + match action_str.as_str() { + "mint" => { + let provider_table = get_provider_from_mint( + env, + data, + provider_scval, + pool_address.clone(), + total_supply, + tx_hash, + )?; + provider_table.put(env); + + env.log().debug("Provider mint event recorded", None); + } + "burn" => { + let provider_table = get_provider_from_burn( + env, + data, + provider_scval, + pool_address.clone(), + total_supply, + tx_hash, + )?; + provider_table.put(env); + env.log().debug("Provider burn event recorded", None); + } + _ => { + env.log().debug(format!("Unhandled action: {:?}", action), None); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use zephyr_sdk::EnvClient; + use soroban_sdk::{xdr::ScVal, Address, Bytes}; + + #[test] + fn test_get_provider_from_mint() { + let env = EnvClient::new(); + + // Manually create the MintEvent data as a ScVal + let data = env.to_scval(MintEvent { + liquidity: 100, + amount_0: 50, + amount_1: 50, + }); + + // Use from_string with a reference to a String + let address_str = String::from("GAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"); + let address = Address::from_string(&address_str); + let pool_address = Address::from_string(&address_str); + + // Convert addresses to ScVal + let address_scval = env.to_scval(address); + let pool_address_scval = env.to_scval(pool_address); + + let total_supply = 1000; + let tx_hash: [u8; 32] = [0; 32]; + + // Call the function being tested + let result = get_provider_from_mint( + &env, + &data, + address_scval, + pool_address_scval, + total_supply, + tx_hash + ); + + // Assertions + assert!(result.is_ok()); + + let provider = result.unwrap(); + assert_eq!(provider.provider_address, address_scval); + assert_eq!(provider.pool_address, pool_address_scval); + assert_eq!(provider.shares, env.to_scval(100)); + assert_eq!(provider.share_percentage, 10.0); + assert_eq!(provider.token_a_amount, env.to_scval(50)); + assert_eq!(provider.token_b_amount, env.to_scval(50)); + assert_eq!(provider.action, "MINT"); + assert_eq!(provider.tx_hash, env.to_scval(tx_hash.to_vec())); + } + + #[test] + fn test_get_provider_from_burn() { + let env = EnvClient::new(); + let data = ScVal::from(r#"{"liquidity": 100, "amount_0": 50, "amount_1": 50}"#).unwrap(); + let address = env.to_scval(Address::from_string(String::from("GAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")).unwrap()); + let pool_address = env.to_scval(Address::from_string(String::from("GAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")).unwrap()); + let total_supply = 1000; + let tx_hash: [u8; 32] = [0; 32]; + + let result = get_provider_from_burn(&env, &data, address, pool_address, total_supply, tx_hash); + assert!(result.is_ok()); + + let provider = result.unwrap(); + assert_eq!(provider.provider_address, address); + assert_eq!(provider.pool_address, pool_address); + assert_eq!(provider.shares, env.to_scval(100)); + assert_eq!(provider.share_percentage, 10.0); + assert_eq!(provider.token_a_amount, env.to_scval(50)); + assert_eq!(provider.token_b_amount, env.to_scval(50)); + assert_eq!(provider.timestamp, env.to_scval(env.reader().ledger_timestamp())); + assert_eq!(provider.action, "BURN"); + assert_eq!(provider.tx_hash, env.to_scval(tx_hash.to_vec())); + } +} \ No newline at end of file diff --git a/programs/soroswap/src/providers/mod.rs b/programs/soroswap/src/providers/mod.rs new file mode 100644 index 0000000..ac6f41a --- /dev/null +++ b/programs/soroswap/src/providers/mod.rs @@ -0,0 +1,2 @@ +pub mod events; +pub mod types; \ No newline at end of file diff --git a/programs/soroswap/src/providers/types.rs b/programs/soroswap/src/providers/types.rs new file mode 100644 index 0000000..15459c0 --- /dev/null +++ b/programs/soroswap/src/providers/types.rs @@ -0,0 +1,50 @@ +use soroban_sdk::contracttype; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MintEvent { + pub amount_0: i128, + pub amount_1: i128, + pub liquidity: i128, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BurnEvent { + pub amount_0: i128, + pub amount_1: i128, + pub liquidity: i128, +} + + +// use serde::{Deserialize, Serialize}; +// use soroban_sdk::{contracttype, Address}; + +// #[derive(Debug, Serialize, Deserialize, Clone)] +// #[contracttype] +// pub struct MintEvent { +// pub liquidity: i128, +// pub amount_0: i128, +// pub amount_1: i128, +// } + +// #[derive(Debug, Serialize, Deserialize, Clone)] +// #[contracttype] +// pub struct BurnEvent { +// pub liquidity: i128, +// pub amount_0: i128, +// pub amount_1: i128, +// } + +// #[derive(Debug, Clone)] +// pub struct ProvidersTable { +// pub provider_address: soroban_sdk::xdr::ScVal, +// pub pool_address: soroban_sdk::xdr::ScVal, +// pub shares: soroban_sdk::xdr::ScVal, +// pub share_percentage: f64, +// pub token_a_amount: soroban_sdk::xdr::ScVal, +// pub token_b_amount: soroban_sdk::xdr::ScVal, +// pub timestamp: soroban_sdk::xdr::ScVal, +// pub action: String, +// pub tx_hash: soroban_sdk::xdr::ScVal, +// } diff --git a/programs/soroswap/zephyr.toml b/programs/soroswap/zephyr.toml index 1f58269..d2b0d5c 100644 --- a/programs/soroswap/zephyr.toml +++ b/programs/soroswap/zephyr.toml @@ -63,27 +63,49 @@ col_type = "BYTEA" name = "reserve_b" col_type = "BYTEA" -#Soroswap Reserves changes table - [[tables]] name = "ssw_rs_ch" - [[tables.columns]] name = "address" col_type = "BYTEA" - [[tables.columns]] name = "reserve_a" col_type = "BYTEA" - [[tables.columns]] name = "reserve_b" col_type = "BYTEA" - [[tables.columns]] name = "timestamp" col_type = "BYTEA" +#Soroswap Reserves changes table - - +[[tables]] +name = "ssw_providers" +[[tables.columns]] +name = "provider_address" +col_type = "BYTEA" +[[tables.columns]] +name = "pool_address" +col_type = "BYTEA" +[[tables.columns]] +name = "shares" +col_type = "BYTEA" +[[tables.columns]] +name = "share_percentage" +col_type = "FLOAT" +[[tables.columns]] +name = "token_a_amount" +col_type = "BYTEA" +[[tables.columns]] +name = "token_b_amount" +col_type = "BYTEA" +[[tables.columns]] +name = "timestamp" +col_type = "BYTEA" +[[tables.columns]] +name = "action" +col_type = "TEXT" +[[tables.columns]] +name = "tx_hash" +col_type = "BYTEA" \ No newline at end of file diff --git a/public/testnet.contracts.json b/public/testnet.contracts.json index 3a40653..e23ad09 100644 --- a/public/testnet.contracts.json +++ b/public/testnet.contracts.json @@ -1,5 +1,5 @@ { "soroswap_factory": "CD7OQTUYT7J4BN6EQWK6XX5SVDWFLKKFZ3PJ7GUJGLFIJSV52ZIQBWLG", "soroswap_router": "CCPEDSWPNWJTRIODNYO3THDJ6RYTMMAMDCGAIORRISXKGYU632Y475IF", - "phoenix_factory": "CDWCZI2VZMXOKTLGLTIEFADMSV6FJPBYNDWQQRLDSRN6WPUX54JGCJGM" + "phoenix_factory": "CC2YZDV2T6PDX7ZY5JC4JNBFLMKAKYCKPJ6DJ6AFTDPJH2WS3EYI3HHO" }