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