penumbra_sdk_asset/asset/
registry.rs

1use std::sync::Arc;
2
3use once_cell::sync::Lazy;
4use regex::{Regex, RegexSet};
5
6use crate::asset::{denom_metadata, Metadata, Unit};
7
8use super::denom_metadata::Inner;
9
10/// A registry of known assets, providing metadata related to a denomination string.
11///
12/// The [`REGISTRY`] constant provides an instance of the registry.
13pub struct Registry {
14    /// Individual regexes for base denominations
15    base_regexes: Vec<Regex>,
16
17    /// Set of regexes that matches any base denomination.
18    base_set: RegexSet,
19
20    /// Individual regexes for the display denominations, grouped by their base denomination.
21    display_regexes: Vec<Vec<Regex>>,
22
23    /// Set of regexes that matches any display denomination.
24    display_set: RegexSet,
25
26    /// Mapping from indices of `display_set` to indices of `base_regexes`.
27    ///
28    /// This allows looking up the base denomination for each display denomination.
29    display_to_base: Vec<usize>,
30
31    /// List of constructors for asset metadata, indexed by base denomination.
32    ///
33    /// Each constructor maps the value of the `data` named capture from the
34    /// base OR display regex to the asset metadata.
35    //
36    // If we wanted to load registry data from a file in the future (would
37    // require working out how to write closures), we could use boxed closures
38    // instead of a function.
39    constructors: Vec<fn(&str) -> denom_metadata::Inner>,
40}
41
42impl Registry {
43    /// Attempt to parse the provided `raw_denom` as a base denomination.
44    ///
45    /// If the denomination is a known base denomination, returns `Some` with
46    /// the parsed base denomination and associated display units.
47    ///
48    /// If the denomination is a known display unit, returns `None`.
49    ///
50    /// If the denomination is unknown, returns `Some` with the parsed base
51    /// denomination and default display denomination (base = display).
52    pub fn parse_denom(&self, raw_denom: &str) -> Option<Metadata> {
53        // We hope that our regexes are disjoint (TODO: add code to test this)
54        // so that there will only ever be one match from the RegexSet.
55
56        if let Some(base_index) = self.base_set.matches(raw_denom).iter().next() {
57            // We've matched a base denomination.
58
59            // Rematch with the specific pattern to obtain captured denomination data.
60            let data = self.base_regexes[base_index]
61                .captures(raw_denom)
62                .expect("already checked this regex matches")
63                .name("data")
64                .map(|m| m.as_str())
65                .unwrap_or("");
66
67            Some(Metadata {
68                inner: Arc::new(self.constructors[base_index](data)),
69            })
70        } else if self.display_set.matches(raw_denom).iter().next().is_some() {
71            // 2. This denom isn't a base denom, it's a display denom
72            None
73        } else {
74            // 3. Fallthrough: create default base denom
75            Some(Metadata {
76                inner: Arc::new(Inner::new(raw_denom.to_string(), Vec::new())),
77            })
78        }
79    }
80
81    /// Parses the provided `raw_unit`, determining whether it is a display unit
82    /// for another denomination or a base denomination itself.
83    ///
84    /// If the denomination is a known display denomination, returns a display
85    /// denomination associated with that display denomination's base
86    /// denomination. Otherwise, returns a display denomination associated with
87    /// the input parsed as a base denomination.
88    pub fn parse_unit(&self, raw_unit: &str) -> Unit {
89        if let Some(display_index) = self.display_set.matches(raw_unit).iter().next() {
90            let base_index = self.display_to_base[display_index];
91            // We need to determine which unit we matched
92            for (unit_index, regex) in self.display_regexes[base_index].iter().enumerate() {
93                if let Some(capture) = regex.captures(raw_unit) {
94                    let data = capture.name("data").map(|m| m.as_str()).unwrap_or("");
95                    return Unit {
96                        inner: Arc::new(self.constructors[base_index](data)),
97                        unit_index,
98                    };
99                }
100            }
101            unreachable!("we matched one of the display regexes");
102        } else {
103            self.parse_denom(raw_unit)
104                .expect("parse_base only returns None on display denom input")
105                .base_unit()
106        }
107    }
108}
109
110#[derive(Default)]
111struct Builder {
112    base_regexes: Vec<&'static str>,
113    constructors: Vec<fn(&str) -> denom_metadata::Inner>,
114    unit_regexes: Vec<Vec<&'static str>>,
115}
116
117impl Builder {
118    /// Add an asset to the registry.
119    ///
120    /// - `base_regex`: matches the base denomination, with optional named capture `data`.
121    /// - `unit_regexes`: match display units, with optional named capture `data`.
122    /// - `constructor`: maps `data` captured by a base OR display regex to the asset metadata,
123    ///    recorded as a `denom::Inner`.
124    ///
125    /// If the `data` capture is present in *any* base or display regex, it must
126    /// match *exactly* the same pattern in all of them, as it is the input to
127    /// the constructor.  Also, the `units` passed to `denom::Inner` must be in
128    /// the same order as the `display_regexes`.
129    fn add_asset(
130        mut self,
131        base_regex: &'static str,
132        unit_regexes: &[&'static str],
133        constructor: fn(&str) -> denom_metadata::Inner,
134    ) -> Self {
135        self.base_regexes.push(base_regex);
136        self.constructors.push(constructor);
137        self.unit_regexes.push(unit_regexes.to_vec());
138
139        self
140    }
141
142    fn build(self) -> Registry {
143        let mut display_to_base = Vec::new();
144        let mut display_regexes = Vec::new();
145        for (base_index, displays) in self.unit_regexes.iter().enumerate() {
146            for _d in displays.iter() {
147                display_to_base.push(base_index);
148            }
149            display_regexes.push(
150                displays
151                    .iter()
152                    .map(|d| Regex::new(d).expect("unable to parse display regex"))
153                    .collect(),
154            );
155        }
156
157        Registry {
158            base_set: RegexSet::new(self.base_regexes.iter())
159                .expect("unable to parse base regexes"),
160            base_regexes: self
161                .base_regexes
162                .iter()
163                .map(|r| Regex::new(r).expect("unable to parse base regex"))
164                .collect(),
165            constructors: self.constructors,
166            display_set: RegexSet::new(
167                self.unit_regexes
168                    .iter()
169                    .flat_map(|displays| displays.iter()),
170            )
171            .expect("unable to parse display regexes"),
172            display_to_base,
173            display_regexes,
174        }
175    }
176}
177
178/// A fixed registry of known asset families.
179pub static REGISTRY: Lazy<Registry> = Lazy::new(|| {
180    Builder::default()
181        .add_asset(
182            "^upenumbra$",
183            &["^penumbra$", "^mpenumbra$"],
184            (|data: &str| {
185                assert!(data.is_empty());
186                denom_metadata::Inner::new(
187                    "upenumbra".to_string(),
188                    vec![
189                        denom_metadata::BareDenomUnit {
190                            exponent: 6,
191                            denom: "penumbra".to_string(),
192                        },
193                        denom_metadata::BareDenomUnit {
194                            exponent: 3,
195                            denom: "mpenumbra".to_string(),
196                        },
197                    ],
198                )
199            }) as for<'r> fn(&'r str) -> _,
200        )
201        .add_asset(
202            "^ugm$",
203            &["^gm$", "^mgm$"],
204            (|data: &str| {
205                assert!(data.is_empty());
206                denom_metadata::Inner::new(
207                    "ugm".to_string(),
208                    vec![
209                        denom_metadata::BareDenomUnit {
210                            exponent: 6,
211                            denom: "gm".to_string(),
212                        },
213                        denom_metadata::BareDenomUnit {
214                            exponent: 3,
215                            denom: "mgm".to_string(),
216                        },
217                    ],
218                )
219            }) as for<'r> fn(&'r str) -> _,
220        )
221        .add_asset(
222            "^ugn$",
223            &["^gn$", "^mgn$"],
224            (|data: &str| {
225                assert!(data.is_empty());
226                denom_metadata::Inner::new(
227                    "ugn".to_string(),
228                    vec![
229                        denom_metadata::BareDenomUnit {
230                            exponent: 6,
231                            denom: "gn".to_string(),
232                        },
233                        denom_metadata::BareDenomUnit {
234                            exponent: 3,
235                            denom: "mgn".to_string(),
236                        },
237                    ],
238                )
239            }) as for<'r> fn(&'r str) -> _,
240        )
241        .add_asset(
242            "^wtest_usd$",
243            &["^test_usd$"],
244            (|data: &str| {
245                assert!(data.is_empty());
246                denom_metadata::Inner::new(
247                    "wtest_usd".to_string(),
248                    vec![
249                        denom_metadata::BareDenomUnit {
250                            exponent: 18,
251                            denom: "test_usd".to_string(),
252                        },
253                    ],
254                )
255            }) as for<'r> fn(&'r str) -> _,
256        )
257        .add_asset(
258            "^wtest_eth$",
259            &["^test_eth$"],
260            (|data: &str| {
261                assert!(data.is_empty());
262                denom_metadata::Inner::new(
263                    "wtest_eth".to_string(),
264                    vec![
265                        denom_metadata::BareDenomUnit {
266                            exponent: 18,
267                            denom: "test_eth".to_string(),
268                        },
269                    ],
270                )
271            }) as for<'r> fn(&'r str) -> _,
272        )
273        .add_asset(
274            "^test_sat$",
275            &["^test_btc$"],
276            (|data: &str| {
277                assert!(data.is_empty());
278                denom_metadata::Inner::new(
279                    "test_sat".to_string(),
280                    vec![
281                        denom_metadata::BareDenomUnit {
282                            exponent: 8,
283                            denom: "test_btc".to_string(),
284                        },
285                    ],
286                )
287            }) as for<'r> fn(&'r str) -> _,
288        )
289        .add_asset(
290            "^utest_atom$",
291            &["^test_atom$", "^mtest_atom$"],
292            (|data: &str| {
293                assert!(data.is_empty());
294                denom_metadata::Inner::new(
295                    "utest_atom".to_string(),
296                    vec![
297                        denom_metadata::BareDenomUnit {
298                            exponent: 6,
299                            denom: "test_atom".to_string(),
300                        },
301                        denom_metadata::BareDenomUnit {
302                            exponent: 3,
303                            denom: "mtest_atom".to_string(),
304                        },
305                    ],
306                )
307            }) as for<'r> fn(&'r str) -> _,
308        )
309        .add_asset(
310            "^utest_osmo$",
311            &["^test_osmo$", "^mtest_osmo$"],
312            (|data: &str| {
313                assert!(data.is_empty());
314                denom_metadata::Inner::new(
315                    "utest_osmo".to_string(),
316                    vec![
317                        denom_metadata::BareDenomUnit {
318                            exponent: 6,
319                            denom: "test_osmo".to_string(),
320                        },
321                        denom_metadata::BareDenomUnit {
322                            exponent: 3,
323                            denom: "mtest_osmo".to_string(),
324                        },
325                    ],
326                )
327            }) as for<'r> fn(&'r str) -> _,
328        )
329        .add_asset(
330            // Note: this regex must be in sync with DelegationToken::try_from
331            // and VALIDATOR_IDENTITY_BECH32_PREFIX in the penumbra-stake crate
332            // TODO: this doesn't restrict the length of the bech32 encoding
333            "^udelegation_(?P<data>penumbravalid1[a-zA-HJ-NP-Z0-9]+)$",
334            &[
335                "^delegation_(?P<data>penumbravalid1[a-zA-HJ-NP-Z0-9]+)$",
336                "^mdelegation_(?P<data>penumbravalid1[a-zA-HJ-NP-Z0-9]+)$",
337            ],
338            (|data: &str| {
339                assert!(!data.is_empty());
340                denom_metadata::Inner::new(
341                    format!("udelegation_{data}"),
342                    vec![
343                        denom_metadata::BareDenomUnit {
344                            exponent: 6,
345                            denom: format!("delegation_{data}"),
346                        },
347                        denom_metadata::BareDenomUnit {
348                            exponent: 3,
349                            denom: format!("mdelegation_{data}"),
350                        },
351                    ],
352                )
353            }) as for<'r> fn(&'r str) -> _,
354        )
355        .add_asset(
356            // Note: this regex must be in sync with UnbondingToken::try_from
357            // and VALIDATOR_IDENTITY_BECH32_PREFIX in the penumbra-stake crate
358            // TODO: this doesn't restrict the length of the bech32 encoding
359            "^uunbonding_(?P<data>start_at_(?P<start>[0-9]+)_(?P<validator>penumbravalid1[a-zA-HJ-NP-Z0-9]+))$",
360            &[
361                "^unbonding_(?P<data>start_at_(?P<start>[0-9]+)_(?P<validator>penumbravalid1[a-zA-HJ-NP-Z0-9]+))$",
362                "^munbonding_(?P<data>start_at_(?P<start>[0-9]+)_(?P<validator>penumbravalid1[a-zA-HJ-NP-Z0-9]+))$",
363            ],
364            (|data: &str| {
365                assert!(!data.is_empty());
366                denom_metadata::Inner::new(
367                    format!("uunbonding_{data}"),
368                    vec![
369                        denom_metadata::BareDenomUnit {
370                            exponent: 6,
371                            denom: format!("unbonding_{data}"),
372                        },
373                        denom_metadata::BareDenomUnit {
374                            exponent: 3,
375                            denom: format!("munbonding_{data}"),
376                        },
377                    ],
378                )
379            }) as for<'r> fn(&'r str) -> _,
380        )
381        .add_asset(
382            // Note: this regex must be in sync with LpNft::try_from
383            // and the bech32 prefix for LP IDs defined in the proto crate.
384            // TODO: this doesn't restrict the length of the bech32 encoding
385            "^lpnft_(?P<data>[a-z_0-9]+_plpid1[a-zA-HJ-NP-Z0-9]+)$",
386            &[ /* no display units - nft, unit 1 */ ],
387            (|data: &str| {
388                assert!(!data.is_empty());
389                denom_metadata::Inner::new(format!("lpnft_{data}"), vec![])
390            }) as for<'r> fn(&'r str) -> _,
391        )
392        .add_asset(
393            // Note: this regex must be in sync with ProposalNft::try_from
394            "^proposal_(?P<data>(?P<proposal_id>[0-9]+)_(?P<proposal_state>deposit|unbonding_deposit|passed|failed|slashed))$",
395            &[ /* no display units - nft, unit 1 */ ],
396            (|data: &str| {
397                assert!(!data.is_empty());
398                denom_metadata::Inner::new(format!("proposal_{data}"), vec![])
399            }) as for<'r> fn(&'r str) -> _,
400        )
401        // Note: this regex must be in sync with VoteReceiptToken::try_from
402        .add_asset("^uvoted_on_(?P<data>(?P<proposal_id>[0-9]+))$",
403            &[
404                "^mvoted_on_(?P<data>(?P<proposal_id>[0-9]+))$",
405                "^voted_on_(?P<data>(?P<proposal_id>[0-9]+))$",
406            ],
407            (|data: &str| {
408                assert!(!data.is_empty());
409                denom_metadata::Inner::new(format!("uvoted_on_{data}"), vec![
410                    denom_metadata::BareDenomUnit {
411                        exponent: 6,
412                        denom: format!("voted_on_{data}"),
413                    },
414                    denom_metadata::BareDenomUnit {
415                        exponent: 3,
416                        denom: format!("mvoted_on_{data}"),
417                    },
418                ])
419            }) as for<'r> fn(&'r str) -> _
420        )
421        .add_asset(
422            "^auctionnft_(?P<data>[a-z_0-9]+_pauctid1[a-zA-HJ-NP-Z0-9]+)$",
423            &[ /* no display units - nft, unit 1 */ ],
424            (|data: &str| {
425                assert!(!data.is_empty());
426                denom_metadata::Inner::new(format!("auctionnft_{data}"), vec![])
427            }) as for<'r> fn(&'r str) -> _,
428        )
429        .build()
430});