diff --git a/lib/Cargo.lock b/lib/Cargo.lock index 0331474503..7f1c521147 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -1531,7 +1531,7 @@ dependencies = [ [[package]] name = "defichain-rpc" version = "0.18.0" -source = "git+https://github.com/defich/rust-defichain-rpc.git#550743d7b18bbd9bc0ddc6a9ecbb16d7d72151f7" +source = "git+https://github.com/defich/rust-defichain-rpc.git#9a338f8d0ed5e837a67eb8c1aa04a9efc0c5d2ba" dependencies = [ "async-trait", "defichain-rpc-json", @@ -1544,7 +1544,7 @@ dependencies = [ [[package]] name = "defichain-rpc-json" version = "0.18.0" -source = "git+https://github.com/defich/rust-defichain-rpc.git#550743d7b18bbd9bc0ddc6a9ecbb16d7d72151f7" +source = "git+https://github.com/defich/rust-defichain-rpc.git#9a338f8d0ed5e837a67eb8c1aa04a9efc0c5d2ba" dependencies = [ "bitcoin", "serde", diff --git a/lib/ain-ocean/src/api/cache.rs b/lib/ain-ocean/src/api/cache.rs index eb17e1115d..428033b963 100644 --- a/lib/ain-ocean/src/api/cache.rs +++ b/lib/ain-ocean/src/api/cache.rs @@ -3,11 +3,12 @@ use std::{collections::HashMap, sync::Arc}; use cached::proc_macro::cached; use defichain_rpc::{ json::{ + loan::LoanSchemeResult, poolpair::{PoolPairInfo, PoolPairPagination, PoolPairsResult}, token::{TokenInfo, TokenPagination, TokenResult}, }, jsonrpc_async::error::{Error as JsonRpcError, RpcError}, - Error, MasternodeRPC, PoolPairRPC, TokenRPC, + Error, LoanRPC, MasternodeRPC, PoolPairRPC, TokenRPC, }; use super::AppContext; @@ -129,3 +130,13 @@ pub async fn get_gov_cached( let gov = ctx.client.get_gov(id).await?; Ok(gov) } + +#[cached( + result = true, + key = "String", + convert = r#"{ format!("getloanscheme{id}") }"# +)] +pub async fn get_loan_scheme_cached(ctx: &Arc, id: String) -> Result { + let loan_scheme = ctx.client.get_loan_scheme(id).await?; + Ok(loan_scheme) +} diff --git a/lib/ain-ocean/src/api/loan.rs b/lib/ain-ocean/src/api/loan.rs index 5980f8315b..6725f08118 100644 --- a/lib/ain-ocean/src/api/loan.rs +++ b/lib/ain-ocean/src/api/loan.rs @@ -1,23 +1,25 @@ -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; use ain_macros::ocean_endpoint; use anyhow::format_err; use axum::{routing::get, Extension, Router}; -use bitcoin::Txid; +use bitcoin::{hashes::Hash, Txid}; use defichain_rpc::{ defichain_rpc_json::{ loan::{CollateralTokenDetail, LoanSchemeResult}, token::TokenInfo, + vault::VaultLiquidationBatch, }, - LoanRPC, + json::vault::{AuctionPagination, AuctionPaginationStart, VaultState}, + LoanRPC, VaultRPC, }; use futures::future::try_join_all; use log::debug; use serde::Serialize; use super::{ - cache::get_token_cached, - common::Paginate, + cache::{get_loan_scheme_cached, get_token_cached}, + common::{from_script, parse_display_symbol, Paginate}, path::Path, query::{PaginationQuery, Query}, response::{ApiPagedResponse, Response}, @@ -26,8 +28,8 @@ use super::{ }; use crate::{ error::{ApiError, Error}, - model::VaultAuctionBatchHistory, - repository::RepositoryOps, + model::{OraclePriceActive, VaultAuctionBatchHistory}, + repository::{RepositoryOps, SecondaryIndex}, storage::SortOrder, Result, }; @@ -292,9 +294,206 @@ async fn list_vault_auction_history( })) } -// async fn list_auction() -> String { -// "List of auctions".to_string() -// } +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct VaultLiquidationResponse { + pub vault_id: String, + pub loan_scheme: LoanSchemeResult, + pub owner_address: String, + #[serde(default = "VaultState::in_liquidation")] + pub state: VaultState, + pub liquidation_height: u64, + pub liquidation_penalty: f64, + pub batch_count: usize, + pub batches: Vec, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct HighestBidResponse { + pub owner: String, + pub amount: Option, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct VaultLiquidationBatchResponse { + index: u32, + collaterals: Vec, + loan: Option, + highest_bid: Option, + froms: Vec, +} + +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct VaultTokenAmountResponse { + pub id: String, + pub amount: String, + pub symbol: String, + pub display_symbol: String, + pub symbol_key: String, + pub name: String, + pub active_price: Option, +} + +#[ocean_endpoint] +async fn list_auction( + Query(query): Query, + Extension(ctx): Extension>, +) -> Result> { + let start = query.next.as_ref().map(|next| { + let vault_id = &next[0..64]; + let height = &next[64..]; + AuctionPaginationStart { + vault_id: vault_id.to_string(), + height: height.parse::().unwrap_or_default(), + } + }); + + let pagination = AuctionPagination { + start, + including_start: None, + limit: if query.size > 30 { + Some(30) + } else { + Some(query.size) + }, + }; + + async fn map_liquidation_batches( + ctx: &Arc, + vault_id: &str, + batches: Vec, + ) -> Result> { + let repo = &ctx.services.auction; + let mut vec = Vec::new(); + for batch in batches { + let highest_bid = if let Some(bid) = batch.highest_bid { + let amount = map_token_amounts(ctx, vec![bid.amount]).await?; + let res = HighestBidResponse { + owner: bid.owner, + amount: amount.first().cloned(), + }; + Some(res) + } else { + None + }; + let id = ( + Txid::from_str(vault_id)?, + batch.index, + Txid::from_byte_array([0xffu8; 32]), + ); + let bids = repo + .by_id + .list(Some(id), SortOrder::Descending)? + .take_while(|item| match item { + Ok(((vid, bindex, _), _)) => { + vid.to_string() == vault_id && bindex == &batch.index + } + _ => true, + }) + .collect::>(); + let froms = bids + .into_iter() + .map(|bid| { + let (_, v) = bid?; + let from_addr = from_script(v.from, ctx.network.into())?; + Ok::(from_addr) + }) + .collect::>>()?; + vec.push(VaultLiquidationBatchResponse { + index: batch.index, + collaterals: map_token_amounts(ctx, batch.collaterals).await?, + loan: map_token_amounts(ctx, vec![batch.loan]) + .await? + .first() + .cloned(), + froms, + highest_bid, + }) + } + Ok(vec) + } + + async fn map_token_amounts( + ctx: &Arc, + amounts: Vec, + ) -> Result> { + if amounts.is_empty() { + return Ok(Vec::new()); + } + let amount_token_symbols = amounts + .into_iter() + .map(|amount| { + let amount = amount.to_owned(); + let parts = amount.split('@').collect::>(); + let [amount, token_symbol] = <[&str; 2]>::try_from(parts) + .map_err(|_| format_err!("Invalid amount structure"))?; + Ok([amount.to_string(), token_symbol.to_string()]) + }) + .collect::>>()?; + + let mut vault_token_amounts = Vec::new(); + for [amount, token_symbol] in amount_token_symbols { + let token = get_token_cached(ctx, &token_symbol).await?; + if token.is_none() { + log::error!("Token {token_symbol} not found"); + continue; + } + let repo = &ctx.services.oracle_price_active; + let (id, token_info) = token.unwrap(); + let keys = repo + .by_key + .list(None, SortOrder::Descending)? + .collect::>(); + log::debug!("list_auctions keys: {:?}, token_id: {:?}", keys, id); + let active_price = repo + .by_key + .list(None, SortOrder::Descending)? + .take(1) + .take_while(|item| match item { + Ok((k, _)) => k.0 == id, + _ => true, + }) + .map(|el| repo.by_key.retrieve_primary_value(el)) + .collect::>>()?; + + vault_token_amounts.push(VaultTokenAmountResponse { + id, + display_symbol: parse_display_symbol(&token_info), + amount: amount.to_string(), + symbol: token_info.symbol, + symbol_key: token_info.symbol_key, + name: token_info.name, + active_price: active_price.first().cloned(), + }) + } + + Ok(vault_token_amounts) + } + + let mut vaults = Vec::new(); + let liquidation_vaults = ctx.client.list_auctions(Some(pagination)).await?; + for vault in liquidation_vaults { + let loan_scheme = get_loan_scheme_cached(&ctx, vault.loan_scheme_id).await?; + let res = VaultLiquidationResponse { + batches: map_liquidation_batches(&ctx, &vault.vault_id, vault.batches).await?, + vault_id: vault.vault_id, + loan_scheme, + owner_address: vault.owner_address, + state: vault.state, + liquidation_height: vault.liquidation_height, + liquidation_penalty: vault.liquidation_penalty, + batch_count: vault.batch_count, + }; + vaults.push(res) + } + + Ok(ApiPagedResponse::of(vaults, query.size, |auction| { + format!("{}{}", auction.vault_id.clone(), auction.liquidation_height) + })) +} pub fn router(ctx: Arc) -> Router { Router::new() @@ -310,6 +509,6 @@ pub fn router(ctx: Arc) -> Router { "/vaults/:id/auctions/:height/batches/:batchIndex/history", get(list_vault_auction_history), ) - // .route("/auctions", get(list_auction)) + .route("/auctions", get(list_auction)) .layer(Extension(ctx)) } diff --git a/lib/ain-ocean/src/api/pool_pair/service.rs b/lib/ain-ocean/src/api/pool_pair/service.rs index d6d317c879..78142d93ba 100644 --- a/lib/ain-ocean/src/api/pool_pair/service.rs +++ b/lib/ain-ocean/src/api/pool_pair/service.rs @@ -198,7 +198,7 @@ fn calculate_rewards(accounts: &[String], dfi_price_usdt: Decimal) -> Result, ctx: &Context) -> Result<()> { let ticker_id = (self.currency_pair.token, self.currency_pair.currency); - let aggregated_price = services + let aggregated_prices = services .oracle_price_aggregated .by_key .list(Some(ticker_id.clone()), SortOrder::Descending)? .map(|item| { let (_, id) = item?; - let b = services + let aggregated = services .oracle_price_aggregated .by_id .get(&id)? .ok_or("Missing oracle previous history index")?; - Ok(b) + Ok(aggregated) }) - .collect::, Box>>()?; - - if !aggregated_price.is_empty() { - let previous_price = services - .oracle_price_active - .by_key - .list(Some(ticker_id.clone()), SortOrder::Descending)? - .map(|item| { - let (_, id) = item?; - let b = services - .oracle_price_active - .by_id - .get(&id)? - .ok_or("Missing oracle previous history index")?; - - Ok(b) - }) - .collect::, Box>>()?; - let price_active_id = ( - ticker_id.0.clone(), - ticker_id.1.clone(), - aggregated_price[0].block.height, - ); - - let oracle_price_key = (ticker_id.0, ticker_id.1); - let next_price = match aggregated_validate(aggregated_price[0].clone(), ctx) { - true => OraclePriceActiveNext { - amount: aggregated_price[0].aggregated.amount.clone(), - weightage: aggregated_price[0].aggregated.weightage, - oracles: OraclePriceActiveNextOracles { - active: aggregated_price[0].aggregated.oracles.active, - total: aggregated_price[0].aggregated.oracles.total, - }, - }, - false => Default::default(), - }; + .collect::>>()?; - let active_price: OraclePriceActiveActive; + log::debug!( + "set_loan_token indexing aggregated_price: {:?}", + aggregated_prices + ); + + if aggregated_prices.is_empty() { + return Ok(()); + } + let aggregated_price = aggregated_prices.first().unwrap(); - if previous_price.is_empty() { - active_price = OraclePriceActiveActive { - amount: Default::default(), - weightage: Default::default(), + let previous_prices = services + .oracle_price_active + .by_key + .list(Some(ticker_id.clone()), SortOrder::Descending)? + .take(1) + .map(|item| { + let (_, id) = item?; + let price = services + .oracle_price_active + .by_id + .get(&id)? + .ok_or("Missing oracle previous history index")?; + Ok(price) + }) + .collect::>>()?; + + let active_price = if previous_prices.first().is_some() { + if previous_prices[0].next.is_some() { + let price = previous_prices[0].next.clone().unwrap(); + Some(OraclePriceActiveActive { + amount: price.amount, + weightage: price.weightage, oracles: OraclePriceActiveActiveOracles { - active: Default::default(), - total: Default::default(), + active: price.oracles.active, + total: price.oracles.total, }, - }; - } else if let Some(next) = previous_price.first().map(|price| &price.next) { - active_price = OraclePriceActiveActive { - amount: next.amount.clone(), - weightage: next.weightage, + }) + } else if previous_prices[0].active.is_some() { + let price = previous_prices[0].active.clone().unwrap(); + Some(OraclePriceActiveActive { + amount: price.amount, + weightage: price.weightage, oracles: OraclePriceActiveActiveOracles { - active: next.oracles.active, - total: next.oracles.total, + active: price.oracles.active, + total: price.oracles.total, }, - }; + }) } else { - let oracles = OraclePriceActiveActiveOracles { - active: previous_price[0].active.oracles.active, - total: previous_price[0].active.oracles.total, - }; - active_price = OraclePriceActiveActive { - amount: previous_price[0].active.amount.clone(), - weightage: previous_price[0].active.weightage, - oracles, - }; + None } + } else { + None + }; - let oracle_price_active = OraclePriceActive { - id: price_active_id.clone(), - key: oracle_price_key, - sort: hex::encode(ctx.block.height.to_be_bytes()), - active: active_price.clone(), - next: next_price.clone(), - is_live: is_live(Some(active_price), Some(next_price)), - block: ctx.block.clone(), - }; - services - .oracle_price_active - .by_id - .put(&price_active_id, &oracle_price_active)?; - services - .oracle_price_active - .by_key - .put(&oracle_price_active.key, &oracle_price_active.id)?; - } + let price_active_id = ( + ticker_id.0.clone(), + ticker_id.1.clone(), + aggregated_price.block.height, + ); + + let next_price = if is_aggregate_valid(aggregated_price, ctx) { + Some(OraclePriceActiveNext { + amount: aggregated_price.aggregated.amount.clone(), + weightage: aggregated_price.aggregated.weightage, + oracles: OraclePriceActiveNextOracles { + active: aggregated_price.aggregated.oracles.active, + total: aggregated_price.aggregated.oracles.total, + }, + }) + } else { + None + }; + + let oracle_price_active = OraclePriceActive { + id: price_active_id.clone(), + key: ticker_id, + sort: hex::encode(ctx.block.height.to_be_bytes()), + active: active_price.clone(), + next: next_price.clone(), + is_live: is_live(active_price, next_price), + block: ctx.block.clone(), + }; + + services + .oracle_price_active + .by_id + .put(&price_active_id, &oracle_price_active)?; + + services + .oracle_price_active + .by_key + .put(&oracle_price_active.key, &oracle_price_active.id)?; + + log::debug!( + "set_loan_token indexing oracle_price_active: {:?}", + oracle_price_active + ); Ok(()) } + fn invalidate(&self, services: &Arc, context: &Context) -> Result<()> { let ticker_id = ( self.currency_pair.token.clone(), @@ -134,52 +149,59 @@ impl Index for SetLoanToken { Ok(()) } } -pub fn aggregated_validate(aggrigated_price: OraclePriceAggregated, context: &Context) -> bool { - let minimum_live_oracles = 2; - if (aggrigated_price.block.time - context.block.time).abs() >= 3600 { + +fn is_aggregate_valid(aggregate: &OraclePriceAggregated, context: &Context) -> bool { + if (aggregate.block.time - context.block.time).abs() >= 3600 { return false; } - if aggrigated_price.aggregated.oracles.active < minimum_live_oracles { + + if aggregate.aggregated.oracles.active < 2 { + // minimum live oracles return false; } - if aggrigated_price.aggregated.weightage <= 0 { + if aggregate.aggregated.weightage <= 0 { return false; } true } -pub fn is_live( - active: Option, - next: Option, -) -> bool { - if let (Some(active), Some(next)) = (active, next) { - let active_price = match Decimal::from_str_exact(&active.amount) { - Ok(num) => num, - Err(_) => return false, - }; +fn is_live(active: Option, next: Option) -> bool { + if active.is_none() { + return false; + } - let next_price = match Decimal::from_str_exact(&next.amount) { - Ok(num) => num, - Err(_) => return false, - }; + if next.is_none() { + return false; + } - if active_price <= Decimal::zero() || next_price <= Decimal::zero() { - return false; - } + let active = active.unwrap(); + let next = next.unwrap(); - let deviation_threshold = Decimal::new(5, 1); // This represents 0.5 + let active_price = match Decimal::from_str(&active.amount) { + Ok(num) => num, + Err(_) => return false, + }; - let diff = next_price - active_price; - let abs_diff = diff.abs(); - let threshold = active_price * deviation_threshold; - if abs_diff >= threshold { - return false; - } + let next_price = match Decimal::from_str(&next.amount) { + Ok(num) => num, + Err(_) => return false, + }; + + if active_price <= Decimal::zero() { + return false; + } - true - } else { - false + if next_price <= Decimal::zero() { + return false; } + + let diff = (next_price - active_price).abs(); + let threshold = active_price * dec!(0.3); // deviation_threshold 0.3 + if diff >= threshold { + return false; + } + + true } diff --git a/lib/ain-ocean/src/indexer/mod.rs b/lib/ain-ocean/src/indexer/mod.rs index 983e10cc63..b0d72048f1 100644 --- a/lib/ain-ocean/src/indexer/mod.rs +++ b/lib/ain-ocean/src/indexer/mod.rs @@ -531,7 +531,7 @@ pub fn index_block(services: &Arc, block: Block) -> Resul DfTx::SetLoanToken(data) => data.index(services, &ctx)?, DfTx::CompositeSwap(data) => data.index(services, &ctx)?, DfTx::CreatePoolPair(data) => data.index(services, &ctx)?, - // DfTx::PlaceAuctionBid(data) => data.index(services, &ctx)?, + DfTx::PlaceAuctionBid(data) => data.index(services, &ctx)?, _ => (), } log_elapsed(start, "Indexed dftx"); diff --git a/lib/ain-ocean/src/model/oracle_price_active.rs b/lib/ain-ocean/src/model/oracle_price_active.rs index 9185cb03d3..08c08c893e 100644 --- a/lib/ain-ocean/src/model/oracle_price_active.rs +++ b/lib/ain-ocean/src/model/oracle_price_active.rs @@ -4,14 +4,14 @@ use super::BlockContext; pub type OraclePriceActiveId = (String, String, u32); //token-currency-height pub type OraclePriceActiveKey = (String, String); //token-currency -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct OraclePriceActive { pub id: OraclePriceActiveId, pub key: OraclePriceActiveKey, pub sort: String, //height - pub active: OraclePriceActiveActive, - pub next: OraclePriceActiveNext, + pub active: Option, + pub next: Option, pub is_live: bool, pub block: BlockContext, } diff --git a/lib/ain-ocean/src/model/vault_auction_batch_history.rs b/lib/ain-ocean/src/model/vault_auction_batch_history.rs index c502265a24..0ba11786ce 100644 --- a/lib/ain-ocean/src/model/vault_auction_batch_history.rs +++ b/lib/ain-ocean/src/model/vault_auction_batch_history.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; use super::BlockContext; -pub type AuctionHistoryKey = (Txid, u32, Txid); -pub type AuctionHistoryByHeightKey = (Txid, u32, u32, usize); +pub type AuctionHistoryKey = (Txid, u32, Txid); // (vault_id, auction_batch_index, txid) +pub type AuctionHistoryByHeightKey = (Txid, u32, u32, usize); // (vault_id, auction_batch_index, block_height, txid) #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] diff --git a/lib/ain-ocean/src/repository/oracle_price_active.rs b/lib/ain-ocean/src/repository/oracle_price_active.rs index de1cfcf515..172ad0c201 100644 --- a/lib/ain-ocean/src/repository/oracle_price_active.rs +++ b/lib/ain-ocean/src/repository/oracle_price_active.rs @@ -3,11 +3,11 @@ use std::sync::Arc; use ain_db::LedgerColumn; use ain_macros::Repository; -use super::RepositoryOps; +use super::{RepositoryOps, SecondaryIndex}; use crate::{ model::{OraclePriceActive, OraclePriceActiveId, OraclePriceActiveKey}, storage::{columns, ocean_store::OceanStore}, - Result, + Error, Result, }; #[derive(Repository)] @@ -23,3 +23,16 @@ pub struct OraclePriceActiveKeyRepository { pub store: Arc, col: LedgerColumn, } + +impl SecondaryIndex for OraclePriceActiveKeyRepository { + type Value = OraclePriceActive; + + fn retrieve_primary_value(&self, el: Self::ListItem) -> Result { + let (_, id) = el?; + + let col = self.store.column::(); + let res = col.get(&id)?.ok_or(Error::SecondaryIndex)?; + + Ok(res) + } +}