iota_genesis_builder/stardust/native_token/
package_data.rs1use anyhow::Result;
9use iota_stardust_types::block::{
10 address::AliasAddress,
11 output::{FoundryId, FoundryOutput, feature::Irc30Metadata},
12};
13use move_compiler::parser::keywords;
14use rand::distributions::{Alphanumeric, DistString};
15use rand_pcg::Pcg64;
16use rand_seeder::{Seeder, SipRng, rand_core::RngCore};
17use regex::Regex;
18
19use crate::stardust::types::{error::StardustError, token_scheme::SimpleTokenSchemeU64};
20
21#[derive(Debug)]
24pub struct NativeTokenPackageData {
25 package_name: String,
26 module: NativeTokenModuleData,
27}
28
29impl NativeTokenPackageData {
30 pub fn new(package_name: impl Into<String>, module: NativeTokenModuleData) -> Self {
32 Self {
33 package_name: package_name.into(),
34 module,
35 }
36 }
37
38 pub fn package_name(&self) -> &String {
40 &self.package_name
41 }
42
43 pub fn module(&self) -> &NativeTokenModuleData {
45 &self.module
46 }
47}
48
49#[derive(Debug)]
52pub struct NativeTokenModuleData {
53 pub foundry_id: FoundryId,
54 pub module_name: String,
55 pub otw_name: String,
56 pub decimals: u8,
57 pub symbol: String,
59 pub circulating_supply: u64,
60 pub maximum_supply: u64,
61 pub coin_name: String,
63 pub coin_description: String,
65 pub icon_url: Option<String>,
66 pub alias_address: AliasAddress,
67}
68
69impl NativeTokenModuleData {
70 pub fn new(
72 foundry_id: FoundryId,
73 module_name: impl Into<String>,
74 otw_name: impl Into<String>,
75 decimals: u8,
76 symbol: impl Into<String>,
77 circulating_supply: u64,
78 maximum_supply: u64,
79 coin_name: impl Into<String>,
80 coin_description: impl Into<String>,
81 icon_url: Option<String>,
82 alias_address: AliasAddress,
83 ) -> Self {
84 Self {
85 foundry_id,
86 module_name: module_name.into(),
87 otw_name: otw_name.into(),
88 decimals,
89 symbol: symbol.into(),
90 circulating_supply,
91 maximum_supply,
92 coin_name: coin_name.into(),
93 coin_description: coin_description.into(),
94 icon_url,
95 alias_address,
96 }
97 }
98}
99
100impl TryFrom<&FoundryOutput> for NativeTokenPackageData {
101 type Error = StardustError;
102 fn try_from(output: &FoundryOutput) -> Result<Self, StardustError> {
103 let irc_30_metadata = extract_irc30_metadata(output);
104
105 let identifier = derive_foundry_package_lowercase_identifier(
108 irc_30_metadata.symbol(),
109 output.id().as_slice(),
110 );
111
112 let decimals = u8::try_from(*irc_30_metadata.decimals()).unwrap_or_default();
115
116 let token_scheme_u64: SimpleTokenSchemeU64 =
117 output.token_scheme().as_simple().try_into()?;
118
119 let native_token_data = NativeTokenPackageData {
120 package_name: identifier.clone(),
121 module: NativeTokenModuleData {
122 foundry_id: output.id(),
123 module_name: identifier.clone(),
124 otw_name: identifier.clone().to_ascii_uppercase(),
125 decimals,
126 symbol: identifier,
127 circulating_supply: token_scheme_u64.circulating_supply(),
128 maximum_supply: token_scheme_u64.maximum_supply(),
129 coin_name: irc_30_metadata.name().clone(),
130 coin_description: irc_30_metadata.description().clone().unwrap_or_default(),
131 icon_url: irc_30_metadata.logo_url().clone(),
132 alias_address: *output.alias_address(),
133 },
134 };
135
136 Ok(native_token_data)
137 }
138}
139
140fn extract_irc30_metadata(output: &FoundryOutput) -> Irc30Metadata {
141 output
142 .immutable_features()
143 .metadata()
144 .and_then(|metadata| serde_json::from_slice::<Irc30Metadata>(metadata.data()).ok())
145 .and_then(|metadata| {
146 metadata
147 .logo_url()
148 .as_ref()
149 .map(|url| url.is_ascii())
150 .unwrap_or(true)
151 .then_some(metadata)
152 })
153 .unwrap_or_else(|| {
154 let name = derive_foundry_package_lowercase_identifier("", output.id().as_slice());
155 Irc30Metadata::new(name.clone(), name, 0)
156 })
157}
158
159fn derive_foundry_package_lowercase_identifier(input: &str, seed: &[u8]) -> String {
160 let input = input.to_ascii_lowercase();
161
162 static VALID_IDENTIFIER_PATTERN: &str = r"[a-z][a-z0-9_]*";
163
164 let valid_parts_re =
166 Regex::new(VALID_IDENTIFIER_PATTERN).expect("should be valid regex pattern");
167 let valid_parts: Vec<&str> = valid_parts_re
168 .find_iter(&input)
169 .map(|mat| mat.as_str())
170 .collect();
171 let concatenated = valid_parts.concat();
172
173 let refined_identifier = concatenated.trim_end_matches('_').to_string();
175 let is_valid = move_core_types::identifier::is_valid(&refined_identifier);
176
177 if is_valid
178 && !keywords::KEYWORDS.contains(&refined_identifier.as_str())
179 && !keywords::CONTEXTUAL_KEYWORDS.contains(&refined_identifier.as_str())
180 && !keywords::PRIMITIVE_TYPES.contains(&refined_identifier.as_str())
181 && !keywords::BUILTINS.contains(&refined_identifier.as_str())
182 {
183 refined_identifier
184 } else {
185 let mut final_identifier = String::from("foundry_");
186 let additional_part = if is_valid {
187 refined_identifier
188 } else {
189 let mut rng: SipRng = Seeder::from(seed).make_rng();
190 fn next_u128(rng: &mut SipRng) -> u128 {
191 ((rng.next_u64() as u128) << 64) | rng.next_u64() as u128
192 }
193 Alphanumeric
195 .sample_string(&mut Pcg64::new(next_u128(&mut rng), next_u128(&mut rng)), 7)
196 .to_lowercase()
197 };
198 final_identifier.push_str(&additional_part);
199 final_identifier
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use std::ops::{Add, Sub};
206
207 use iota_stardust_types::block::{
208 address::AliasAddress,
209 output::{
210 AliasId, Feature, FoundryOutputBuilder, SimpleTokenScheme, TokenScheme,
211 feature::{Irc30Metadata, MetadataFeature},
212 unlock_condition::ImmutableAliasAddressUnlockCondition,
213 },
214 };
215 use primitive_types::U256;
216 use url::Url;
217
218 use super::*;
219 use crate::stardust::{
220 native_token::package_builder, types::token_scheme::MAX_ALLOWED_U64_SUPPLY,
221 };
222
223 #[test]
224 fn foundry_output_with_default_metadata() -> Result<()> {
225 let token_scheme = SimpleTokenScheme::new(
227 U256::from(100_000_000),
228 U256::from(0),
229 U256::from(100_000_000),
230 )
231 .unwrap();
232
233 let irc_30_metadata = Irc30Metadata::new("Dogecoin", "DOGEā¤", 0);
234
235 let alias_id = AliasId::new([0; AliasId::LENGTH]);
236 let builder = FoundryOutputBuilder::new_with_amount(
237 100_000_000_000,
238 1,
239 TokenScheme::Simple(token_scheme),
240 )
241 .add_unlock_condition(ImmutableAliasAddressUnlockCondition::new(
242 AliasAddress::new(alias_id),
243 ))
244 .add_feature(Feature::Metadata(
245 MetadataFeature::new(irc_30_metadata).unwrap(),
246 ));
247 let output = builder.finish().unwrap();
248
249 let native_token_data = NativeTokenPackageData::try_from(&output)?;
251
252 assert!(package_builder::build_and_compile(native_token_data).is_ok());
254
255 Ok(())
256 }
257
258 #[test]
259 fn foundry_output_with_additional_metadata() -> Result<()> {
260 let token_scheme = SimpleTokenScheme::new(
262 U256::from(100_000_000),
263 U256::from(0),
264 U256::from(100_000_000),
265 )
266 .unwrap();
267
268 let irc_30_metadata = Irc30Metadata::new("Dogecoin", "DOGE", 0)
269 .with_description("Much wow")
270 .with_url(Url::parse("https://dogecoin.com").unwrap())
271 .with_logo_url(Url::parse("https://dogecoin.com/logo.png").unwrap())
272 .with_logo("0x54654");
273
274 let alias_id = AliasId::new([0; AliasId::LENGTH]);
275 let builder = FoundryOutputBuilder::new_with_amount(
276 100_000_000_000,
277 1,
278 TokenScheme::Simple(token_scheme),
279 )
280 .add_unlock_condition(ImmutableAliasAddressUnlockCondition::new(
281 AliasAddress::new(alias_id),
282 ))
283 .add_feature(Feature::Metadata(
284 MetadataFeature::new(irc_30_metadata).unwrap(),
285 ));
286 let output = builder.finish().unwrap();
287
288 let native_token_data = NativeTokenPackageData::try_from(&output)?;
290
291 assert!(package_builder::build_and_compile(native_token_data).is_ok());
293
294 Ok(())
295 }
296
297 #[test]
298 fn foundry_output_with_exceeding_max_supply() -> Result<()> {
299 let minted_tokens = U256::from(MAX_ALLOWED_U64_SUPPLY).add(1);
300 let melted_tokens = U256::from(1);
301 let maximum_supply = U256::MAX;
302
303 let token_scheme =
305 SimpleTokenScheme::new(minted_tokens, melted_tokens, maximum_supply).unwrap();
306
307 let irc_30_metadata = Irc30Metadata::new("Dogecoin", "DOGE", 0)
308 .with_description("Much wow")
309 .with_url(Url::parse("https://dogecoin.com").unwrap())
310 .with_logo_url(Url::parse("https://dogecoin.com/logo.png").unwrap())
311 .with_logo("0x54654");
312
313 let alias_id = AliasId::new([0; AliasId::LENGTH]);
314 let builder = FoundryOutputBuilder::new_with_amount(
315 100_000_000_000,
316 1,
317 TokenScheme::Simple(token_scheme),
318 )
319 .add_unlock_condition(ImmutableAliasAddressUnlockCondition::new(
320 AliasAddress::new(alias_id),
321 ))
322 .add_feature(Feature::Metadata(
323 MetadataFeature::new(irc_30_metadata).unwrap(),
324 ));
325 let output = builder.finish().unwrap();
326
327 let native_token_data = NativeTokenPackageData::try_from(&output)?;
329 assert_eq!(
330 native_token_data.module().circulating_supply,
331 minted_tokens.sub(melted_tokens).as_u64()
332 );
333 assert_eq!(
334 native_token_data.module().maximum_supply,
335 MAX_ALLOWED_U64_SUPPLY
336 );
337
338 assert!(package_builder::build_and_compile(native_token_data).is_ok());
340
341 Ok(())
342 }
343
344 #[test]
345 fn foundry_output_with_exceeding_circulating_supply() -> Result<()> {
346 let minted_tokens = U256::from(u64::MAX).add(1);
347 let melted_tokens = U256::from(0);
348 let maximum_supply = U256::MAX;
349
350 let token_scheme =
352 SimpleTokenScheme::new(minted_tokens, melted_tokens, maximum_supply).unwrap();
353
354 let irc_30_metadata = Irc30Metadata::new("Dogecoin", "DOGE", 0)
355 .with_description("Much wow")
356 .with_url(Url::parse("https://dogecoin.com").unwrap())
357 .with_logo_url(Url::parse("https://dogecoin.com/logo.png").unwrap())
358 .with_logo("0x54654");
359
360 let alias_id = AliasId::new([0; AliasId::LENGTH]);
361 let builder = FoundryOutputBuilder::new_with_amount(
362 100_000_000_000,
363 1,
364 TokenScheme::Simple(token_scheme),
365 )
366 .add_unlock_condition(ImmutableAliasAddressUnlockCondition::new(
367 AliasAddress::new(alias_id),
368 ))
369 .add_feature(Feature::Metadata(
370 MetadataFeature::new(irc_30_metadata).unwrap(),
371 ));
372 let output = builder.finish().unwrap();
373
374 let native_token_data = NativeTokenPackageData::try_from(&output)?;
376
377 assert_eq!(
378 native_token_data.module().circulating_supply,
379 MAX_ALLOWED_U64_SUPPLY
380 );
381 assert_eq!(
382 native_token_data.module().maximum_supply,
383 MAX_ALLOWED_U64_SUPPLY
384 );
385
386 assert!(package_builder::build_and_compile(native_token_data).is_ok());
388
389 Ok(())
390 }
391
392 #[test]
393 fn empty_identifier() {
394 let identifier = "".to_string();
395 let result = derive_foundry_package_lowercase_identifier(&identifier, &[]);
396 assert_eq!(15, result.len());
397 }
398
399 #[test]
400 fn identifier_with_only_invalid_chars() {
401 let identifier = "!@#$%^".to_string();
402 let result = derive_foundry_package_lowercase_identifier(&identifier, &[]);
403 assert_eq!(15, result.len());
404 }
405
406 #[test]
407 fn identifier_with_only_one_char() {
408 let identifier = "a".to_string();
409 assert_eq!(
410 derive_foundry_package_lowercase_identifier(&identifier, &[]),
411 "a".to_string()
412 );
413 }
414
415 #[test]
416 fn identifier_with_whitespaces_and_ending_underscore() {
417 let identifier = " a bc-d e_".to_string();
418 assert_eq!(
419 derive_foundry_package_lowercase_identifier(&identifier, &[]),
420 "abcde".to_string()
421 );
422 }
423
424 #[test]
425 fn identifier_with_minus() {
426 let identifier = "hello-world".to_string();
427 assert_eq!(
428 derive_foundry_package_lowercase_identifier(&identifier, &[]),
429 "helloworld".to_string()
430 );
431 }
432
433 #[test]
434 fn identifier_with_multiple_invalid_chars() {
435 let identifier = "#hello-move_world/token&".to_string();
436 assert_eq!(
437 derive_foundry_package_lowercase_identifier(&identifier, &[]),
438 "hellomove_worldtoken"
439 );
440 }
441 #[test]
442 fn valid_identifier() {
443 let identifier = "valid_identifier".to_string();
444 assert_eq!(
445 derive_foundry_package_lowercase_identifier(&identifier, &[]),
446 identifier
447 );
448 }
449}