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