diff --git a/lib/Cargo.lock b/lib/Cargo.lock index 2fbc384a6e..56aadea45a 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -1530,7 +1530,7 @@ dependencies = [ [[package]] name = "defichain-rpc" version = "0.18.0" -source = "git+https://github.com/defich/rust-defichain-rpc.git#9d0a769b11c24b855843ecb6dbe556a3b30dfcdf" +source = "git+https://github.com/defich/rust-defichain-rpc.git#90b50580fb913a58e474c66a26dc21c7a9b97d95" dependencies = [ "async-trait", "defichain-rpc-json", @@ -1543,7 +1543,7 @@ dependencies = [ [[package]] name = "defichain-rpc-json" version = "0.18.0" -source = "git+https://github.com/defich/rust-defichain-rpc.git#9d0a769b11c24b855843ecb6dbe556a3b30dfcdf" +source = "git+https://github.com/defich/rust-defichain-rpc.git#90b50580fb913a58e474c66a26dc21c7a9b97d95" dependencies = [ "bitcoin", "serde", diff --git a/lib/ain-dftx/src/types/pool.rs b/lib/ain-dftx/src/types/pool.rs index 3d3c7a97a5..d9550fef9b 100644 --- a/lib/ain-dftx/src/types/pool.rs +++ b/lib/ain-dftx/src/types/pool.rs @@ -1,3 +1,5 @@ +use std::fmt; + use ain_macros::ConsensusEncoding; use bitcoin::{io, ScriptBuf}; @@ -65,3 +67,8 @@ pub struct PoolUpdatePair { pub owner_address: ScriptBuf, pub custom_rewards: Maybe>, } +impl fmt::Display for PoolId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "PoolId: {:?}", self.id) + } +} diff --git a/lib/ain-ocean/src/api/mod.rs b/lib/ain-ocean/src/api/mod.rs index d3ccc3f51b..f1707120df 100644 --- a/lib/ain-ocean/src/api/mod.rs +++ b/lib/ain-ocean/src/api/mod.rs @@ -4,18 +4,18 @@ use axum::{extract::Request, http::StatusCode, response::IntoResponse, Json, Rou // mod address; mod block; +mod cache; +pub mod common; mod fee; mod governance; mod loan; mod masternode; mod oracle; +mod path; mod pool_pair; pub mod prices; -// mod rawtx; -mod cache; -pub mod common; -mod path; mod query; +mod rawtx; mod response; mod stats; mod tokens; @@ -81,7 +81,7 @@ pub async fn ocean_router( .nest("/oracles", oracle::router(Arc::clone(&context))) .nest("/poolpairs", pool_pair::router(Arc::clone(&context))) .nest("/prices", prices::router(Arc::clone(&context))) - // .nest("/rawtx", rawtx::router(Arc::clone(&context))) + .nest("/rawtx", rawtx::router(Arc::clone(&context))) .nest("/stats", stats::router(Arc::clone(&context))) .nest("/tokens", tokens::router(Arc::clone(&context))) .nest("/transactions", transactions::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 949b03fb20..529507fb6c 100644 --- a/lib/ain-ocean/src/api/pool_pair/mod.rs +++ b/lib/ain-ocean/src/api/pool_pair/mod.rs @@ -19,6 +19,7 @@ use path::{ SwapPathsResponse, }; use petgraph::graphmap::UnGraphMap; +use price::DexPriceResponse; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -44,8 +45,6 @@ use crate::{ Result, TokenIdentifier, }; -use price::DexPriceResponse; - pub mod path; pub mod price; pub mod service; diff --git a/lib/ain-ocean/src/api/pool_pair/price.rs b/lib/ain-ocean/src/api/pool_pair/price.rs index e4d1bf6f32..1bdb408d16 100644 --- a/lib/ain-ocean/src/api/pool_pair/price.rs +++ b/lib/ain-ocean/src/api/pool_pair/price.rs @@ -1,10 +1,9 @@ -use serde::Serialize; use std::{collections::HashMap, sync::Arc}; use defichain_rpc::json::token::TokenInfo; +use serde::Serialize; use super::{path::get_best_path, AppContext}; - use crate::{ api::{ cache::{get_token_cached, list_token_cached}, diff --git a/lib/ain-ocean/src/api/pool_pair/service.rs b/lib/ain-ocean/src/api/pool_pair/service.rs index b8b93fd6dd..d6d317c879 100644 --- a/lib/ain-ocean/src/api/pool_pair/service.rs +++ b/lib/ain-ocean/src/api/pool_pair/service.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, str::FromStr, sync::Arc}; +use ain_dftx::{deserialize, pool::CompositeSwap, DfTx, Stack}; use anyhow::{format_err, Context}; use bitcoin::Txid; use defichain_rpc::{json::poolpair::PoolPairInfo, AccountRPC, BlockchainRPC}; @@ -21,7 +22,6 @@ use crate::{ storage::SortOrder, Result, }; -use ain_dftx::{deserialize, pool::CompositeSwap, DfTx, Stack}; #[allow(clippy::upper_case_acronyms)] #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/lib/ain-ocean/src/api/rawtx.rs b/lib/ain-ocean/src/api/rawtx.rs index d412441a68..f178b16c74 100644 --- a/lib/ain-ocean/src/api/rawtx.rs +++ b/lib/ain-ocean/src/api/rawtx.rs @@ -1,27 +1,223 @@ -use std::sync::Arc; +use std::{result::Result as StdResult, str::FromStr, sync::Arc}; +use ain_dftx::{deserialize, DfTx}; +use ain_macros::ocean_endpoint; use axum::{ + extract::{Json, Path}, routing::{get, post}, - Router, + Extension, Router, }; -use defichain_rpc::{Client, RpcApi}; -use super::path::Path; +use bitcoin::{Transaction, Txid}; +use defichain_rpc::{PoolPairRPC, RpcApi}; +use rust_decimal::prelude::ToPrimitive; +use rust_decimal_macros::dec; +use serde::{Deserialize, Serialize, Serializer}; -async fn send_rawtx() -> String { - "Sending raw transaction".to_string() +use super::{query::Query, response::Response, AppContext}; +use crate::{ + error::{ApiError, NotFoundKind}, + model::{default_max_fee_rate, MempoolAcceptResult, RawTransactionResult, RawTxDto}, + Error, Result, +}; + +enum TransactionResponse { + HexString(String), + TransactionDetails(Box), +} + +#[derive(Deserialize, Default)] +struct QueryParams { + verbose: bool, } -async fn test_rawtx() -> String { - "Testing raw transaction".to_string() +#[ocean_endpoint] +async fn send_raw_tx( + Extension(ctx): Extension>, + Json(raw_tx_dto): Json, +) -> Result { + validate(ctx.clone(), raw_tx_dto.hex.clone()).await?; + let max_fee = match raw_tx_dto.max_fee_rate { + Some(fee_rate) => { + let sat_per_bitcoin = dec!(100_000_000); + let fee_in_satoshis = fee_rate.checked_mul(sat_per_bitcoin); + match fee_in_satoshis { + Some(value) => Some(value.to_u64().unwrap_or_default()), + None => Some(default_max_fee_rate().to_sat()), + } + } + None => Some(default_max_fee_rate().to_sat()), + }; + match ctx + .client + .send_raw_transaction(raw_tx_dto.hex, max_fee) + .await + { + Ok(tx_hash) => Ok(tx_hash.to_string()), + Err(e) => { + eprintln!("Failed to send raw transaction: {:?}", e); + if e.to_string().contains("TX decode failed") { + Err(Error::BadRequest("Transaction decode failed".to_string())) + } else { + Err(Error::RpcError(e)) + } + } + } +} +#[ocean_endpoint] +async fn test_raw_tx( + Extension(ctx): Extension>, + Json(raw_tx_dto): Json, +) -> Result>> { + let trx = defichain_rpc::RawTx::raw_hex(raw_tx_dto.hex); + let max_fee = match raw_tx_dto.max_fee_rate { + Some(fee_rate) => { + let sat_per_bitcoin = dec!(100_000_000); + let fee_in_satoshis = fee_rate.checked_mul(sat_per_bitcoin); + match fee_in_satoshis { + Some(value) => Some(value.to_u64().unwrap_or_default()), + None => Some(default_max_fee_rate().to_sat()), + } + } + None => Some(default_max_fee_rate().to_sat()), + }; + match ctx.client.test_mempool_accept(&[trx], max_fee).await { + Ok(mempool_tx) => { + let results = mempool_tx + .into_iter() + .map(|tx_result| MempoolAcceptResult { + txid: tx_result.txid, + allowed: tx_result.allowed, + reject_reason: tx_result.reject_reason, + vsize: tx_result.vsize, + fees: tx_result.fees.map(|f| f.base), + }) + .collect::>(); + Ok(Response::new(results)) + } + Err(e) => { + eprintln!("Failed to send raw transaction: {:?}", e); + if e.to_string().contains("TX decode failed") { + Err(Error::BadRequest("Transaction decode failed".to_string())) + } else { + Err(Error::RpcError(e)) + } + } + } } -async fn get_rawtx(Path(txid): Path) -> String { - format!("Details of raw transaction with txid {}", txid) +impl Serialize for TransactionResponse { + fn serialize(&self, serializer: S) -> StdResult + where + S: Serializer, + { + match *self { + TransactionResponse::HexString(ref s) => serializer.serialize_str(s), + TransactionResponse::TransactionDetails(ref details) => details.serialize(serializer), + } + } +} + +#[ocean_endpoint] +async fn get_raw_tx( + Extension(ctx): Extension>, + Path(txid): Path, + Query(QueryParams { verbose }): Query, +) -> Result { + let tx_hash = Txid::from_str(&txid)?; + if !verbose { + let tx_hex = ctx.client.get_raw_transaction_hex(&tx_hash, None).await.map_err(|e| { + if e.to_string().contains("No such mempool or blockchain transaction. Use gettransaction for wallet transactions.") { + Error::NotFound(NotFoundKind::RawTx) + } else { + Error::RpcError(e) + } + })?; + Ok(TransactionResponse::HexString(tx_hex)) + } else { + let tx_info = ctx + .client + .get_raw_transaction_info(&tx_hash, None) + .await + .map_err(|e| { + eprintln!("Failed to get raw transaction hex: {:?}", e); + Error::RpcError(e) + })?; + let result = RawTransactionResult { + in_active_chain: tx_info.in_active_chain, + hex: tx_info.hex, + txid: tx_info.txid, + hash: tx_info.hash, + size: tx_info.size, + vsize: tx_info.vsize, + version: tx_info.version, + locktime: tx_info.locktime, + vin: tx_info.vin, + vout: tx_info.vout, + blockhash: tx_info.blockhash, + confirmations: tx_info.confirmations, + time: tx_info.time, + blocktime: tx_info.blocktime, + }; + Ok(TransactionResponse::TransactionDetails(Box::new(result))) + } +} + +async fn validate(ctx: Arc, hex: String) -> Result<()> { + if !hex.starts_with("040000000001") { + return Ok(()); + } + let data = hex::decode(hex)?; + println!("decode_hex {:?}", data); + let trx = deserialize::(&data)?; + let bytes = trx.output[0].clone().script_pubkey.into_bytes(); + let tx: Option = if bytes.len() > 2 && bytes[0] == 0x6a && bytes[1] <= 0x4e { + let offset = 1 + match bytes[1] { + 0x4c => 2, + 0x4d => 3, + 0x4e => 4, + _ => 1, + }; + + let raw_tx = &bytes[offset..]; + Some(deserialize::(raw_tx)?) + } else { + return Ok(()); + }; + + if let Some(tx) = tx { + if let DfTx::CompositeSwap(composite_swap) = tx { + if composite_swap.pools.as_ref().is_empty() { + return Ok(()); + } + let pool_id = composite_swap.pools.iter().last().unwrap(); + let tokio_id = composite_swap.pool_swap.to_token_id.0.to_string(); + let pool_pair = ctx + .client + .get_pool_pair(pool_id.to_string(), Some(true)) + .await?; + for (_, pool_pair_info) in pool_pair.0 { + if pool_pair_info.id_token_a.eq(&tokio_id) + || pool_pair_info.id_token_b.eq(&tokio_id) + { + println!("Found a match: {:?}", pool_pair_info); + } + } + Ok(()) + } else { + Err(Error::BadRequest( + "Transaction is not a composite swap".to_string(), + )) + } + } else { + Ok(()) + } } pub fn router(ctx: Arc) -> Router { + println!("{:?}", ctx.network); Router::new() - .route("/send", post(send_rawtx)) - .route("/test", get(test_rawtx)) - .route("/:txid", get(get_rawtx)) + .route("/send", post(send_raw_tx)) + .route("/test", post(test_raw_tx)) + .route("/:txid", get(get_raw_tx)) + .layer(Extension(ctx)) } diff --git a/lib/ain-ocean/src/error.rs b/lib/ain-ocean/src/error.rs index 01f157c768..4ad4ccf3c7 100644 --- a/lib/ain-ocean/src/error.rs +++ b/lib/ain-ocean/src/error.rs @@ -26,6 +26,8 @@ pub enum NotFoundKind { Token, #[error("poolpair")] PoolPair, + #[error("rawtx")] + RawTx, } #[derive(Error, Debug)] @@ -70,6 +72,10 @@ pub enum Error { TryFromIntError(#[from] std::num::TryFromIntError), #[error(transparent)] Other(#[from] anyhow::Error), + #[error("Validation error: {0}")] + ValidationError(String), + #[error("{0}")] + BadRequest(String), } #[derive(Serialize)] @@ -146,6 +152,7 @@ impl Error { ) } Error::NotFound(_) => (StatusCode::NOT_FOUND, format!("{self}")), + Error::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()), _ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), }; (code, reason) diff --git a/lib/ain-ocean/src/model/mod.rs b/lib/ain-ocean/src/model/mod.rs index 44afffc91c..9ad04ef098 100644 --- a/lib/ain-ocean/src/model/mod.rs +++ b/lib/ain-ocean/src/model/mod.rs @@ -12,6 +12,7 @@ mod poolswap; mod poolswap_aggregated; mod price_ticker; mod raw_block; +mod raw_tx; mod script_activity; mod script_aggregation; mod script_unspent; @@ -33,6 +34,7 @@ pub use oracle_token_currency::*; pub use poolswap::*; pub use poolswap_aggregated::*; pub use price_ticker::*; +pub use raw_tx::*; // pub use raw_block::*; // pub use script_activity::*; // pub use script_aggregation::*; diff --git a/lib/ain-ocean/src/model/raw_tx.rs b/lib/ain-ocean/src/model/raw_tx.rs new file mode 100644 index 0000000000..5d4f2e9e7f --- /dev/null +++ b/lib/ain-ocean/src/model/raw_tx.rs @@ -0,0 +1,42 @@ +use bitcoin::{Amount, Txid}; +use defichain_rpc::json::{GetRawTransactionResultVin, GetRawTransactionResultVout}; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize, Default, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RawTxDto { + pub hex: String, + pub max_fee_rate: Option, +} + +pub fn default_max_fee_rate() -> Amount { + Amount::from_btc(0.1).unwrap_or_default() +} + +#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RawTransactionResult { + pub in_active_chain: Option, + pub hex: Vec, + pub txid: bitcoin::Txid, + pub hash: bitcoin::Wtxid, + pub size: usize, + pub vsize: usize, + pub version: u32, + pub locktime: u32, + pub vin: Vec, + pub vout: Vec, + pub blockhash: Option, + pub confirmations: Option, + pub time: Option, + pub blocktime: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct MempoolAcceptResult { + pub txid: Txid, + pub allowed: bool, + pub reject_reason: Option, + pub vsize: Option, + pub fees: Option, +}