diff --git a/lib/Cargo.lock b/lib/Cargo.lock index 26e1f9276b..e5a36c970b 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -738,7 +738,7 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitcoin" version = "0.31.0" -source = "git+https://github.com/defich/rust-bitcoin.git#af64fe422669e2d4fc24fca93561566e22cce1df" +source = "git+https://github.com/defich/rust-bitcoin.git#c68ce40fd7f618105ab7d6e8be49ce083f97d4e8" dependencies = [ "bech32", "bitcoin-internals", @@ -762,7 +762,7 @@ dependencies = [ [[package]] name = "bitcoin-io" version = "0.1.0" -source = "git+https://github.com/defich/rust-bitcoin.git#af64fe422669e2d4fc24fca93561566e22cce1df" +source = "git+https://github.com/defich/rust-bitcoin.git#c68ce40fd7f618105ab7d6e8be49ce083f97d4e8" [[package]] name = "bitcoin_hashes" @@ -1527,7 +1527,7 @@ dependencies = [ [[package]] name = "defichain-rpc" version = "0.18.0" -source = "git+https://github.com/defich/rust-defichain-rpc.git#2e948d7d7ebb9df548c5773903acb0e46c67ab1b" +source = "git+https://github.com/defich/rust-defichain-rpc.git#c2329bcf26c497cdf4f1c22ef1198819542cf70d" dependencies = [ "async-trait", "defichain-rpc-json", @@ -1540,7 +1540,7 @@ dependencies = [ [[package]] name = "defichain-rpc-json" version = "0.18.0" -source = "git+https://github.com/defich/rust-defichain-rpc.git#2e948d7d7ebb9df548c5773903acb0e46c67ab1b" +source = "git+https://github.com/defich/rust-defichain-rpc.git#c2329bcf26c497cdf4f1c22ef1198819542cf70d" dependencies = [ "bitcoin", "serde", diff --git a/lib/ain-ocean/src/api/mod.rs b/lib/ain-ocean/src/api/mod.rs index d3fce25083..d3ccc3f51b 100644 --- a/lib/ain-ocean/src/api/mod.rs +++ b/lib/ain-ocean/src/api/mod.rs @@ -80,8 +80,6 @@ pub async fn ocean_router( .nest("/masternodes", masternode::router(Arc::clone(&context))) .nest("/oracles", oracle::router(Arc::clone(&context))) .nest("/poolpairs", pool_pair::router(Arc::clone(&context))) - // .nest("/prices", prices::router(Arc::clone(&context))) - // .nest("/poolpairs", poolpairs::router(Arc::clone(&context))) .nest("/prices", prices::router(Arc::clone(&context))) // .nest("/rawtx", rawtx::router(Arc::clone(&context))) .nest("/stats", stats::router(Arc::clone(&context))) diff --git a/lib/ain-ocean/src/api/pool_pair/mod.rs b/lib/ain-ocean/src/api/pool_pair/mod.rs index f58d159ed7..b2208f6917 100644 --- a/lib/ain-ocean/src/api/pool_pair/mod.rs +++ b/lib/ain-ocean/src/api/pool_pair/mod.rs @@ -22,8 +22,10 @@ use petgraph::graphmap::UnGraphMap; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use serde_json::json; +use serde_with::skip_serializing_none; use service::{ - get_aggregated_in_usd, get_apr, get_total_liquidity_usd, get_usd_volume, PoolPairVolumeResponse, + check_swap_type, find_swap_from_to, get_aggregated_in_usd, get_apr, get_total_liquidity_usd, + get_usd_volume, PoolPairVolumeResponse, PoolSwapFromTo, PoolSwapFromToData, SwapType, }; use super::{ @@ -64,15 +66,7 @@ struct DexPrices { denomination: String, } -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct PoolSwapFromToResponse { - address: String, - amount: String, - // symbol: String, - // display_symbol: String, -} - +#[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct PoolSwapVerboseResponse { @@ -84,35 +78,25 @@ pub struct PoolSwapVerboseResponse { from_amount: String, from_token_id: u64, block: BlockContext, - from: PoolSwapFromToResponse, - to: PoolSwapFromToResponse, - // type: todo()! + from: Option, + to: Option, + r#type: Option, } -impl From for PoolSwapVerboseResponse { - fn from(v: PoolSwap) -> Self { +impl PoolSwapVerboseResponse { + fn map(v: PoolSwap, from_to: Option, swap_type: Option) -> Self { Self { id: v.id, sort: v.sort, txid: v.txid.to_string(), txno: v.txno, pool_pair_id: v.pool_id.to_string(), - from_amount: v.from_amount.to_string(), + from_amount: Decimal::new(v.from_amount, 8).to_string(), from_token_id: v.from_token_id, - from: PoolSwapFromToResponse { - address: v.from.to_hex_string(), - amount: v.from_amount.to_string(), - // symbol: todo!(), - // display_symbol: todo!(), - }, - to: PoolSwapFromToResponse { - address: v.to.to_hex_string(), - amount: v.to_amount.to_string(), - // symbol: todo!(), - // display_symbol: todo!(), - }, + from: from_to.clone().and_then(|item| item.from), + to: from_to.and_then(|item| item.to), block: v.block, - // type: todo!(), + r#type: swap_type, } } } @@ -138,7 +122,7 @@ impl From for PoolSwapResponse { txid: v.txid.to_string(), txno: v.txno, pool_pair_id: v.pool_id.to_string(), - from_amount: v.from_amount.to_string(), + from_amount: Decimal::new(v.from_amount, 8).to_string(), from_token_id: v.from_token_id, block: v.block, } @@ -456,9 +440,9 @@ async fn list_pool_swaps_verbose( .transpose()? .unwrap_or(PoolSwapRepository::initial_key(id)); - let size = if query.size > 200 { 200 } else { query.size }; + let size = if query.size > 20 { 20 } else { query.size }; - let swaps = ctx + let fut = ctx .services .pool .by_id @@ -468,11 +452,20 @@ async fn list_pool_swaps_verbose( Ok((k, _)) => k.0 == id, _ => true, }) - .map(|item| { + .map(|item| async { let (_, swap) = item?; - Ok(PoolSwapVerboseResponse::from(swap)) + let from_to = + find_swap_from_to(&ctx, swap.block.height, swap.txid, swap.txno.try_into()?) + .await?; + + let swap_type = check_swap_type(&ctx, swap.clone()).await?; + + let res = PoolSwapVerboseResponse::map(swap, from_to, swap_type); + Ok::(res) }) - .collect::>>()?; + .collect::>(); + + let swaps = try_join_all(fut).await?; Ok(ApiPagedResponse::of(swaps, query.size, |swap| { swap.sort.to_string() diff --git a/lib/ain-ocean/src/api/pool_pair/path.rs b/lib/ain-ocean/src/api/pool_pair/path.rs index 9ec0ea74e0..12b0015b50 100644 --- a/lib/ain-ocean/src/api/pool_pair/path.rs +++ b/lib/ain-ocean/src/api/pool_pair/path.rs @@ -19,7 +19,7 @@ use crate::{ enum TokenDirection { In, - Out + Out, } #[derive(Debug, Serialize)] @@ -323,7 +323,7 @@ fn get_dex_fees_pct( ab: match token_b_direction { TokenDirection::In => format!("{:.8}", dex_fee_in_pct_token_b.unwrap_or_default()), TokenDirection::Out => format!("{:.8}", dex_fee_out_pct_token_b.unwrap_or_default()), - } + }, }) } @@ -518,7 +518,6 @@ pub async fn sync_token_graph(ctx: &Arc) -> Result<()> { if !graph.lock().contains_edge(id_token_a, id_token_b) { graph.lock().add_edge(id_token_a, id_token_b, k); } - log::debug!("sync_token_graph edges: {:?}", graph.lock().edge_count()); } // wait 120s diff --git a/lib/ain-ocean/src/api/pool_pair/service.rs b/lib/ain-ocean/src/api/pool_pair/service.rs index 19c5f8e28d..59805da943 100644 --- a/lib/ain-ocean/src/api/pool_pair/service.rs +++ b/lib/ain-ocean/src/api/pool_pair/service.rs @@ -1,15 +1,20 @@ use std::{collections::HashMap, str::FromStr, sync::Arc}; use anyhow::{format_err, Context}; -use defichain_rpc::{json::poolpair::PoolPairInfo, BlockchainRPC}; +use bitcoin::{Address, Txid}; +use defichain_rpc::{ + json::{account::AccountHistory, poolpair::PoolPairInfo}, + AccountRPC, BlockchainRPC, +}; use rust_decimal::{prelude::FromPrimitive, Decimal}; use rust_decimal_macros::dec; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use super::{AppContext, PoolPairAprResponse}; use crate::{ api::{ cache::{get_gov_cached, get_pool_pair_cached, get_token_cached}, + common::parse_display_symbol, pool_pair::path::{get_best_path, BestSwapPathResponse}, }, error::{Error, NotFoundKind}, @@ -19,6 +24,18 @@ use crate::{ storage::SortOrder, Result, }; +use ain_dftx::{ + deserialize, + pool::{CompositeSwap, PoolSwap}, + DfTx, Stack, +}; + +#[allow(clippy::upper_case_acronyms)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum SwapType { + BUY, + SELL, +} #[derive(Serialize, Debug, Clone, Default)] pub struct PoolPairVolumeResponse { @@ -26,6 +43,22 @@ pub struct PoolPairVolumeResponse { pub h24: Decimal, } +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PoolSwapFromToData { + pub address: String, + pub amount: String, + pub symbol: String, + pub display_symbol: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct PoolSwapFromTo { + pub from: Option, + pub to: Option, +} + async fn get_usd_per_dfi(ctx: &Arc) -> Result { let usdt = get_pool_pair_cached(ctx, "USDT-DFI".to_string()).await?; @@ -521,3 +554,230 @@ pub async fn get_aggregated_in_usd( Ok(value) } + +fn call_dftx(ctx: &Arc, txid: Txid) -> Result> { + let vout = ctx + .services + .transaction + .vout_by_id + .list(Some((txid, 0)), SortOrder::Ascending)? + .take(1) + .take_while(|item| match item { + Ok((_, vout)) => vout.txid == txid, + _ => true, + }) + .map(|item| { + let (_, v) = item?; + Ok(v) + }) + .collect::>>()?; + + if vout.is_empty() { + return Ok(None); + } + + let bytes = &vout[0].script.hex; + if bytes.len() > 6 && bytes[0] == 0x6a && bytes[1] <= 0x4e { + let offset = 1 + match bytes[1] { + 0x4c => 2, + 0x4d => 3, + 0x4e => 4, + _ => 1, + }; + + let raw_tx = &bytes[offset..]; + let dftx = match deserialize::(raw_tx) { + Ok(stack) => stack.dftx, + Err(e) => return Err(e.into()), + }; + return Ok(Some(dftx)); + }; + + Ok(None) +} + +fn find_composite_swap_dftx(ctx: &Arc, txid: Txid) -> Result> { + let dftx = call_dftx(ctx, txid)?; + if dftx.is_none() { + return Ok(None); + } + let dftx = dftx.unwrap(); + + let composite_swap_dftx = match dftx { + DfTx::CompositeSwap(data) => Some(data), + _ => None, + }; + + Ok(composite_swap_dftx) +} + +fn find_pool_swap_dftx(ctx: &Arc, txid: Txid) -> Result> { + let dftx = call_dftx(ctx, txid)?; + if dftx.is_none() { + return Ok(None); + } + let dftx = dftx.unwrap(); + let pool_swap_dftx = match dftx { + DfTx::PoolSwap(data) => Some(data), + DfTx::CompositeSwap(data) => Some(data.pool_swap), + _ => None, + }; + + Ok(pool_swap_dftx) +} + +fn find_pool_swap_from_to( + history: AccountHistory, + from: bool, + display_symbol: String, +) -> Result> { + for account in history.amounts { + let parts = account.split('@').collect::>(); + let [value, symbol] = parts + .as_slice() + .try_into() + .context("Invalid amount structure")?; + + let value = Decimal::from_str(value)?; + + if value.is_sign_negative() && from { + return Ok(Some(PoolSwapFromToData { + address: history.owner, + amount: format!("{:.8}", value.abs()), + symbol: symbol.to_string(), + display_symbol, + })); + } + + if value.is_sign_positive() && !from { + return Ok(Some(PoolSwapFromToData { + address: history.owner, + amount: format!("{:.8}", value.abs()), + symbol: symbol.to_string(), + display_symbol, + })); + } + } + + Ok(None) +} + +pub async fn find_swap_from_to( + ctx: &Arc, + height: u32, + txid: Txid, + txno: u32, +) -> Result> { + let dftx = find_pool_swap_dftx(ctx, txid)?; + if dftx.is_none() { + return Ok(None); + } + let dftx = dftx.unwrap(); + + let from_script = dftx.from_script.as_script(); + + let from_address = Address::from_script(from_script, ctx.network.into()); + if from_address.is_err() { + return Ok(None); + } + let from_address = from_address.unwrap().to_string(); + + let to_script = dftx.to_script.as_script(); + let to_address = Address::from_script(to_script, ctx.network.into()); + if to_address.is_err() { + return Ok(None); + } + let to_address = to_address.unwrap().to_string(); + + let from_token = get_token_cached(ctx, &dftx.from_token_id.0.to_string()).await?; + if from_token.is_none() { + return Ok(None); + } + let (_, from_token) = from_token.unwrap(); + + let to_token = get_token_cached(ctx, &dftx.to_token_id.0.to_string()).await?; + if to_token.is_none() { + return Ok(None); + } + let (_, to_token) = to_token.unwrap(); + + let from = PoolSwapFromToData { + address: from_address, + amount: Decimal::new(dftx.from_amount, 8).to_string(), + display_symbol: parse_display_symbol(&from_token), + symbol: from_token.symbol, + }; + + let history = ctx + .client + .get_account_history(&to_address.to_string(), height, txno) + .await?; + + let to = find_pool_swap_from_to(history, false, parse_display_symbol(&to_token))?; + + Ok(Some(PoolSwapFromTo { + from: Some(from), + to, + })) +} + +async fn get_pool_swap_type( + ctx: &Arc, + swap: crate::model::PoolSwap, +) -> Result> { + let pool_pair = get_pool_pair_cached(ctx, swap.pool_id.to_string()).await?; + if pool_pair.is_none() { + return Ok(None); + } + let (_, pool_pair_info) = pool_pair.unwrap(); + let id_token_a = pool_pair_info.id_token_a.parse::()?; + let swap_type = if id_token_a == swap.from_token_id { + SwapType::SELL + } else { + SwapType::BUY + }; + Ok(Some(swap_type)) +} + +pub async fn check_swap_type( + ctx: &Arc, + swap: crate::model::PoolSwap, +) -> Result> { + let dftx = find_composite_swap_dftx(ctx, swap.txid)?; + if dftx.is_none() { + return get_pool_swap_type(ctx, swap).await; + } + let dftx = dftx.unwrap(); + + if dftx.pools.iter().count() <= 1 { + return get_pool_swap_type(ctx, swap).await; + } + + let mut prev = swap.from_token_id.to_string(); + for pool in dftx.pools.iter() { + let pool_id = pool.id.0.to_string(); + let pool_pair = get_pool_pair_cached(ctx, pool_id.clone()).await?; + if pool_pair.is_none() { + break; + } + let (_, pool_pair_info) = pool_pair.unwrap(); + + // if this is current pool pair, if previous token is primary token, indicator = sell + if pool_id == swap.pool_id.to_string() { + let swap_type = if pool_pair_info.id_token_a == prev { + SwapType::SELL + } else { + SwapType::BUY + }; + return Ok(Some(swap_type)); + } + // set previous token as pair swapped out token + prev = if prev == pool_pair_info.id_token_a { + pool_pair_info.id_token_b + } else { + pool_pair_info.id_token_a + } + } + + Ok(None) +} diff --git a/lib/ain-ocean/src/error.rs b/lib/ain-ocean/src/error.rs index 617b1e6c06..fb8e2478f2 100644 --- a/lib/ain-ocean/src/error.rs +++ b/lib/ain-ocean/src/error.rs @@ -61,6 +61,10 @@ pub enum Error { SecondaryIndex, #[error("Token {0:?} is invalid as it is not tradeable")] UntradeableTokenError(String), + #[error("Ocean: BitcoinAddressError: {0:?}")] + BitcoinAddressError(#[from] bitcoin::address::Error), + #[error("Ocean: TryFromIntError: {0:?}")] + TryFromIntError(#[from] std::num::TryFromIntError), #[error(transparent)] Other(#[from] anyhow::Error), } diff --git a/lib/ain-ocean/src/indexer/mod.rs b/lib/ain-ocean/src/indexer/mod.rs index 88417cd339..3fac998c63 100644 --- a/lib/ain-ocean/src/indexer/mod.rs +++ b/lib/ain-ocean/src/indexer/mod.rs @@ -166,7 +166,7 @@ pub fn index_block( DfTx::SetOracleData(data) => data.index(services, &ctx)?, DfTx::PoolSwap(data) => data.index(services, &ctx)?, DfTx::SetLoanToken(data) => data.index(services, &ctx)?, - // DfTx::CompositeSwap(data) => data.index(services, &ctx)?, + DfTx::CompositeSwap(data) => data.index(services, &ctx)?, // DfTx::PlaceAuctionBid(data) => data.index(services, &ctx)?, _ => (), } diff --git a/lib/ain-ocean/src/model/poolswap.rs b/lib/ain-ocean/src/model/poolswap.rs index ace6dadf4b..1dee98f7b7 100644 --- a/lib/ain-ocean/src/model/poolswap.rs +++ b/lib/ain-ocean/src/model/poolswap.rs @@ -5,7 +5,7 @@ use super::BlockContext; pub type PoolSwapKey = (u32, u32, usize); -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct PoolSwap { pub id: String, diff --git a/lib/ain-ocean/src/network.rs b/lib/ain-ocean/src/network.rs index 19d59082b1..911ab64162 100644 --- a/lib/ain-ocean/src/network.rs +++ b/lib/ain-ocean/src/network.rs @@ -22,6 +22,19 @@ impl Network { } } +#[allow(clippy::from_over_into)] +impl Into for Network { + fn into(self) -> bitcoin::Network { + match self { + Network::Mainnet => bitcoin::Network::Mainnet, + Network::Testnet => bitcoin::Network::Testnet, + Network::Devnet => bitcoin::Network::Devnet, + Network::Regtest => bitcoin::Network::Regtest, + _ => bitcoin::Network::Regtest, + } + } +} + impl std::str::FromStr for Network { type Err = &'static str; fn from_str(s: &str) -> Result { diff --git a/src/dfi/mn_checks.cpp b/src/dfi/mn_checks.cpp index 4be0377caa..42763ecaa6 100644 --- a/src/dfi/mn_checks.cpp +++ b/src/dfi/mn_checks.cpp @@ -1155,7 +1155,8 @@ Res CPoolSwap::ExecuteSwap(CCustomCSView &view, result = swapAmountResult.nValue; // Send final swap amount Rust side for indexer - if (txInfo) { + bool isOceanEnabled = gArgs.GetBoolArg("-oceanarchive", false); + if (txInfo && isOceanEnabled) { const auto &[txType, txHash] = *txInfo; CrossBoundaryResult ffiResult; ocean_try_set_tx_result(ffiResult,