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