iota_genesis_builder/stardust/native_token/
package_builder.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5//! The `package_builder` module provides functions for building and
6//! compiling Stardust native token packages.
7use std::{fs, path::Path};
8
9use anyhow::Result;
10use iota_move_build::{BuildConfig, CompiledPackage, IotaPackageHooks};
11use tempfile::tempdir;
12
13use crate::stardust::native_token::package_data::NativeTokenPackageData;
14
15const IOTA_FRAMEWORK_GENESIS_REVISION: &str = "framework/genesis/mainnet";
16
17const MODULE_CONTENT: &str = r#"// Copyright (c) 2024 IOTA Stiftung
18// SPDX-License-Identifier: Apache-2.0
19
20#[allow(lint(share_owned))]
21module 0x0::$MODULE_NAME {
22    use iota::coin;
23    use iota::coin_manager;
24    use iota::url::Url;
25
26    /// The type identifier of coin. The coin will have a type
27    /// tag of kind: `Coin<package_object::$MODULE_NAME::$OTW`
28    /// Make sure that the name of the type matches the module's name.
29    public struct $OTW has drop {}
30
31    /// Module initializer is called once on module publish. A treasury
32    /// cap is sent to the publisher, who then controls minting and burning
33    fun init(witness: $OTW, ctx: &mut TxContext) {
34        let icon_url = $ICON_URL;
35
36        // Create the currency
37        let (mut treasury_cap, metadata) = coin::create_currency<$OTW>(
38            witness,
39            $COIN_DECIMALS,
40            b"$COIN_SYMBOL",
41            $COIN_NAME,
42            $COIN_DESCRIPTION,
43            icon_url,
44            ctx
45        );
46
47        // Mint the tokens and transfer them to the publisher
48        let minted_coins = coin::mint(&mut treasury_cap, $CIRCULATING_SUPPLY, ctx);
49        transfer::public_transfer(minted_coins, ctx.sender());
50
51        // Create a coin manager
52        let (cm_treasury_cap, cm_metadata_cap, mut coin_manager) = coin_manager::new(treasury_cap, metadata, ctx);
53        cm_treasury_cap.enforce_maximum_supply(&mut coin_manager, $MAXIMUM_SUPPLY);
54
55        // Make the metadata immutable
56        cm_metadata_cap.renounce_metadata_ownership(&mut coin_manager);
57
58        // Publicly sharing the `CoinManager` object for convenient usage by anyone interested
59        transfer::public_share_object(coin_manager);
60
61        // Transfer the coin manager treasury capability to the alias address
62        transfer::public_transfer(cm_treasury_cap, iota::address::from_ascii_bytes(&b"$ALIAS"));
63    }
64}
65"#;
66
67const TOML_CONTENT: &str = r#"[package]
68name = "$PACKAGE_NAME"
69version = "0.0.1"
70edition = "2024.beta"
71
72[dependencies]
73Iota = { git = "https://github.com/iotaledger/iota.git", subdir = "crates/iota-framework/packages/iota-framework", rev = "$GENESIS_REVISION" }
74"#;
75
76/// Builds and compiles a Stardust native token package.
77pub fn build_and_compile(package: NativeTokenPackageData) -> Result<CompiledPackage> {
78    // Set up a temporary directory to build the native token package
79    let tmp_dir = tempdir()?;
80    let package_path = tmp_dir.path().join("native_token_package");
81    fs::create_dir_all(&package_path).expect("Failed to create native_token_package directory");
82
83    // Write and replace template variables in the Move.toml file
84    write_move_toml(&package_path, &package, IOTA_FRAMEWORK_GENESIS_REVISION)?;
85
86    // Write and replace template variables in the .move file
87    write_native_token_module(&package_path, &package)?;
88
89    // Compile the package
90    move_package::package_hooks::register_package_hooks(Box::new(IotaPackageHooks));
91    let compiled_package = BuildConfig::default().build(&package_path)?;
92
93    // Step 5: Clean up the temporary directory
94    tmp_dir.close()?;
95
96    Ok(compiled_package)
97}
98
99// Write the Move.toml file with the package name and alias address.
100fn write_move_toml(
101    package_path: &Path,
102    package: &NativeTokenPackageData,
103    iota_framework_genesis_revision: &str,
104) -> Result<()> {
105    let cargo_toml_path = package_path.join("Move.toml");
106    let new_contents = TOML_CONTENT
107        .replace("$PACKAGE_NAME", package.package_name())
108        .replace("$GENESIS_REVISION", iota_framework_genesis_revision);
109    fs::write(&cargo_toml_path, new_contents)?;
110
111    Ok(())
112}
113
114// Replaces template variables in the .move file with the actual values.
115fn write_native_token_module(package_path: &Path, package: &NativeTokenPackageData) -> Result<()> {
116    let move_source_path = package_path.join("sources");
117    fs::create_dir_all(&move_source_path).expect("Failed to create sources directory");
118    let new_move_file_name = format!("{}.move", package.module().module_name);
119    let new_move_file_path = move_source_path.join(new_move_file_name);
120
121    let icon_url = match &package.module().icon_url {
122        Some(url) => format!(
123            "option::some<Url>(iota::url::new_unsafe_from_bytes({}))",
124            format_string_as_move_vector(url.as_str())
125        ),
126        None => "option::none<Url>()".to_string(),
127    };
128
129    let new_contents = MODULE_CONTENT
130        .replace("$MODULE_NAME", &package.module().module_name)
131        .replace("$OTW", &package.module().otw_name)
132        .replace("$COIN_DECIMALS", &package.module().decimals.to_string())
133        .replace("$COIN_SYMBOL", &package.module().symbol)
134        .replace(
135            "$CIRCULATING_SUPPLY",
136            &package.module().circulating_supply.to_string(),
137        )
138        .replace(
139            "$MAXIMUM_SUPPLY",
140            &package.module().maximum_supply.to_string(),
141        )
142        .replace(
143            "$COIN_NAME",
144            format_string_as_move_vector(package.module().coin_name.as_str()).as_str(),
145        )
146        .replace(
147            "$COIN_DESCRIPTION",
148            format_string_as_move_vector(package.module().coin_description.as_str()).as_str(),
149        )
150        .replace("$ICON_URL", &icon_url)
151        .replace(
152            "$ALIAS",
153            // Remove the "0x" prefix
154            &package.module().alias_address.to_string().replace("0x", ""),
155        );
156
157    fs::write(&new_move_file_path, new_contents)?;
158
159    Ok(())
160}
161
162/// Converts a string x to a string y representing the bytes of x as hexadecimal
163/// values, which can be used as a piece of Move code.
164///
165/// Example: It converts "abc" to "vector<u8>[0x61, 0x62, 0x63]" plus the
166/// original human-readable string in a comment.
167fn format_string_as_move_vector(string: &str) -> String {
168    let mut byte_string = String::new();
169    byte_string.push_str("/* The utf-8 bytes of '");
170    byte_string.push_str(string);
171    byte_string.push_str("' */\n");
172
173    byte_string.push_str("            vector<u8>[");
174
175    for (idx, byte) in string.as_bytes().iter().enumerate() {
176        byte_string.push_str(&format!("{byte:#x}"));
177
178        if idx != string.len() - 1 {
179            byte_string.push_str(", ");
180        }
181    }
182
183    byte_string.push(']');
184
185    byte_string
186}
187
188#[cfg(test)]
189mod tests {
190
191    use super::*;
192
193    #[test]
194    fn string_to_move_vector() {
195        let tests = [
196            ("", "vector<u8>[]"),
197            ("a", "vector<u8>[0x61]"),
198            ("ab", "vector<u8>[0x61, 0x62]"),
199            ("abc", "vector<u8>[0x61, 0x62, 0x63]"),
200            (
201                "\nöäü",
202                "vector<u8>[0xa, 0xc3, 0xb6, 0xc3, 0xa4, 0xc3, 0xbc]",
203            ),
204        ];
205
206        for (test_input, expected_result) in tests {
207            let move_string = format_string_as_move_vector(test_input);
208            // Ignore the comment and whitespace.
209            let actual_result = move_string.split('\n').next_back().unwrap().trim_start();
210            assert_eq!(expected_result, actual_result);
211        }
212    }
213}