Skip to content

Commit d176c47

Browse files
committed
feat: add registry market statistics
1 parent 0bbd401 commit d176c47

36 files changed

Lines changed: 4118 additions & 50 deletions

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ derive_builder = "0.20.0"
4848
thirtyfour = "0.31.0"
4949
test-context = "0.3.0"
5050
portpicker = "0.1.1"
51-
rain-erc = "0.1.1"
51+
rain-erc = "0.1.5"
5252
rain-math-float = "0.1.7"
5353
rain-error-decoding = "0.1.2"
5454
# Direct dep required: the #[wasm_bindgen] macro emits `::wasm_bindgen` paths, and

crates/common/ARCHITECTURE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ require its `COMMIT_SHA` env var.
3838
prepare batch withdraw calldata; expose WASM‑friendly structs. The `local_db/`
3939
subtree is split into `state.rs` (runtime state, query routing via
4040
`LocalDbState`/`QuerySource`/`SyncReadiness`) and `status.rs` (UI
41-
status‑reporting types).
41+
status‑reporting types). The `markets/` subtree discovers direct quote-token
42+
pairs from active indexed orders and assembles normalized order books, trades,
43+
and rolling statistics without application-specific token semantics. Registry
44+
token data identifies each network's quote token and enriches indexed
45+
metadata; it does not decide which base tokens are listed.
4246
- `dotrain_order` — Parse and validate a DOTRAIN config; compose
4347
scenarios/deployments to Rainlang; fetch authoring metadata and pragma words;
4448
merge additional settings.

crates/common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ csv = { workspace = true }
4343
chrono = { workspace = true }
4444
futures = { workspace = true }
4545
rain-error-decoding = { workspace = true }
46+
rain-erc = { workspace = true }
4647
rain-interpreter-eval = { workspace = true }
4748
wasm-bindgen = { workspace = true }
4849
wasm-bindgen-utils = { workspace = true }
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
use crate::local_db::query::fetch_order_trades::LocalDbOrderTrade;
2+
use crate::local_db::query::{SqlBuildError, SqlStatement, SqlValue};
3+
use alloy::primitives::Address;
4+
5+
const QUERY_TEMPLATE: &str = include_str!("query.sql");
6+
const RAINDEXES_CLAUSE: &str = "/*RAINDEXES_CLAUSE*/";
7+
const RAINDEXES_CLAUSE_BODY: &str = "AND tws.raindex_address IN ({list})";
8+
const PAIR_CLAUSE: &str = "/*PAIR_CLAUSE*/";
9+
10+
#[derive(Debug, Clone)]
11+
pub struct FetchLatestTradesPerTokenArgs {
12+
pub chain_id: u32,
13+
pub raindex_addresses: Vec<Address>,
14+
pub quote_token: Address,
15+
pub base_tokens: Vec<Address>,
16+
}
17+
18+
pub fn build_fetch_latest_trades_per_token_stmt(
19+
args: &FetchLatestTradesPerTokenArgs,
20+
) -> Result<SqlStatement, SqlBuildError> {
21+
let mut stmt = SqlStatement::new(QUERY_TEMPLATE);
22+
23+
let quote_partition = push_param(&mut stmt, args.quote_token);
24+
stmt.replace("/*QUOTE_PARTITION*/", &quote_partition)?;
25+
26+
let chain_id = push_param(&mut stmt, args.chain_id);
27+
stmt.replace("/*CHAIN_ID*/", &chain_id)?;
28+
29+
let mut raindexes = args.raindex_addresses.clone();
30+
raindexes.sort_unstable();
31+
raindexes.dedup();
32+
stmt.bind_list_clause(
33+
RAINDEXES_CLAUSE,
34+
RAINDEXES_CLAUSE_BODY,
35+
raindexes.into_iter().map(SqlValue::from),
36+
)?;
37+
38+
let mut base_tokens = args.base_tokens.clone();
39+
base_tokens.sort_unstable();
40+
base_tokens.dedup();
41+
if base_tokens.is_empty() {
42+
stmt.replace(PAIR_CLAUSE, "AND 1 = 0")?;
43+
return Ok(stmt);
44+
}
45+
46+
let input_quote = push_param(&mut stmt, args.quote_token);
47+
let output_bases = push_list(&mut stmt, &base_tokens);
48+
let output_quote = push_param(&mut stmt, args.quote_token);
49+
let input_bases = push_list(&mut stmt, &base_tokens);
50+
stmt.replace(
51+
PAIR_CLAUSE,
52+
&format!(
53+
"AND ((tws.input_token = {input_quote} AND tws.output_token IN ({output_bases})) \
54+
OR (tws.output_token = {output_quote} AND tws.input_token IN ({input_bases})))"
55+
),
56+
)?;
57+
Ok(stmt)
58+
}
59+
60+
fn push_param(stmt: &mut SqlStatement, value: impl Into<SqlValue>) -> String {
61+
let placeholder = format!("?{}", stmt.params().len() + 1);
62+
stmt.push(value.into());
63+
placeholder
64+
}
65+
66+
fn push_list(stmt: &mut SqlStatement, values: &[Address]) -> String {
67+
values
68+
.iter()
69+
.map(|value| push_param(stmt, *value))
70+
.collect::<Vec<_>>()
71+
.join(", ")
72+
}
73+
74+
pub type LatestTradeRow = LocalDbOrderTrade;
75+
76+
#[cfg(test)]
77+
mod tests {
78+
use super::*;
79+
use alloy::primitives::address;
80+
81+
#[test]
82+
fn query_selects_one_latest_trade_per_base_token() {
83+
let quote = address!("2222222222222222222222222222222222222222");
84+
let base_a = address!("1111111111111111111111111111111111111111");
85+
let base_b = address!("3333333333333333333333333333333333333333");
86+
let raindex = address!("4444444444444444444444444444444444444444");
87+
88+
let stmt = build_fetch_latest_trades_per_token_stmt(&FetchLatestTradesPerTokenArgs {
89+
chain_id: 8453,
90+
raindex_addresses: vec![raindex, raindex],
91+
quote_token: quote,
92+
base_tokens: vec![base_b, base_a, base_a],
93+
})
94+
.unwrap();
95+
96+
assert!(stmt.sql.contains("ROW_NUMBER() OVER"));
97+
assert!(stmt.sql.contains("market_rank = 1"));
98+
assert!(stmt.sql.contains("tws.raindex_address IN (?3)"));
99+
assert!(stmt.sql.contains("tws.output_token IN (?5, ?6)"));
100+
assert!(stmt.sql.contains("tws.input_token IN (?8, ?9)"));
101+
assert_eq!(stmt.params().len(), 9);
102+
assert!(!stmt.sql.contains("/*"));
103+
}
104+
105+
#[test]
106+
fn empty_base_tokens_build_a_match_none_query() {
107+
let stmt = build_fetch_latest_trades_per_token_stmt(&FetchLatestTradesPerTokenArgs {
108+
chain_id: 8453,
109+
raindex_addresses: vec![],
110+
quote_token: Address::ZERO,
111+
base_tokens: vec![],
112+
})
113+
.unwrap();
114+
115+
assert!(stmt.sql.contains("AND 1 = 0"));
116+
assert!(!stmt.sql.contains("/*"));
117+
}
118+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
WITH ranked_trades AS (
2+
SELECT
3+
tws.*,
4+
ROW_NUMBER() OVER (
5+
PARTITION BY CASE
6+
WHEN tws.input_token = /*QUOTE_PARTITION*/ THEN tws.output_token
7+
ELSE tws.input_token
8+
END
9+
ORDER BY
10+
tws.block_timestamp DESC,
11+
tws.block_number DESC,
12+
tws.log_index DESC,
13+
tws.trade_kind,
14+
tws.trade_side
15+
) AS market_rank
16+
FROM derived_trades tws
17+
WHERE tws.chain_id = /*CHAIN_ID*/
18+
/*RAINDEXES_CLAUSE*/
19+
/*PAIR_CLAUSE*/
20+
)
21+
SELECT
22+
tws.chain_id,
23+
tws.trade_kind,
24+
tws.raindex_address AS raindex,
25+
tws.order_hash,
26+
tws.order_owner,
27+
tws.order_nonce,
28+
tws.transaction_hash,
29+
tws.log_index,
30+
tws.block_number,
31+
tws.block_timestamp,
32+
tws.transaction_sender,
33+
tws.input_vault_id,
34+
tws.input_token,
35+
tok_in.name AS input_token_name,
36+
tok_in.symbol AS input_token_symbol,
37+
tok_in.decimals AS input_token_decimals,
38+
tws.input_delta,
39+
tws.input_running_balance,
40+
tws.output_vault_id,
41+
tws.output_token,
42+
tok_out.name AS output_token_name,
43+
tok_out.symbol AS output_token_symbol,
44+
tok_out.decimals AS output_token_decimals,
45+
tws.output_delta,
46+
tws.output_running_balance,
47+
tws.trade_id
48+
FROM ranked_trades tws
49+
LEFT JOIN erc20_tokens tok_in
50+
ON tok_in.chain_id = tws.chain_id
51+
AND tok_in.raindex_address = tws.raindex_address
52+
AND tok_in.token_address = tws.input_token
53+
LEFT JOIN erc20_tokens tok_out
54+
ON tok_out.chain_id = tws.chain_id
55+
AND tok_out.raindex_address = tws.raindex_address
56+
AND tok_out.token_address = tws.output_token
57+
WHERE tws.market_rank = 1
58+
ORDER BY
59+
tws.block_timestamp DESC,
60+
tws.block_number DESC,
61+
tws.log_index DESC,
62+
tws.trade_kind,
63+
tws.trade_side;

crates/common/src/local_db/query/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod fetch_all_tokens;
77
pub mod fetch_db_metadata;
88
pub mod fetch_erc20_tokens_by_addresses;
99
pub mod fetch_last_synced_block;
10+
pub mod fetch_latest_trades_per_token;
1011
pub mod fetch_order_trades;
1112
pub mod fetch_order_trades_count;
1213
pub mod fetch_order_vaults_volume;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use crate::local_db::query::fetch_latest_trades_per_token::{
2+
build_fetch_latest_trades_per_token_stmt, FetchLatestTradesPerTokenArgs, LatestTradeRow,
3+
};
4+
use crate::local_db::query::{LocalDbQueryError, LocalDbQueryExecutor};
5+
use crate::utils::timing::Timing;
6+
7+
pub async fn fetch_latest_trades_per_token<E: LocalDbQueryExecutor + ?Sized>(
8+
exec: &E,
9+
args: FetchLatestTradesPerTokenArgs,
10+
) -> Result<Vec<LatestTradeRow>, LocalDbQueryError> {
11+
if args.base_tokens.is_empty() {
12+
return Ok(Vec::new());
13+
}
14+
let started = Timing::now();
15+
let base_tokens_count = args.base_tokens.len();
16+
let raindexes_count = args.raindex_addresses.len();
17+
let stmt = build_fetch_latest_trades_per_token_stmt(&args)?;
18+
let trades = exec.query_json::<Vec<LatestTradeRow>>(&stmt).await?;
19+
tracing::info!(
20+
chain_id = args.chain_id,
21+
base_tokens_count,
22+
raindexes_count,
23+
rows = trades.len(),
24+
duration_ms = started.elapsed_ms(),
25+
"local DB latest market trades fetch completed"
26+
);
27+
Ok(trades)
28+
}

crates/common/src/raindex_client/local_db/query/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod create_tables;
33
pub mod fetch_all_tokens;
44
pub mod fetch_erc20_tokens_by_addresses;
55
pub mod fetch_last_synced_block;
6+
pub mod fetch_latest_trades_per_token;
67
pub mod fetch_order_trades;
78
pub mod fetch_order_trades_count;
89
pub mod fetch_order_vaults_volume;

0 commit comments

Comments
 (0)