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});