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