Skip to content

Commit 4e3902b

Browse files
nagarajm22Jouzo
andauthored
RawTx API (#2921)
* ocean rawtx api * updated validate * updated api for raw_trasaction * fixed get_raw_tx * updated request to json rawtx * updated raw tx api * added validate to rawtx * update send_rawtx * fixed clippy * resolved issue * fixed get method * fixed validate in raw_tx * fixed pool format * fixed rawtx bad request test cases * fixed validate return type * updated send error message * fixed lint issue --------- Co-authored-by: jouzo <jdesclercs@gmail.com>
1 parent ae6d0a3 commit 4e3902b

10 files changed

Lines changed: 277 additions & 25 deletions

File tree

lib/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/ain-dftx/src/types/pool.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::fmt;
2+
13
use ain_macros::ConsensusEncoding;
24
use bitcoin::{io, ScriptBuf};
35

@@ -65,3 +67,8 @@ pub struct PoolUpdatePair {
6567
pub owner_address: ScriptBuf,
6668
pub custom_rewards: Maybe<CompactVec<TokenBalanceUInt32>>,
6769
}
70+
impl fmt::Display for PoolId {
71+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72+
write!(f, "PoolId: {:?}", self.id)
73+
}
74+
}

lib/ain-ocean/src/api/mod.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ use axum::{extract::Request, http::StatusCode, response::IntoResponse, Json, Rou
44

55
// mod address;
66
mod block;
7+
mod cache;
8+
pub mod common;
79
mod fee;
810
mod governance;
911
mod loan;
1012
mod masternode;
1113
mod oracle;
14+
mod path;
1215
mod pool_pair;
1316
pub mod prices;
14-
// mod rawtx;
15-
mod cache;
16-
pub mod common;
17-
mod path;
1817
mod query;
18+
mod rawtx;
1919
mod response;
2020
mod stats;
2121
mod tokens;
@@ -81,7 +81,7 @@ pub async fn ocean_router(
8181
.nest("/oracles", oracle::router(Arc::clone(&context)))
8282
.nest("/poolpairs", pool_pair::router(Arc::clone(&context)))
8383
.nest("/prices", prices::router(Arc::clone(&context)))
84-
// .nest("/rawtx", rawtx::router(Arc::clone(&context)))
84+
.nest("/rawtx", rawtx::router(Arc::clone(&context)))
8585
.nest("/stats", stats::router(Arc::clone(&context)))
8686
.nest("/tokens", tokens::router(Arc::clone(&context)))
8787
.nest("/transactions", transactions::router(Arc::clone(&context)))

lib/ain-ocean/src/api/pool_pair/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use path::{
1919
SwapPathsResponse,
2020
};
2121
use petgraph::graphmap::UnGraphMap;
22+
use price::DexPriceResponse;
2223
use rust_decimal::Decimal;
2324
use serde::{Deserialize, Serialize};
2425
use serde_json::json;
@@ -44,8 +45,6 @@ use crate::{
4445
Result, TokenIdentifier,
4546
};
4647

47-
use price::DexPriceResponse;
48-
4948
pub mod path;
5049
pub mod price;
5150
pub mod service;

lib/ain-ocean/src/api/pool_pair/price.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
use serde::Serialize;
21
use std::{collections::HashMap, sync::Arc};
32

43
use defichain_rpc::json::token::TokenInfo;
4+
use serde::Serialize;
55

66
use super::{path::get_best_path, AppContext};
7-
87
use crate::{
98
api::{
109
cache::{get_token_cached, list_token_cached},

lib/ain-ocean/src/api/pool_pair/service.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{collections::HashMap, str::FromStr, sync::Arc};
22

3+
use ain_dftx::{deserialize, pool::CompositeSwap, DfTx, Stack};
34
use anyhow::{format_err, Context};
45
use bitcoin::Txid;
56
use defichain_rpc::{json::poolpair::PoolPairInfo, AccountRPC, BlockchainRPC};
@@ -21,7 +22,6 @@ use crate::{
2122
storage::SortOrder,
2223
Result,
2324
};
24-
use ain_dftx::{deserialize, pool::CompositeSwap, DfTx, Stack};
2525

2626
#[allow(clippy::upper_case_acronyms)]
2727
#[derive(Serialize, Deserialize, Debug, Clone)]

lib/ain-ocean/src/api/rawtx.rs

Lines changed: 209 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,223 @@
1-
use std::sync::Arc;
1+
use std::{result::Result as StdResult, str::FromStr, sync::Arc};
22

3+
use ain_dftx::{deserialize, DfTx};
4+
use ain_macros::ocean_endpoint;
35
use axum::{
6+
extract::{Json, Path},
47
routing::{get, post},
5-
Router,
8+
Extension, Router,
69
};
7-
use defichain_rpc::{Client, RpcApi};
8-
use super::path::Path;
10+
use bitcoin::{Transaction, Txid};
11+
use defichain_rpc::{PoolPairRPC, RpcApi};
12+
use rust_decimal::prelude::ToPrimitive;
13+
use rust_decimal_macros::dec;
14+
use serde::{Deserialize, Serialize, Serializer};
915

10-
async fn send_rawtx() -> String {
11-
"Sending raw transaction".to_string()
16+
use super::{query::Query, response::Response, AppContext};
17+
use crate::{
18+
error::{ApiError, NotFoundKind},
19+
model::{default_max_fee_rate, MempoolAcceptResult, RawTransactionResult, RawTxDto},
20+
Error, Result,
21+
};
22+
23+
enum TransactionResponse {
24+
HexString(String),
25+
TransactionDetails(Box<RawTransactionResult>),
26+
}
27+
28+
#[derive(Deserialize, Default)]
29+
struct QueryParams {
30+
verbose: bool,
1231
}
1332

14-
async fn test_rawtx() -> String {
15-
"Testing raw transaction".to_string()
33+
#[ocean_endpoint]
34+
async fn send_raw_tx(
35+
Extension(ctx): Extension<Arc<AppContext>>,
36+
Json(raw_tx_dto): Json<RawTxDto>,
37+
) -> Result<String> {
38+
validate(ctx.clone(), raw_tx_dto.hex.clone()).await?;
39+
let max_fee = match raw_tx_dto.max_fee_rate {
40+
Some(fee_rate) => {
41+
let sat_per_bitcoin = dec!(100_000_000);
42+
let fee_in_satoshis = fee_rate.checked_mul(sat_per_bitcoin);
43+
match fee_in_satoshis {
44+
Some(value) => Some(value.to_u64().unwrap_or_default()),
45+
None => Some(default_max_fee_rate().to_sat()),
46+
}
47+
}
48+
None => Some(default_max_fee_rate().to_sat()),
49+
};
50+
match ctx
51+
.client
52+
.send_raw_transaction(raw_tx_dto.hex, max_fee)
53+
.await
54+
{
55+
Ok(tx_hash) => Ok(tx_hash.to_string()),
56+
Err(e) => {
57+
eprintln!("Failed to send raw transaction: {:?}", e);
58+
if e.to_string().contains("TX decode failed") {
59+
Err(Error::BadRequest("Transaction decode failed".to_string()))
60+
} else {
61+
Err(Error::RpcError(e))
62+
}
63+
}
64+
}
65+
}
66+
#[ocean_endpoint]
67+
async fn test_raw_tx(
68+
Extension(ctx): Extension<Arc<AppContext>>,
69+
Json(raw_tx_dto): Json<RawTxDto>,
70+
) -> Result<Response<Vec<MempoolAcceptResult>>> {
71+
let trx = defichain_rpc::RawTx::raw_hex(raw_tx_dto.hex);
72+
let max_fee = match raw_tx_dto.max_fee_rate {
73+
Some(fee_rate) => {
74+
let sat_per_bitcoin = dec!(100_000_000);
75+
let fee_in_satoshis = fee_rate.checked_mul(sat_per_bitcoin);
76+
match fee_in_satoshis {
77+
Some(value) => Some(value.to_u64().unwrap_or_default()),
78+
None => Some(default_max_fee_rate().to_sat()),
79+
}
80+
}
81+
None => Some(default_max_fee_rate().to_sat()),
82+
};
83+
match ctx.client.test_mempool_accept(&[trx], max_fee).await {
84+
Ok(mempool_tx) => {
85+
let results = mempool_tx
86+
.into_iter()
87+
.map(|tx_result| MempoolAcceptResult {
88+
txid: tx_result.txid,
89+
allowed: tx_result.allowed,
90+
reject_reason: tx_result.reject_reason,
91+
vsize: tx_result.vsize,
92+
fees: tx_result.fees.map(|f| f.base),
93+
})
94+
.collect::<Vec<MempoolAcceptResult>>();
95+
Ok(Response::new(results))
96+
}
97+
Err(e) => {
98+
eprintln!("Failed to send raw transaction: {:?}", e);
99+
if e.to_string().contains("TX decode failed") {
100+
Err(Error::BadRequest("Transaction decode failed".to_string()))
101+
} else {
102+
Err(Error::RpcError(e))
103+
}
104+
}
105+
}
16106
}
17107

18-
async fn get_rawtx(Path(txid): Path<String>) -> String {
19-
format!("Details of raw transaction with txid {}", txid)
108+
impl Serialize for TransactionResponse {
109+
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error>
110+
where
111+
S: Serializer,
112+
{
113+
match *self {
114+
TransactionResponse::HexString(ref s) => serializer.serialize_str(s),
115+
TransactionResponse::TransactionDetails(ref details) => details.serialize(serializer),
116+
}
117+
}
118+
}
119+
120+
#[ocean_endpoint]
121+
async fn get_raw_tx(
122+
Extension(ctx): Extension<Arc<AppContext>>,
123+
Path(txid): Path<String>,
124+
Query(QueryParams { verbose }): Query<QueryParams>,
125+
) -> Result<TransactionResponse> {
126+
let tx_hash = Txid::from_str(&txid)?;
127+
if !verbose {
128+
let tx_hex = ctx.client.get_raw_transaction_hex(&tx_hash, None).await.map_err(|e| {
129+
if e.to_string().contains("No such mempool or blockchain transaction. Use gettransaction for wallet transactions.") {
130+
Error::NotFound(NotFoundKind::RawTx)
131+
} else {
132+
Error::RpcError(e)
133+
}
134+
})?;
135+
Ok(TransactionResponse::HexString(tx_hex))
136+
} else {
137+
let tx_info = ctx
138+
.client
139+
.get_raw_transaction_info(&tx_hash, None)
140+
.await
141+
.map_err(|e| {
142+
eprintln!("Failed to get raw transaction hex: {:?}", e);
143+
Error::RpcError(e)
144+
})?;
145+
let result = RawTransactionResult {
146+
in_active_chain: tx_info.in_active_chain,
147+
hex: tx_info.hex,
148+
txid: tx_info.txid,
149+
hash: tx_info.hash,
150+
size: tx_info.size,
151+
vsize: tx_info.vsize,
152+
version: tx_info.version,
153+
locktime: tx_info.locktime,
154+
vin: tx_info.vin,
155+
vout: tx_info.vout,
156+
blockhash: tx_info.blockhash,
157+
confirmations: tx_info.confirmations,
158+
time: tx_info.time,
159+
blocktime: tx_info.blocktime,
160+
};
161+
Ok(TransactionResponse::TransactionDetails(Box::new(result)))
162+
}
163+
}
164+
165+
async fn validate(ctx: Arc<AppContext>, hex: String) -> Result<()> {
166+
if !hex.starts_with("040000000001") {
167+
return Ok(());
168+
}
169+
let data = hex::decode(hex)?;
170+
println!("decode_hex {:?}", data);
171+
let trx = deserialize::<Transaction>(&data)?;
172+
let bytes = trx.output[0].clone().script_pubkey.into_bytes();
173+
let tx: Option<DfTx> = if bytes.len() > 2 && bytes[0] == 0x6a && bytes[1] <= 0x4e {
174+
let offset = 1 + match bytes[1] {
175+
0x4c => 2,
176+
0x4d => 3,
177+
0x4e => 4,
178+
_ => 1,
179+
};
180+
181+
let raw_tx = &bytes[offset..];
182+
Some(deserialize::<DfTx>(raw_tx)?)
183+
} else {
184+
return Ok(());
185+
};
186+
187+
if let Some(tx) = tx {
188+
if let DfTx::CompositeSwap(composite_swap) = tx {
189+
if composite_swap.pools.as_ref().is_empty() {
190+
return Ok(());
191+
}
192+
let pool_id = composite_swap.pools.iter().last().unwrap();
193+
let tokio_id = composite_swap.pool_swap.to_token_id.0.to_string();
194+
let pool_pair = ctx
195+
.client
196+
.get_pool_pair(pool_id.to_string(), Some(true))
197+
.await?;
198+
for (_, pool_pair_info) in pool_pair.0 {
199+
if pool_pair_info.id_token_a.eq(&tokio_id)
200+
|| pool_pair_info.id_token_b.eq(&tokio_id)
201+
{
202+
println!("Found a match: {:?}", pool_pair_info);
203+
}
204+
}
205+
Ok(())
206+
} else {
207+
Err(Error::BadRequest(
208+
"Transaction is not a composite swap".to_string(),
209+
))
210+
}
211+
} else {
212+
Ok(())
213+
}
20214
}
21215

22216
pub fn router(ctx: Arc<AppContext>) -> Router {
217+
println!("{:?}", ctx.network);
23218
Router::new()
24-
.route("/send", post(send_rawtx))
25-
.route("/test", get(test_rawtx))
26-
.route("/:txid", get(get_rawtx))
219+
.route("/send", post(send_raw_tx))
220+
.route("/test", post(test_raw_tx))
221+
.route("/:txid", get(get_raw_tx))
222+
.layer(Extension(ctx))
27223
}

lib/ain-ocean/src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub enum NotFoundKind {
2626
Token,
2727
#[error("poolpair")]
2828
PoolPair,
29+
#[error("rawtx")]
30+
RawTx,
2931
}
3032

3133
#[derive(Error, Debug)]
@@ -70,6 +72,10 @@ pub enum Error {
7072
TryFromIntError(#[from] std::num::TryFromIntError),
7173
#[error(transparent)]
7274
Other(#[from] anyhow::Error),
75+
#[error("Validation error: {0}")]
76+
ValidationError(String),
77+
#[error("{0}")]
78+
BadRequest(String),
7379
}
7480

7581
#[derive(Serialize)]
@@ -146,6 +152,7 @@ impl Error {
146152
)
147153
}
148154
Error::NotFound(_) => (StatusCode::NOT_FOUND, format!("{self}")),
155+
Error::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
149156
_ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
150157
};
151158
(code, reason)

lib/ain-ocean/src/model/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ mod poolswap;
1212
mod poolswap_aggregated;
1313
mod price_ticker;
1414
mod raw_block;
15+
mod raw_tx;
1516
mod script_activity;
1617
mod script_aggregation;
1718
mod script_unspent;
@@ -33,6 +34,7 @@ pub use oracle_token_currency::*;
3334
pub use poolswap::*;
3435
pub use poolswap_aggregated::*;
3536
pub use price_ticker::*;
37+
pub use raw_tx::*;
3638
// pub use raw_block::*;
3739
// pub use script_activity::*;
3840
// pub use script_aggregation::*;

0 commit comments

Comments
 (0)