Skip to main content

chainstream_sdk/stream/
fields.rs

1//! CEL field mappings for stream filter expressions
2//!
3//! This module provides field name mappings used to convert human-readable
4//! field names to their short form for server-side CEL filter expressions.
5
6use std::collections::HashMap;
7
8use once_cell::sync::Lazy;
9
10/// Field mapping type: maps long field names to short field names
11pub type FieldMapping = HashMap<&'static str, &'static str>;
12
13/// Method field mappings type: maps subscription method names to their field mappings
14pub type MethodFieldMappings = HashMap<&'static str, FieldMapping>;
15
16/// CEL field mappings organized by subscription method
17pub static CEL_FIELD_MAPPINGS: Lazy<MethodFieldMappings> = Lazy::new(|| {
18    let mut mappings = HashMap::new();
19
20    // Wallet balance subscription fields
21    mappings.insert(
22        "subscribe_wallet_balance",
23        HashMap::from([
24            ("walletAddress", "a"),
25            ("tokenAddress", "ta"),
26            ("tokenPriceInUsd", "tpiu"),
27            ("balance", "b"),
28            ("timestamp", "t"),
29        ]),
30    );
31
32    // Token candle subscription fields
33    mappings.insert(
34        "subscribe_token_candles",
35        HashMap::from([
36            ("open", "o"),
37            ("close", "c"),
38            ("high", "h"),
39            ("low", "l"),
40            ("volume", "v"),
41            ("resolution", "r"),
42            ("time", "t"),
43            ("number", "n"),
44        ]),
45    );
46
47    // Token stats subscription fields
48    mappings.insert(
49        "subscribe_token_stats",
50        HashMap::from([
51            ("address", "a"),
52            ("timestamp", "t"),
53            ("buys1m", "b1m"),
54            ("sells1m", "s1m"),
55            ("buyers1m", "be1m"),
56            ("sellers1m", "se1m"),
57            ("buyVolumeInUsd1m", "bviu1m"),
58            ("sellVolumeInUsd1m", "sviu1m"),
59            ("price1m", "p1m"),
60            ("openInUsd1m", "oiu1m"),
61            ("closeInUsd1m", "ciu1m"),
62            ("buys5m", "b5m"),
63            ("sells5m", "s5m"),
64            ("buyers5m", "be5m"),
65            ("sellers5m", "se5m"),
66            ("buyVolumeInUsd5m", "bviu5m"),
67            ("sellVolumeInUsd5m", "sviu5m"),
68            ("price5m", "p5m"),
69            ("openInUsd5m", "oiu5m"),
70            ("closeInUsd5m", "ciu5m"),
71            ("buys15m", "b15m"),
72            ("sells15m", "s15m"),
73            ("buyers15m", "be15m"),
74            ("sellers15m", "se15m"),
75            ("buyVolumeInUsd15m", "bviu15m"),
76            ("sellVolumeInUsd15m", "sviu15m"),
77            ("price15m", "p15m"),
78            ("openInUsd15m", "oiu15m"),
79            ("closeInUsd15m", "ciu15m"),
80            ("buys30m", "b30m"),
81            ("sells30m", "s30m"),
82            ("buyers30m", "be30m"),
83            ("sellers30m", "se30m"),
84            ("buyVolumeInUsd30m", "bviu30m"),
85            ("sellVolumeInUsd30m", "sviu30m"),
86            ("price30m", "p30m"),
87            ("openInUsd30m", "oiu30m"),
88            ("closeInUsd30m", "ciu30m"),
89            ("buys1h", "b1h"),
90            ("sells1h", "s1h"),
91            ("buyers1h", "be1h"),
92            ("sellers1h", "se1h"),
93            ("buyVolumeInUsd1h", "bviu1h"),
94            ("sellVolumeInUsd1h", "sviu1h"),
95            ("price1h", "p1h"),
96            ("openInUsd1h", "oiu1h"),
97            ("closeInUsd1h", "ciu1h"),
98            ("buys4h", "b4h"),
99            ("sells4h", "s4h"),
100            ("buyers4h", "be4h"),
101            ("sellers4h", "se4h"),
102            ("buyVolumeInUsd4h", "bviu4h"),
103            ("sellVolumeInUsd4h", "sviu4h"),
104            ("price4h", "p4h"),
105            ("openInUsd4h", "oiu4h"),
106            ("closeInUsd4h", "ciu4h"),
107            ("buys24h", "b24h"),
108            ("sells24h", "s24h"),
109            ("buyers24h", "be24h"),
110            ("sellers24h", "se24h"),
111            ("buyVolumeInUsd24h", "bviu24h"),
112            ("sellVolumeInUsd24h", "sviu24h"),
113            ("price24h", "p24h"),
114            ("price", "p"),
115            ("openInUsd24h", "oiu24h"),
116            ("closeInUsd24h", "ciu24h"),
117        ]),
118    );
119
120    // Token holder subscription fields
121    mappings.insert(
122        "subscribe_token_holders",
123        HashMap::from([
124            ("tokenAddress", "a"),
125            ("holders", "h"),
126            ("top100Amount", "t100a"),
127            ("top10Amount", "t10a"),
128            ("top100Holders", "t100h"),
129            ("top10Holders", "t10h"),
130            ("top100Ratio", "t100r"),
131            ("top10Ratio", "t10r"),
132            ("timestamp", "ts"),
133        ]),
134    );
135
136    // New token subscription fields (dex-new-token, supports CEL filter)
137    mappings.insert(
138        "subscribe_new_token",
139        HashMap::from([
140            ("tokenAddress", "a"),
141            ("name", "n"),
142            ("symbol", "s"),
143            ("decimals", "dec"),
144            ("imageUrl", "iu"),
145            ("description", "de"),
146            ("createdAtMs", "cts"),
147            ("coingeckoCoinId", "cgi"),
148            ("socialMedia.twitter", "sm.tw"),
149            ("socialMedia.telegram", "sm.tg"),
150            ("socialMedia.website", "sm.w"),
151            ("socialMedia.tiktok", "sm.tt"),
152            ("socialMedia.discord", "sm.dc"),
153            ("socialMedia.facebook", "sm.fb"),
154            ("socialMedia.github", "sm.gh"),
155            ("socialMedia.instagram", "sm.ig"),
156            ("socialMedia.linkedin", "sm.li"),
157            ("socialMedia.medium", "sm.md"),
158            ("socialMedia.reddit", "sm.rd"),
159            ("socialMedia.youtube", "sm.yt"),
160            ("socialMedia.bitbucket", "sm.bb"),
161            ("launchFrom.programAddress", "lf.pa"),
162            ("launchFrom.protocolFamily", "lf.pf"),
163            ("launchFrom.protocolName", "lf.pn"),
164            ("migratedTo.programAddress", "mt.pa"),
165            ("migratedTo.protocolFamily", "mt.pf"),
166            ("migratedTo.protocolName", "mt.pn"),
167        ]),
168    );
169
170    // Token supply subscription fields
171    mappings.insert(
172        "subscribe_token_supply",
173        HashMap::from([("tokenAddress", "a"), ("supply", "s"), ("timestamp", "ts")]),
174    );
175
176    // DEX pool balance subscription fields
177    mappings.insert(
178        "subscribe_dex_pool_balance",
179        HashMap::from([
180            ("poolAddress", "a"),
181            ("tokenAAddress", "taa"),
182            ("tokenALiquidityInUsd", "taliu"),
183            ("tokenBAddress", "tba"),
184            ("tokenBLiquidityInUsd", "tbliu"),
185        ]),
186    );
187
188    // Token liquidity subscription fields
189    mappings.insert(
190        "subscribe_token_liquidity",
191        HashMap::from([
192            ("tokenAddress", "a"),
193            ("metricType", "t"),
194            ("value", "v"),
195            ("timestamp", "ts"),
196        ]),
197    );
198
199    // New token metadata subscription fields (dex-new-tokens-metadata)
200    mappings.insert(
201        "subscribe_new_tokens_metadata",
202        HashMap::from([
203            ("tokenAddress", "a"),
204            ("name", "n"),
205            ("decimals", "dec"),
206            ("symbol", "s"),
207            ("imageUrl", "iu"),
208            ("description", "de"),
209            ("createdAtMs", "cts"),
210            ("coingeckoCoinId", "cgi"),
211            ("socialMedia.twitter", "sm.tw"),
212            ("socialMedia.telegram", "sm.tg"),
213            ("socialMedia.website", "sm.w"),
214            ("socialMedia.tiktok", "sm.tt"),
215            ("socialMedia.discord", "sm.dc"),
216            ("socialMedia.facebook", "sm.fb"),
217            ("socialMedia.github", "sm.gh"),
218            ("socialMedia.instagram", "sm.ig"),
219            ("socialMedia.linkedin", "sm.li"),
220            ("socialMedia.medium", "sm.md"),
221            ("socialMedia.reddit", "sm.rd"),
222            ("socialMedia.youtube", "sm.yt"),
223            ("socialMedia.bitbucket", "sm.bb"),
224            ("launchFrom.programAddress", "lf.pa"),
225            ("launchFrom.protocolFamily", "lf.pf"),
226            ("launchFrom.protocolName", "lf.pn"),
227            ("migratedTo.programAddress", "mt.pa"),
228            ("migratedTo.protocolFamily", "mt.pf"),
229            ("migratedTo.protocolName", "mt.pn"),
230        ]),
231    );
232
233    // New tokens list subscription fields (dex-new-tokens)
234    mappings.insert(
235        "subscribe_new_tokens",
236        HashMap::from([
237            ("tokenAddress", "a"),
238            ("name", "n"),
239            ("decimals", "dec"),
240            ("symbol", "s"),
241            ("imageUrl", "iu"),
242            ("description", "de"),
243            ("createdAtMs", "cts"),
244            ("coingeckoCoinId", "cgi"),
245            ("socialMedia.twitter", "sm.tw"),
246            ("socialMedia.telegram", "sm.tg"),
247            ("socialMedia.website", "sm.w"),
248            ("socialMedia.tiktok", "sm.tt"),
249            ("socialMedia.discord", "sm.dc"),
250            ("socialMedia.facebook", "sm.fb"),
251            ("socialMedia.github", "sm.gh"),
252            ("socialMedia.instagram", "sm.ig"),
253            ("socialMedia.linkedin", "sm.li"),
254            ("socialMedia.medium", "sm.md"),
255            ("socialMedia.reddit", "sm.rd"),
256            ("socialMedia.youtube", "sm.yt"),
257            ("socialMedia.bitbucket", "sm.bb"),
258            ("launchFrom.programAddress", "lf.pa"),
259            ("launchFrom.protocolFamily", "lf.pf"),
260            ("launchFrom.protocolName", "lf.pn"),
261            ("migratedTo.programAddress", "mt.pa"),
262            ("migratedTo.protocolFamily", "mt.pf"),
263            ("migratedTo.protocolName", "mt.pn"),
264        ]),
265    );
266
267    // Token trade subscription fields
268    mappings.insert(
269        "subscribe_token_trades",
270        HashMap::from([
271            ("tokenAddress", "a"),
272            ("timestamp", "t"),
273            ("kind", "k"),
274            ("buyAmount", "ba"),
275            ("buyAmountInUsd", "baiu"),
276            ("buyTokenAddress", "btma"),
277            ("buyTokenName", "btn"),
278            ("buyTokenSymbol", "bts"),
279            ("buyWalletAddress", "bwa"),
280            ("sellAmount", "sa"),
281            ("sellAmountInUsd", "saiu"),
282            ("sellTokenAddress", "stma"),
283            ("sellTokenName", "stn"),
284            ("sellTokenSymbol", "sts"),
285            ("sellWalletAddress", "swa"),
286            ("txHash", "h"),
287        ]),
288    );
289
290    // Wallet token PnL subscription fields
291    mappings.insert(
292        "subscribe_wallet_pnl",
293        HashMap::from([
294            ("walletAddress", "a"),
295            ("tokenAddress", "ta"),
296            ("tokenPriceInUsd", "tpiu"),
297            ("timestamp", "t"),
298            ("opentime", "ot"),
299            ("lasttime", "lt"),
300            ("closetime", "ct"),
301            ("buyAmount", "ba"),
302            ("buyAmountInUsd", "baiu"),
303            ("buyCount", "bs"),
304            ("buyCount30d", "bs30d"),
305            ("buyCount7d", "bs7d"),
306            ("sellAmount", "sa"),
307            ("sellAmountInUsd", "saiu"),
308            ("sellCount", "ss"),
309            ("sellCount30d", "ss30d"),
310            ("sellCount7d", "ss7d"),
311            ("heldDurationTimestamp", "hdts"),
312            ("averageBuyPriceInUsd", "abpiu"),
313            ("averageSellPriceInUsd", "aspiu"),
314            ("unrealizedProfitInUsd", "upiu"),
315            ("unrealizedProfitRatio", "upr"),
316            ("realizedProfitInUsd", "rpiu"),
317            ("realizedProfitRatio", "rpr"),
318            ("totalRealizedProfitInUsd", "trpiu"),
319            ("totalRealizedProfitRatio", "trr"),
320        ]),
321    );
322
323    // Wallet trade subscription fields
324    mappings.insert(
325        "subscribe_wallet_trade",
326        HashMap::from([
327            ("tokenAddress", "a"),
328            ("timestamp", "t"),
329            ("kind", "k"),
330            ("buyAmount", "ba"),
331            ("buyAmountInUsd", "baiu"),
332            ("buyTokenAddress", "btma"),
333            ("buyTokenName", "btn"),
334            ("buyTokenSymbol", "bts"),
335            ("buyWalletAddress", "bwa"),
336            ("sellAmount", "sa"),
337            ("sellAmountInUsd", "saiu"),
338            ("sellTokenAddress", "stma"),
339            ("sellTokenName", "stn"),
340            ("sellTokenSymbol", "sts"),
341            ("sellWalletAddress", "swa"),
342            ("txHash", "h"),
343        ]),
344    );
345
346    // Token max liquidity subscription fields
347    mappings.insert(
348        "subscribe_token_max_liquidity",
349        HashMap::from([
350            ("tokenAddress", "a"),
351            ("poolAddress", "p"),
352            ("liquidityInUsd", "liu"),
353            ("liquidityInNative", "lin"),
354            ("timestamp", "ts"),
355        ]),
356    );
357
358    // Token total liquidity subscription fields
359    mappings.insert(
360        "subscribe_token_total_liquidity",
361        HashMap::from([
362            ("tokenAddress", "a"),
363            ("liquidityInUsd", "liu"),
364            ("liquidityInNative", "lin"),
365            ("poolCount", "pc"),
366            ("timestamp", "ts"),
367        ]),
368    );
369
370    mappings
371});
372
373/// Get field mappings for a specific subscription method
374pub fn get_field_mappings(method_name: &str) -> Option<&FieldMapping> {
375    CEL_FIELD_MAPPINGS.get(method_name)
376}
377
378/// Replace long field names with short field names in filter expressions
379/// Automatically adds meta. prefix if not present
380/// Supports nested field paths (e.g., launchFrom.protocolFamily -> lf.pf)
381pub fn replace_filter_fields(filter: &str, method_name: &str) -> String {
382    if filter.is_empty() {
383        return filter.to_string();
384    }
385
386    let field_mappings = match get_field_mappings(method_name) {
387        Some(m) => m,
388        None => return filter.to_string(),
389    };
390
391    let mut result = filter.to_string();
392
393    // Sort entries by key length descending to replace longer (nested) paths first
394    let mut entries: Vec<_> = field_mappings.iter().collect();
395    entries.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
396
397    for (long_field, short_field) in entries {
398        // Handle two cases: with and without meta. prefix
399        // Pattern 1: meta.fieldName (with meta. prefix) — check first to avoid double meta.
400        let pattern1 = format!(r"\bmeta\.{}\b", regex::escape(long_field));
401        if let Ok(re) = regex::Regex::new(&pattern1) {
402            result = re
403                .replace_all(&result, format!("meta.{}", short_field))
404                .to_string();
405        }
406
407        // Pattern 2: fieldName (without meta. prefix)
408        let pattern2 = format!(r"\b{}\b", regex::escape(long_field));
409        if let Ok(re) = regex::Regex::new(&pattern2) {
410            result = re
411                .replace_all(&result, format!("meta.{}", short_field))
412                .to_string();
413        }
414    }
415
416    result
417}
418
419/// Get available field names for a specific subscription method
420pub fn get_available_fields(method_name: &str) -> Vec<&'static str> {
421    match get_field_mappings(method_name) {
422        Some(mappings) => mappings.keys().copied().collect(),
423        None => Vec::new(),
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn test_get_field_mappings() {
433        let mappings = get_field_mappings("subscribe_token_candles");
434        assert!(mappings.is_some());
435        let mappings = mappings.unwrap();
436        assert_eq!(mappings.get("open"), Some(&"o"));
437        assert_eq!(mappings.get("close"), Some(&"c"));
438    }
439
440    #[test]
441    fn test_replace_filter_fields() {
442        let filter = "open > 100 && close < 200";
443        let result = replace_filter_fields(filter, "subscribe_token_candles");
444        assert!(result.contains("meta.o"));
445        assert!(result.contains("meta.c"));
446    }
447
448    #[test]
449    fn test_get_available_fields() {
450        let fields = get_available_fields("subscribe_token_candles");
451        assert!(fields.contains(&"open"));
452        assert!(fields.contains(&"close"));
453        assert!(fields.contains(&"high"));
454        assert!(fields.contains(&"low"));
455    }
456}