Skip to content

Commit d412583

Browse files
authored
Merge pull request #439 from namada-net/ian+tiago/recent-tx-filters
Add optional inner-tx kind and token filters to recent-wrappers endpoint#429
2 parents 61259a8 + 3c988c6 commit d412583

File tree

6 files changed

+401
-27
lines changed

6 files changed

+401
-27
lines changed

swagger.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,80 @@ paths:
878878
application/json:
879879
schema:
880880
$ref: "#/components/schemas/WrapperTransaction"
881+
/api/v1/chain/wrapper/recent:
882+
get:
883+
summary: Get the most recent wrapper transactions with optional filtering by inner tx kind and/or transfer token
884+
parameters:
885+
- in: query
886+
name: offset
887+
schema:
888+
type: integer
889+
minimum: 0
890+
maximum: 1000000
891+
default: 0
892+
description: Number of matching wrappers to skip (for pagination)
893+
- in: query
894+
name: size
895+
schema:
896+
type: integer
897+
minimum: 10
898+
maximum: 30
899+
default: 10
900+
description: Number of matching wrappers to return
901+
- in: query
902+
name: kind
903+
schema:
904+
type: array
905+
items:
906+
type: string
907+
enum:
908+
- "transparentTransfer"
909+
- "shieldedTransfer"
910+
- "shieldingTransfer"
911+
- "unshieldingTransfer"
912+
- "mixedTransfer"
913+
- "bond"
914+
- "redelegation"
915+
- "unbond"
916+
- "withdraw"
917+
- "claimRewards"
918+
- "voteProposal"
919+
- "initProposal"
920+
- "changeMetadata"
921+
- "changeCommission"
922+
- "revealPk"
923+
- "ibcMsgTransfer"
924+
- "ibcTransparentTransfer"
925+
- "ibcShieldingTransfer"
926+
- "ibcUnshieldingTransfer"
927+
- "becomeValidator"
928+
- "deactivateValidator"
929+
- "reactivateValidator"
930+
- "unjailValidator"
931+
- "changeConsensusKey"
932+
- "initAccount"
933+
- "unknown"
934+
style: form
935+
explode: false
936+
description: Optionally, filter by inner-tx kind(s) (comma-separated).
937+
- in: query
938+
name: token
939+
schema:
940+
type: array
941+
items:
942+
type: string
943+
style: form
944+
explode: false
945+
description: Optionally, filter by inner-tx transfer token(s) (comma-separated). Using this filter will exclude non-transfer inner-tx kinds from the results.
946+
responses:
947+
"200":
948+
description: List of wrapper transactions with filtered inner transactions
949+
content:
950+
application/json:
951+
schema:
952+
type: array
953+
items:
954+
$ref: "#/components/schemas/WrapperTransaction"
881955
/api/v1/chain/inner/{tx_id}:
882956
get:
883957
summary: Get the inner transaction by hash

webserver/src/dto/transaction.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
22
use subtle_encoding::hex;
33
use validator::Validate;
44

5+
use crate::entity::transaction::TransactionKind;
56
use crate::error::transaction::TransactionError;
67

78
#[derive(Clone, Serialize, Deserialize, Validate)]
@@ -36,6 +37,12 @@ impl TransactionIdParam {
3637
#[derive(Clone, Serialize, Deserialize, Validate)]
3738
#[serde(rename_all = "camelCase")]
3839
pub struct TransactionMostRecentQueryParams {
40+
#[validate(range(min = 0, max = 1000000))]
41+
pub offset: Option<u64>,
3942
#[validate(range(min = 10, max = 30))]
4043
pub size: Option<u64>,
44+
#[serde(default)]
45+
pub kind: Vec<TransactionKind>,
46+
#[serde(default)]
47+
pub token: Vec<String>,
4148
}

webserver/src/entity/transaction.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ use orm::transactions::{
33
InnerTransactionDb, TransactionHistoryDb, TransactionHistoryKindDb,
44
TransactionKindDb, TransactionResultDb, WrapperTransactionDb,
55
};
6+
use serde::{Deserialize, Serialize};
67
use shared::id::Id;
78
use shared::token::{IbcToken, Token};
89

9-
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
10+
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
11+
#[serde(rename_all = "camelCase")]
1012
pub enum TransactionKind {
1113
TransparentTransfer,
1214
ShieldedTransfer,
@@ -75,6 +77,43 @@ impl From<TransactionKindDb> for TransactionKind {
7577
}
7678
}
7779

80+
impl From<TransactionKind> for TransactionKindDb {
81+
fn from(value: TransactionKind) -> Self {
82+
match value {
83+
TransactionKind::TransparentTransfer => Self::TransparentTransfer,
84+
TransactionKind::ShieldedTransfer => Self::ShieldedTransfer,
85+
TransactionKind::ShieldingTransfer => Self::ShieldingTransfer,
86+
TransactionKind::UnshieldingTransfer => Self::UnshieldingTransfer,
87+
TransactionKind::MixedTransfer => Self::MixedTransfer,
88+
TransactionKind::Bond => Self::Bond,
89+
TransactionKind::Redelegation => Self::Redelegation,
90+
TransactionKind::Unbond => Self::Unbond,
91+
TransactionKind::Withdraw => Self::Withdraw,
92+
TransactionKind::ClaimRewards => Self::ClaimRewards,
93+
TransactionKind::VoteProposal => Self::VoteProposal,
94+
TransactionKind::InitProposal => Self::InitProposal,
95+
TransactionKind::ChangeMetadata => Self::ChangeMetadata,
96+
TransactionKind::ChangeCommission => Self::ChangeCommission,
97+
TransactionKind::RevealPk => Self::RevealPk,
98+
TransactionKind::Unknown => Self::Unknown,
99+
TransactionKind::IbcMsgTransfer => Self::IbcMsgTransfer,
100+
TransactionKind::IbcTransparentTransfer => {
101+
Self::IbcTransparentTransfer
102+
}
103+
TransactionKind::IbcShieldingTransfer => Self::IbcShieldingTransfer,
104+
TransactionKind::IbcUnshieldingTransfer => {
105+
Self::IbcUnshieldingTransfer
106+
}
107+
TransactionKind::BecomeValidator => Self::BecomeValidator,
108+
TransactionKind::ReactivateValidator => Self::ReactivateValidator,
109+
TransactionKind::DeactivateValidator => Self::DeactivateValidator,
110+
TransactionKind::UnjailValidator => Self::UnjailValidator,
111+
TransactionKind::ChangeConsensusKey => Self::ChangeConsensusKey,
112+
TransactionKind::InitAccount => Self::InitAccount,
113+
}
114+
}
115+
}
116+
78117
#[derive(Debug, Clone)]
79118
pub struct WrapperTransaction {
80119
pub id: Id,

webserver/src/handler/transaction.rs

Lines changed: 130 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ use axum::extract::{Path, State};
33
use axum::http::HeaderMap;
44
use axum_extra::extract::Query;
55
use axum_macros::debug_handler;
6+
use serde_json;
67

8+
use crate::constant::ITEM_PER_PAGE;
79
use crate::dto::transaction::{
810
TransactionHistoryQueryParams, TransactionIdParam,
911
TransactionMostRecentQueryParams,
1012
};
13+
use crate::entity::transaction::{InnerTransaction, TransactionKind};
1114
use crate::error::api::ApiError;
1215
use crate::error::transaction::TransactionError;
1316
use crate::response::headers;
@@ -103,32 +106,136 @@ pub async fn get_most_recent_transactions(
103106
Query(query): Query<TransactionMostRecentQueryParams>,
104107
State(state): State<CommonState>,
105108
) -> Result<Json<Vec<WrapperTransactionResponse>>, ApiError> {
106-
let size = query.size.unwrap_or(10);
107-
108-
let transactions = state
109-
.transaction_service
110-
.get_most_recent_transactions(size)
111-
.await?;
112-
113-
let inner_txs = transactions
114-
.iter()
115-
.map(|tx| {
116-
state
117-
.transaction_service
118-
.get_inner_tx_by_wrapper_id(tx.id.to_string())
109+
let offset = query.offset.unwrap_or(0);
110+
let size = query.size.unwrap_or(ITEM_PER_PAGE);
111+
let kind = query.kind;
112+
let token = query.token;
113+
114+
let filters_present = !kind.is_empty() || !token.is_empty();
115+
116+
let wrappers = if !filters_present {
117+
state
118+
.transaction_service
119+
.get_most_recent_transactions(offset, size)
120+
.await?
121+
} else {
122+
state
123+
.transaction_service
124+
.get_filtered_most_recent_wrappers(
125+
offset,
126+
size,
127+
kind.clone(),
128+
token.clone(),
129+
)
130+
.await?
131+
};
132+
133+
let inner_futs = wrappers.iter().map(|tx| {
134+
state
135+
.transaction_service
136+
.get_inner_tx_by_wrapper_id(tx.id.to_string())
137+
});
138+
let inner_results = futures::future::join_all(inner_futs).await;
139+
140+
let response = wrappers
141+
.into_iter()
142+
.zip(inner_results.into_iter())
143+
.filter_map(|(tx, inner_res)| {
144+
let mut inners = inner_res.unwrap_or_default();
145+
146+
if filters_present {
147+
if !kind.is_empty() {
148+
inners.retain(|inner_tx| kind.contains(&inner_tx.kind));
149+
}
150+
if !token.is_empty() {
151+
inners.retain(|inner_tx| {
152+
filter_inner_tx_by_tokens(inner_tx, &token)
153+
});
154+
}
155+
156+
if inners.is_empty() {
157+
return None;
158+
}
159+
}
160+
161+
Some(WrapperTransactionResponse::new(tx, inners))
119162
})
120163
.collect::<Vec<_>>();
121164

122-
let inner_txs = futures::future::join_all(inner_txs).await;
165+
Ok(Json(response))
166+
}
123167

124-
let response = transactions
125-
.into_iter()
126-
.zip(inner_txs.into_iter())
127-
.map(|(tx, inner_tx_result)| {
128-
let inner_txs = inner_tx_result.unwrap_or_default();
129-
WrapperTransactionResponse::new(tx, inner_txs)
130-
})
131-
.collect();
168+
/// Filter a single inner transaction by token addresses involved (only
169+
/// applicable to transfer types)
170+
fn filter_inner_tx_by_tokens(
171+
inner_tx: &InnerTransaction,
172+
token_filter: &[String],
173+
) -> bool {
174+
if token_filter.is_empty() {
175+
return true;
176+
}
132177

133-
Ok(Json(response))
178+
let Some(data) = &inner_tx.data else {
179+
return false;
180+
};
181+
182+
let Ok(json_value) = serde_json::from_str::<serde_json::Value>(data) else {
183+
return false;
184+
};
185+
186+
// Check if a candidate token equals any of the filter tokens
187+
let token_matches = |candidate: &str| -> bool {
188+
token_filter
189+
.iter()
190+
.any(|filter_token| candidate.eq_ignore_ascii_case(filter_token))
191+
};
192+
193+
// IBC transfers: data is an array; get token from [0].Ibc.address.Account
194+
let is_ibc_kind = matches!(
195+
inner_tx.kind,
196+
TransactionKind::IbcTransparentTransfer
197+
| TransactionKind::IbcShieldingTransfer
198+
| TransactionKind::IbcUnshieldingTransfer
199+
);
200+
201+
if is_ibc_kind {
202+
if let Some(arr) = json_value.as_array() {
203+
for item in arr {
204+
if let Some(ibc_obj) = item.get("Ibc") {
205+
if let Some(account) = ibc_obj
206+
.get("address")
207+
.and_then(|a| a.get("Account"))
208+
.and_then(|a| a.as_str())
209+
{
210+
if token_matches(account) {
211+
return true;
212+
}
213+
}
214+
}
215+
}
216+
}
217+
218+
// If structure differs unexpectedly, do not match
219+
return false;
220+
}
221+
222+
// Non-IBC transfers: check "sources" array containing entries with a
223+
// "token" field
224+
if let Some(obj) = json_value.as_object() {
225+
if let Some(sources) = obj.get("sources").and_then(|v| v.as_array()) {
226+
for src in sources {
227+
if let Some(token_str) = src
228+
.as_object()
229+
.and_then(|o| o.get("token"))
230+
.and_then(|t| t.as_str())
231+
{
232+
if token_matches(token_str) {
233+
return true;
234+
}
235+
}
236+
}
237+
}
238+
}
239+
240+
false
134241
}

0 commit comments

Comments
 (0)