diff --git a/bot/src/bot.rs b/bot/src/bot.rs index 0a82c02..4c74baa 100644 --- a/bot/src/bot.rs +++ b/bot/src/bot.rs @@ -7,36 +7,29 @@ use bigdecimal::{ num_bigint::{BigInt, ToBigInt}, BigDecimal, }; -use bson::{doc, Bson}; -use chrono::{Duration, Utc}; +use bson::doc; use futures::stream::{self, StreamExt}; -use futures::TryStreamExt; use mongodb::options::FindOneOptions; use starknet::accounts::ConnectedAccount; -use starknet::core::types::{BlockTag, FunctionCall}; use starknet::{ accounts::{Account, Call, SingleOwnerAccount}, - core::types::{BlockId, FieldElement}, + core::types::FieldElement, macros::selector, - providers::{jsonrpc::HttpTransport, JsonRpcClient, Provider}, + providers::{jsonrpc::HttpTransport, JsonRpcClient}, signers::LocalWallet, }; use starknet_id::encode; +use std::str::FromStr; use tokio::time::{sleep, Duration as TokioDuration}; use crate::logger::Logger; use crate::models::TxResult; -use crate::models::{ - AggregateResult, AggregateResults, DomainAggregateResult, MetadataDoc, Unzip5, -}; +use crate::models::{AggregateResult, AggregateResults, DomainAggregateResult, MetadataDoc}; +use crate::pipelines::{get_auto_renewal_altcoins_data, get_auto_renewal_data}; use crate::starknet_utils::check_pending_transactions; -use crate::starknet_utils::create_jsonrpc_client; -use crate::starknetid_utils::get_renewal_price; +use crate::starknetid_utils::{get_altcoin_quote, get_balances, get_renewal_price_eth}; use crate::utils::{from_uint256, hex_to_bigdecimal, to_uint256}; -use crate::{ - config::Config, - models::{AppState, Domain}, -}; +use crate::{config::Config, models::AppState}; lazy_static::lazy_static! { static ref RENEW_TIME: FieldElement = FieldElement::from_dec_str("365").unwrap(); @@ -46,149 +39,94 @@ pub async fn get_domains_ready_for_renewal( config: &Config, state: &Arc, logger: &Logger, -) -> Result { - let auto_renews_collection = state.db.collection::("auto_renew_flows"); - let min_expiry_date = Utc::now() + Duration::days(30); - println!("timestamp: {}", min_expiry_date.timestamp()); - // Define aggregate pipeline - let pipeline = vec![ - doc! { "$match": { "_cursor.to": null } }, - doc! { "$match": { "enabled": true } }, - doc! { "$lookup": { - "from": "domains", - "let": { "domain_name": "$domain" }, - "pipeline": [ - { "$match": - { "$expr": - { "$and": [ - { "$eq": [ "$domain", "$$domain_name" ] }, - { "$eq": [ { "$ifNull": [ "$_cursor.to", null ] }, null ] }, - ]} - } - }, - ], - "as": "domain_info", - }}, - doc! { "$unwind": "$domain_info" }, - doc! { "$match": { "domain_info.expiry": { "$lt": Bson::Int64(min_expiry_date.timestamp()) } } }, - doc! { "$lookup": { - "from": "auto_renew_approvals", - "let": { "renewer_addr": "$renewer_address" }, - "pipeline": [ - { "$match": - { "$expr": - { "$and": [ - { "$eq": [ "$renewer", "$$renewer_addr" ] }, - { "$eq": [ { "$ifNull": [ "$_cursor.to", null ] }, null ] }, - ]} - } - } - ], - "as": "approval_info", - }}, - doc! { "$unwind": { "path": "$approval_info", "preserveNullAndEmptyArrays": true } }, - doc! { "$group": { - "_id": "$domain_info.domain", - "expiry": { "$first": "$domain_info.expiry" }, - "renewer_address": { "$first": "$renewer_address" }, - "enabled": { "$first": "$enabled" }, - "approval_value": { "$first": { "$ifNull": [ "$approval_info.allowance", "0x0" ] } }, - "allowance": { "$first": "$allowance" }, - "last_renewal": { "$first": "$last_renewal" }, - "meta_hash": { "$first": "$meta_hash" }, - "_cursor": { "$first": "$_cursor" }, - }}, - doc! { "$project": { - "_id": 0, - "domain": "$_id", - "expiry": 1, - "renewer_address": 1, - "enabled": 1, - "approval_value": 1, - "allowance": 1, - "last_renewal": 1, - "meta_hash": 1, - "_cursor": 1, - }}, - ]; - - // Execute the pipeline - let cursor = auto_renews_collection.aggregate(pipeline, None).await?; - // Extract the results as Vec - let bson_docs: Vec = cursor.try_collect().await?; - // Convert each bson::Document into DomainAggregateResult - let results: Result, _> = bson_docs - .into_iter() - .map(|doc| bson::from_bson(bson::Bson::Document(doc))) - .collect(); - // Check if the conversion was successful - let results = results?; - - if results.is_empty() { - return Ok(AggregateResults { - domains: vec![], - renewers: vec![], - domain_prices: vec![], - tax_prices: vec![], - meta_hashes: vec![], - }); +) -> Result> { + let mut results = get_auto_renewal_data(config, state).await?; + let results_altcoins = get_auto_renewal_altcoins_data(config, state).await?; + + let mut grouped_results: HashMap = HashMap::new(); + + if results.is_empty() && results_altcoins.is_empty() { + grouped_results.insert( + config.contract.erc20, + AggregateResults { + domains: vec![], + renewers: vec![], + domain_prices: vec![], + tax_prices: vec![], + meta_hashes: vec![], + auto_renew_contracts: vec![], + }, + ); + return Ok(grouped_results); } + // merge all results together + results.extend(results_altcoins.iter().cloned()); + // Fetch balances for all renewers - let renewer_addresses: Vec = results + let renewer_and_erc20: Vec<(String, String)> = results .iter() - .map(|result| result.renewer_address.clone()) + .map(|result| (result.renewer_address.clone(), result.erc20_addr.clone())) .collect(); - // Batch calls to fetch balances to max 5000 addresses per call - let mut balances: Vec = vec![]; - let mut renewer_addresses_batch = renewer_addresses.clone(); - while !renewer_addresses_batch.is_empty() { - let size = renewer_addresses_batch.len().min(2500); - let batch = renewer_addresses_batch.drain(0..size).collect(); - let batch_balances = fetch_users_balances(config, batch).await; - // we skip the first 2 elements, the first one is the index of the call, the 2nd the length of the results - balances.extend(batch_balances.into_iter().skip(2)); - } - println!("balances at the end: {:?}", balances.len()); + + let balances = get_balances(config, renewer_and_erc20.clone()).await; if balances.is_empty() { - logger.severe(format!( - "Error while fetching balances for {} users", - renewer_addresses.len() - )); + logger.severe("Error while fetching balances for users".to_string()); return Err(anyhow!("Error while fetching balances")); } - let dynamic_balances = Arc::new(Mutex::new(HashMap::new())); + let dynamic_balances: Arc>>> = + Arc::new(Mutex::new(HashMap::new())); let mut balances_iter = balances.into_iter(); - for address in &renewer_addresses { + for (address, erc20) in &renewer_and_erc20 { balances_iter.next(); // we skip the first result as its value is 2 for low and high let balance = from_uint256( balances_iter.next().expect("Expected low not found"), balances_iter.next().expect("Expected high not found"), ); - dynamic_balances - .lock() - .unwrap() - .insert(address.to_owned(), balance); + let mut outer_map = dynamic_balances.lock().unwrap(); + let inner_map = outer_map + .entry(address.clone()) + .or_default(); + inner_map.insert(erc20.clone(), balance); } // Then process the results let results_stream = stream::iter(results.into_iter().enumerate()); let processed_results = results_stream .then(|(i, result)| { - let address = renewer_addresses.get(i).unwrap(); - let balance = dynamic_balances - .lock() - .unwrap() - .get(address) - .unwrap() - .to_owned(); - let renewal_price = get_renewal_price(result.domain.clone()); + let renewer_and_erc20_cloned = renewer_and_erc20.clone(); let dynamic_balances = Arc::clone(&dynamic_balances); async move { + let (address, erc20) = renewer_and_erc20_cloned.get(i).unwrap(); + let balance = dynamic_balances + .lock() + .unwrap() + .get(address) + .and_then(|erc20_balances| erc20_balances.get(erc20)) + .cloned() + .expect("Balance not found for this erc20"); + let renewal_price_eth = get_renewal_price_eth(result.domain.clone()); + let renewal_price = + if FieldElement::from_hex_be(erc20).unwrap() == config.contract.erc20 { + renewal_price_eth + } else { + match get_altcoin_quote(config, result.erc20_addr.clone()).await { + Ok(quote) => { + (quote * renewal_price_eth) + / BigInt::from_str("1000000000000000000").unwrap() + } + Err(e) => { + println!("Error while fetching quote: {:?}", e); + // this case can happen if the quote is not in the right range + // we return 0 and won't renew this domain + BigInt::from(0) + } + } + }; let output = process_aggregate_result( state, - result, + result.clone(), logger, BigDecimal::from(balance.to_owned()), BigDecimal::from(renewal_price.to_owned()), @@ -200,6 +138,8 @@ pub async fn get_domains_ready_for_renewal( dynamic_balances .lock() .unwrap() + .entry(result.erc20_addr) + .or_default() .insert(address.to_owned(), new_balance); }; @@ -210,73 +150,34 @@ pub async fn get_domains_ready_for_renewal( .await; let mut none_count = 0; - let (domains, renewers, domain_prices, tax_prices, meta_hashes): ( - Vec, - Vec, - Vec, - Vec, - Vec, - ) = processed_results - .into_iter() - .filter_map(|x| match x { - Some(res) => Some(res), - None => { - none_count += 1; - None - } - }) - .map(|res| { - ( - res.domain, - res.renewer_addr, - res.domain_price, - res.tax_price, - res.meta_hash, - ) - }) - .unzip5(); - logger.warning(format!("Domains that couldn't be renewed: {}", none_count)); - - Ok(AggregateResults { - domains, - renewers, - domain_prices, - tax_prices, - meta_hashes, - }) -} - -async fn fetch_users_balances( - config: &Config, - renewer_addresses: Vec, -) -> Vec { - let mut calls: Vec = vec![FieldElement::from(renewer_addresses.len())]; - for address in &renewer_addresses { - calls.push(config.contract.erc20); - calls.push(selector!("balanceOf")); - calls.push(FieldElement::ONE); - calls.push(FieldElement::from_hex_be(address).unwrap()); - } - - let provider = create_jsonrpc_client(&config); - let call_result = provider - .call( - FunctionCall { - contract_address: config.contract.multicall, - entry_point_selector: selector!("aggregate"), - calldata: calls, - }, - BlockId::Tag(BlockTag::Latest), - ) - .await; + for res_option in processed_results.into_iter() { + if let Some(res) = res_option { + // Fetch or initialize the AggregateResults for this key + let entry = grouped_results + .entry(res.auto_renew_contract) + .or_insert_with(|| AggregateResults { + domains: vec![], + renewers: vec![], + domain_prices: vec![], + tax_prices: vec![], + meta_hashes: vec![], + auto_renew_contracts: vec![], + }); - match call_result { - Ok(result) => result, - Err(err) => { - println!("Error while fetching balances: {:?}", err); - vec![] + // Append the current result to the vectors in AggregateResults + entry.domains.push(res.domain); + entry.renewers.push(res.renewer_addr); + entry.domain_prices.push(res.domain_price); + entry.tax_prices.push(res.tax_price); + entry.meta_hashes.push(res.meta_hash); + } else { + // Increment none_count if the result is None + none_count += 1; } } + logger.warning(format!("Domains that couldn't be renewed: {}", none_count)); + + Ok(grouped_results) } async fn process_aggregate_result( @@ -299,6 +200,11 @@ async fn process_aggregate_result( }; let allowance = hex_to_bigdecimal(&result.allowance.unwrap()).unwrap(); + // if renewal_price is 0, we don't renew the domain + if renewal_price == BigDecimal::from(0) { + return None; + } + // Check user meta hash let mut tax_price = BigDecimal::from(0); let mut meta_hash = FieldElement::ZERO; @@ -324,16 +230,16 @@ async fn process_aggregate_result( } let final_price = renewal_price.clone() + tax_price.clone(); - // Check user ETH allowance is greater or equal than final price = renew_price + tax_price + // Check user ERC20 allowance is greater or equal than final price = renew_price + tax_price if erc20_allowance >= final_price { // check user allowance is greater or equal than final price if allowance >= final_price { // check user balance is sufficiant if balance < final_price { - // logger.warning(format!( + // println!( // "Domain {} cannot be renewed because {} has not enough balance ({}) for domain price({})", // result.domain, result.renewer_address, balance, final_price - // )); + // ); return None; } @@ -357,19 +263,20 @@ async fn process_aggregate_result( domain_price: renewal_price, tax_price, meta_hash, + auto_renew_contract: result.auto_renew_contract, }) } else { - // logger.warning(format!( + // println!( // "Domain {} cannot be renewed because {} has set an allowance({}) lower than final price({})", // result.domain, result.renewer_address, allowance, final_price - // )); + // ); None } } else { - // logger.warning(format!( + // println!( // "Domain {} cannot be renewed because {} has set an erc20_allowance ({}) lower than domain price({}) + tax({})", // result.domain, result.renewer_address, erc20_allowance, renewal_price, tax_price - // )); + // ); None } } @@ -378,11 +285,13 @@ pub async fn renew_domains( config: &Config, account: &SingleOwnerAccount, LocalWallet>, mut aggregate_results: AggregateResults, + auto_renew_contract: &FieldElement, logger: &Logger, ) -> Result<()> { logger.info(format!( - "Renewing {} domains", - aggregate_results.domains.len() + "Renewing {} domains on autorenewal contract {}", + aggregate_results.domains.len(), + auto_renew_contract )); let mut nonce = account.get_nonce().await.unwrap(); let mut tx_results = Vec::::new(); @@ -404,14 +313,15 @@ pub async fn renew_domains( let meta_hashes: Vec = aggregate_results.meta_hashes.drain(0..size).collect(); match send_transaction( - config, account, + auto_renew_contract.to_owned(), AggregateResults { domains: domains_to_renew.clone(), renewers, domain_prices, tax_prices, meta_hashes, + auto_renew_contracts: vec![], }, nonce, ) @@ -488,8 +398,8 @@ pub async fn renew_domains( } pub async fn send_transaction( - config: &Config, account: &SingleOwnerAccount, LocalWallet>, + auto_renew_contract: FieldElement, aggregate_results: AggregateResults, nonce: FieldElement, ) -> Result { @@ -549,7 +459,7 @@ pub async fn send_transaction( let execution = account .execute(vec![Call { - to: config.contract.renewal, + to: auto_renew_contract, selector: selector!("batch_renew"), calldata: calldata.clone(), }]) diff --git a/bot/src/config.rs b/bot/src/config.rs index 8fd2b48..7a73711 100644 --- a/bot/src/config.rs +++ b/bot/src/config.rs @@ -60,6 +60,10 @@ pub_struct!(Clone, Deserialize; WatchtowerTypes { severe: String, }); +pub_struct!(Clone, Deserialize; Server { + starknetid_api: String, +}); + pub_struct!(Clone, Deserialize; Config { contract: Contract, database: Database, @@ -68,6 +72,7 @@ pub_struct!(Clone, Deserialize; Config { indexer_server: IndexerServer, rpc: Rpc, watchtower: Watchtower, + server: Server, }); pub fn load() -> Config { diff --git a/bot/src/main.rs b/bot/src/main.rs index 38229fb..87b7718 100644 --- a/bot/src/main.rs +++ b/bot/src/main.rs @@ -11,7 +11,6 @@ use starknet::{ providers::Provider, signers::{LocalWallet, SigningKey}, }; -use starknet_id::decode; use starknet_utils::create_jsonrpc_client; use tokio::time::sleep; @@ -23,6 +22,7 @@ mod bot; mod config; mod logger; mod models; +mod pipelines; mod sales_tax; mod starknet_utils; mod starknetid_utils; @@ -161,30 +161,31 @@ async fn main() { println!("[bot] Checking domains to renew"); match bot::get_domains_ready_for_renewal(&conf, &shared_state, &logger).await { Ok(aggregate_results) => { - //println!("[bot] checking domains to renew today"); - if !aggregate_results.domains.is_empty() { - match renew_domains(&conf, &account, aggregate_results.clone(), &logger) + if !aggregate_results.is_empty() { + for (auto_renew_contract, result) in &aggregate_results { + match renew_domains( + &conf, + &account, + result.clone(), + auto_renew_contract, + &logger, + ) .await - { - Ok(_) => { - aggregate_results - .domains - .iter() - .zip(aggregate_results.renewers.iter()) - .for_each(|(d, r)| { - logger.info(format!( - "- `Renewal: {}` by `{:#x}`", - &decode(*d), - r - )) - }); - } - Err(e) => { - logger.severe(format!("Unable to renew domains: {}", e)); - if e.to_string().contains("request rate limited") { - continue; - } else { - break; + { + Ok(_) => { + logger.info(format!( + "`Renewed {} domains on auto renewal contract address {}", + result.domains.len(), + auto_renew_contract + )); + } + Err(e) => { + logger.severe(format!("Unable to renew domains: {}", e)); + if e.to_string().contains("request rate limited") { + continue; + } else { + break; + } } } } diff --git a/bot/src/models.rs b/bot/src/models.rs index ad5d3a2..abd7acb 100644 --- a/bot/src/models.rs +++ b/bot/src/models.rs @@ -4,7 +4,7 @@ use bigdecimal::BigDecimal; use bson::DateTime; use mongodb::Database; use serde::{Deserialize, Serialize}; -use starknet::core::types::{FieldElement, TransactionExecutionStatus}; +use starknet::core::types::FieldElement; pub struct AppState { pub db: Database, @@ -19,6 +19,7 @@ pub struct Domain { pub token_id: Option, pub creation_date: Option, pub rev_addr: Option, + pub auto_renew_contract: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -51,6 +52,8 @@ pub struct DomainAggregateResult { pub last_renewal: Option, pub meta_hash: Option, pub _cursor: Cursor, + pub erc20_addr: String, + pub auto_renew_contract: FieldElement, } pub struct AggregateResult { @@ -59,6 +62,7 @@ pub struct AggregateResult { pub domain_price: BigDecimal, pub tax_price: BigDecimal, pub meta_hash: FieldElement, + pub auto_renew_contract: FieldElement, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -68,6 +72,7 @@ pub struct AggregateResults { pub domain_prices: Vec, pub tax_prices: Vec, pub meta_hashes: Vec, + pub auto_renew_contracts: Vec, } #[derive(Serialize, Deserialize, Debug)] @@ -97,58 +102,3 @@ pub struct TxResult { pub revert_reason: Option, pub domains_renewed: usize, } - -pub trait Unzip5 { - type A; - type B; - type C; - type D; - type E; - - fn unzip5( - self, - ) -> ( - Vec, - Vec, - Vec, - Vec, - Vec, - ); -} - -impl Unzip5 for T -where - T: Iterator, -{ - type A = A; - type B = B; - type C = C; - type D = D; - type E = E; - - fn unzip5( - self, - ) -> ( - Vec, - Vec, - Vec, - Vec, - Vec, - ) { - let mut a = Vec::new(); - let mut b = Vec::new(); - let mut c = Vec::new(); - let mut d = Vec::new(); - let mut e = Vec::new(); - - for (x, y, z, w, v) in self { - a.push(x); - b.push(y); - c.push(z); - d.push(w); - e.push(v); - } - - (a, b, c, d, e) - } -} diff --git a/bot/src/pipelines.rs b/bot/src/pipelines.rs new file mode 100644 index 0000000..6a66724 --- /dev/null +++ b/bot/src/pipelines.rs @@ -0,0 +1,194 @@ +use anyhow::Result; +use bson::{doc, Bson}; +use chrono::{Duration, Utc}; +use futures::TryStreamExt; +use starknet::core::types::FieldElement; +use std::sync::Arc; + +use crate::{ + config::Config, + models::{AppState, Domain, DomainAggregateResult}, + utils::to_hex, +}; + +pub async fn get_auto_renewal_data( + config: &Config, + state: &Arc, +) -> Result> { + let auto_renews_collection = state.db.collection::("auto_renew_flows"); + let min_expiry_date = Utc::now() + Duration::days(400); + let erc20_addr = to_hex(config.contract.erc20); + let auto_renew_contract = FieldElement::to_string(&config.contract.renewal); + println!("timestamp: {}", min_expiry_date.timestamp()); + // Define aggregate pipeline + let pipeline = vec![ + doc! { "$match": { "_cursor.to": null } }, + doc! { "$match": { "enabled": true } }, + doc! { "$lookup": { + "from": "domains", + "let": { "domain_name": "$domain" }, + "pipeline": [ + { "$match": + { "$expr": + { "$and": [ + { "$eq": [ "$domain", "$$domain_name" ] }, + { "$eq": [ { "$ifNull": [ "$_cursor.to", null ] }, null ] }, + ]} + } + }, + ], + "as": "domain_info", + }}, + doc! { "$unwind": "$domain_info" }, + doc! { "$match": { "domain_info.expiry": { "$lt": Bson::Int64(min_expiry_date.timestamp()) } } }, + doc! { "$lookup": { + "from": "auto_renew_approvals", + "let": { "renewer_addr": "$renewer_address" }, + "pipeline": [ + { "$match": + { "$expr": + { "$and": [ + { "$eq": [ "$renewer", "$$renewer_addr" ] }, + { "$eq": [ { "$ifNull": [ "$_cursor.to", null ] }, null ] }, + ]} + } + } + ], + "as": "approval_info", + }}, + doc! { "$unwind": { "path": "$approval_info", "preserveNullAndEmptyArrays": true } }, + doc! { "$addFields": { + "erc20_addr": erc20_addr, + "auto_renew_contract": auto_renew_contract, + }}, + doc! { "$group": { + "_id": "$domain_info.domain", + "expiry": { "$first": "$domain_info.expiry" }, + "renewer_address": { "$first": "$renewer_address" }, + "enabled": { "$first": "$enabled" }, + "approval_value": { "$first": { "$ifNull": [ "$approval_info.allowance", "0x0" ] } }, + "allowance": { "$first": "$allowance" }, + "last_renewal": { "$first": "$last_renewal" }, + "meta_hash": { "$first": "$meta_hash" }, + "_cursor": { "$first": "$_cursor" }, + "erc20_addr": { "$first": "$erc20_addr" }, + "auto_renew_contract": { "$first": "$auto_renew_contract" }, + }}, + doc! { "$project": { + "_id": 0, + "domain": "$_id", + "expiry": 1, + "renewer_address": 1, + "enabled": 1, + "approval_value": 1, + "allowance": 1, + "last_renewal": 1, + "meta_hash": 1, + "_cursor": 1, + "erc20_addr": 1, + "auto_renew_contract": 1, + }}, + ]; + + // Execute the pipeline + let cursor = auto_renews_collection.aggregate(pipeline, None).await?; + // Extract the results as Vec + let bson_docs: Vec = cursor.try_collect().await?; + // Convert each bson::Document into DomainAggregateResult + let results: Result, _> = bson_docs + .into_iter() + .map(|doc| bson::from_bson(bson::Bson::Document(doc))) + .collect(); + // Check if the conversion was successful + let results = results?; + + Ok(results) +} + +pub async fn get_auto_renewal_altcoins_data( + config: &Config, + state: &Arc, +) -> Result> { + let auto_renews_collection = state.db.collection::("auto_renew_flows_altcoins"); + let min_expiry_date = Utc::now() + Duration::days(400); // todo : change to 30 days + + // Define aggregate pipeline + let pipeline = vec![ + doc! { "$match": { "_cursor.to": null } }, + doc! { "$match": { "enabled": true } }, + doc! { "$lookup": { + "from": "domains", + "let": { "domain_name": "$domain" }, + "pipeline": [ + { "$match": + { "$expr": + { "$and": [ + { "$eq": [ "$domain", "$$domain_name" ] }, + { "$eq": [ { "$ifNull": [ "$_cursor.to", null ] }, null ] }, + ]} + } + }, + ], + "as": "domain_info", + }}, + doc! { "$unwind": "$domain_info" }, + doc! { "$match": { "domain_info.expiry": { "$lt": Bson::Int64(min_expiry_date.timestamp()) } } }, + doc! { "$lookup": { + "from": "auto_renew_approvals_altcoins", + "let": { "renewer_addr": "$renewer_address" }, + "pipeline": [ + { "$match": + { "$expr": + { "$and": [ + { "$eq": [ "$renewer", "$$renewer_addr" ] }, + { "$eq": [ { "$ifNull": [ "$_cursor.to", null ] }, null ] }, + ]} + } + } + ], + "as": "approval_info", + }}, + doc! { "$unwind": { "path": "$approval_info", "preserveNullAndEmptyArrays": true } }, + doc! { "$group": { + "_id": "$domain_info.domain", + "expiry": { "$first": "$domain_info.expiry" }, + "renewer_address": { "$first": "$renewer_address" }, + "enabled": { "$first": "$enabled" }, + "approval_value": { "$first": { "$ifNull": [ "$approval_info.allowance", "0x0" ] } }, + "allowance": { "$first": "$allowance" }, + "last_renewal": { "$first": "$last_renewal" }, + "meta_hash": { "$first": "$meta_hash" }, + "_cursor": { "$first": "$_cursor" }, + "erc20_addr": { "$first": "$approval_info.erc20_addr" }, + "auto_renew_contract": { "$first": "$auto_renew_contract" }, + }}, + doc! { "$project": { + "_id": 0, + "domain": "$_id", + "expiry": 1, + "renewer_address": 1, + "enabled": 1, + "approval_value": 1, + "allowance": 1, + "last_renewal": 1, + "meta_hash": 1, + "_cursor": 1, + "erc20_addr": 1, + "auto_renew_contract": 1, + }}, + ]; + + // Execute the pipeline + let cursor = auto_renews_collection.aggregate(pipeline, None).await?; + // Extract the results as Vec + let bson_docs: Vec = cursor.try_collect().await?; + // Convert each bson::Document into DomainAggregateResult + let results: Result, _> = bson_docs + .into_iter() + .map(|doc| bson::from_bson(bson::Bson::Document(doc))) + .collect(); + // Check if the conversion was successful + let results = results?; + + Ok(results) +} diff --git a/bot/src/starknetid_utils.rs b/bot/src/starknetid_utils.rs index 82bb8fe..8eb47e1 100644 --- a/bot/src/starknetid_utils.rs +++ b/bot/src/starknetid_utils.rs @@ -1,4 +1,14 @@ +use anyhow::{anyhow, Result}; use bigdecimal::{num_bigint::BigInt, FromPrimitive}; +use serde::Deserialize; +use starknet::{ + core::types::{BlockId, BlockTag, FieldElement, FunctionCall}, + macros::selector, + providers::Provider, +}; +use std::str::FromStr; + +use crate::{config::Config, starknet_utils::create_jsonrpc_client}; lazy_static::lazy_static! { static ref PRICE_DOMAIN_LEN_1: BigInt = BigInt::from_u128(1068493150684932 * 365).unwrap(); @@ -8,7 +18,7 @@ lazy_static::lazy_static! { static ref PRICE_DOMAIN: BigInt = BigInt::from_u128(24657534246575 * 365).unwrap(); } -pub fn get_renewal_price(domain: String) -> BigInt { +pub fn get_renewal_price_eth(domain: String) -> BigInt { let domain_name = domain .strip_suffix(".stark") .ok_or_else(|| anyhow::anyhow!("Invalid domain name: {:?}", domain)) @@ -22,3 +32,80 @@ pub fn get_renewal_price(domain: String) -> BigInt { _ => PRICE_DOMAIN.clone(), } } + +#[derive(Deserialize, Debug)] +pub struct QuoteQueryResult { + quote: String, + max_quote_validity: u64, +} + +pub async fn get_altcoin_quote(config: &Config, erc20: String) -> Result { + // Get quote from starknetid api + let url = format!( + "{}/get_altcoin_quote?erc20_addr={}", + config.server.starknetid_api, erc20 + ); + let client = reqwest::Client::new(); + match client.get(&url).send().await { + Ok(response) => match response.text().await { + Ok(text) => match serde_json::from_str::(&text) { + Ok(results) => { + Ok(BigInt::from_str(&results.quote).unwrap()) + } + Err(err) => Err(anyhow!("Error parsing response: {:?}", err)), + }, + Err(err) => Err(anyhow!("Error fetching response: {:?}", err)), + }, + Err(err) => Err(anyhow!("Error fetching quote: {:?}", err)), + } +} + +pub async fn get_balances( + config: &Config, + mut renewer_and_erc20: Vec<(String, String)>, +) -> Vec { + let mut balances: Vec = vec![]; + + while !renewer_and_erc20.is_empty() { + let size = renewer_and_erc20.len().min(2500); + let batch = renewer_and_erc20.drain(0..size).collect(); + let batch_balances = fetch_users_balances(config, batch).await; + // we skip the first 2 elements, the first one is the index of the call, the 2nd the length of the results + balances.extend(batch_balances.into_iter().skip(2)); + } + + balances +} + +pub async fn fetch_users_balances( + config: &Config, + renewers_and_erc20: Vec<(String, String)>, +) -> Vec { + let mut calls: Vec = vec![FieldElement::from(renewers_and_erc20.len())]; + for (renewer, erc20) in &renewers_and_erc20 { + calls.push(FieldElement::from_hex_be(erc20).unwrap()); + calls.push(selector!("balanceOf")); + calls.push(FieldElement::ONE); + calls.push(FieldElement::from_hex_be(renewer).unwrap()); + } + + let provider = create_jsonrpc_client(config); + let call_result = provider + .call( + FunctionCall { + contract_address: config.contract.multicall, + entry_point_selector: selector!("aggregate"), + calldata: calls, + }, + BlockId::Tag(BlockTag::Latest), + ) + .await; + + match call_result { + Ok(result) => result, + Err(err) => { + println!("Error while fetching balances: {:?}", err); + vec![] + } + } +} diff --git a/bot/src/utils.rs b/bot/src/utils.rs index e9f52d9..e3bb9ce 100644 --- a/bot/src/utils.rs +++ b/bot/src/utils.rs @@ -1,6 +1,7 @@ use bigdecimal::{num_bigint::BigInt, BigDecimal}; use num_integer::Integer; use starknet::core::types::FieldElement; +use std::fmt::Write; lazy_static::lazy_static! { static ref TWO_POW_128: BigInt = BigInt::from(2).pow(128); @@ -34,3 +35,13 @@ pub fn from_uint256(low: FieldElement, high: FieldElement) -> BigInt { &high_bigint.checked_mul(&TWO_POW_128).unwrap() + low_bigint } + +pub fn to_hex(felt: FieldElement) -> String { + let bytes = felt.to_bytes_be(); + let mut result = String::with_capacity(bytes.len() * 2 + 2); + result.push_str("0x"); + for byte in bytes { + write!(&mut result, "{:02x}", byte).unwrap(); + } + result +} diff --git a/config.template.toml b/config.template.toml index 8900eeb..360eda5 100644 --- a/config.template.toml +++ b/config.template.toml @@ -34,3 +34,6 @@ severe = "goerli/severe" [rpc] rpc_url = "https://starknet-goerli.g.alchemy.com/v2/xxxxxxx" + +[server] +starknetid_api = "https://api.starknet.id"