iota_genesis_builder/stardust/migration/
executor.rs

1// Copyright (c) 2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::{BTreeSet, HashMap},
6    sync::Arc,
7};
8
9use anyhow::Result;
10use iota_adapter_latest::{
11    adapter::new_move_vm, execution_mode, gas_charger::GasCharger, programmable_transactions,
12    temporary_store::TemporaryStore,
13};
14use iota_framework::BuiltInFramework;
15use iota_move_build::CompiledPackage;
16use iota_move_natives_latest::all_natives;
17use iota_protocol_config::{Chain, ProtocolConfig, ProtocolVersion};
18use iota_sdk::types::block::output::{
19    AliasOutput, BasicOutput, FoundryOutput, NativeTokens, NftOutput, OutputId, TokenId,
20};
21use iota_types::{
22    IOTA_FRAMEWORK_PACKAGE_ID, STARDUST_PACKAGE_ID, TypeTag,
23    balance::Balance,
24    base_types::{IotaAddress, ObjectID, ObjectRef, SequenceNumber, TxContext},
25    coin_manager::{CoinManager, CoinManagerTreasuryCap},
26    collection_types::Bag,
27    dynamic_field::Field,
28    id::UID,
29    in_memory_storage::InMemoryStorage,
30    inner_temporary_store::InnerTemporaryStore,
31    metrics::LimitsMetrics,
32    move_package::{MovePackage, TypeOrigin, UpgradeCap},
33    object::Object,
34    programmable_transaction_builder::ProgrammableTransactionBuilder,
35    stardust::{
36        coin_type::CoinType,
37        output::{Nft, foundry::create_foundry_amount_coin},
38    },
39    timelock::timelock,
40    transaction::{
41        Argument, CheckedInputObjects, Command, InputObjectKind, InputObjects, ObjectArg,
42        ObjectReadResult, ProgrammableTransaction,
43    },
44};
45use move_core_types::{ident_str, language_storage::StructTag};
46use move_vm_runtime_latest::move_vm::MoveVM;
47
48use crate::{
49    process_package,
50    stardust::{
51        migration::{
52            MigrationTargetNetwork, PACKAGE_DEPS, create_migration_context, package_module_bytes,
53            verification::created_objects::CreatedObjects,
54        },
55        types::{
56            address_swap_map::AddressSwapMap, output_header::OutputHeader,
57            token_scheme::SimpleTokenSchemeU64,
58        },
59    },
60};
61
62/// Creates the objects that map to the stardust UTXO ledger.
63///
64/// Internally uses an unmetered Move VM.
65pub(super) struct Executor {
66    protocol_config: ProtocolConfig,
67    tx_context: TxContext,
68    /// Stores all the migration objects.
69    store: InMemoryStorage,
70    /// Caches the system packages and init objects. Useful for evicting
71    /// them from the store before creating the snapshot.
72    system_packages_and_objects: BTreeSet<ObjectID>,
73    move_vm: Arc<MoveVM>,
74    metrics: Arc<LimitsMetrics>,
75    /// Map the stardust token id [`TokenId`] to the on-chain info of the
76    /// published foundry objects.
77    native_tokens: HashMap<TokenId, FoundryLedgerData>,
78    /// The coin type to use in order to migrate outputs. Can only be equal to
79    /// `Iota` at the moment. Is fixed for the entire migration process.
80    coin_type: CoinType,
81}
82
83impl Executor {
84    /// Setup the execution environment backed by an in-memory store that holds
85    /// all the system packages.
86    pub(super) fn new(
87        protocol_version: ProtocolVersion,
88        target_network: MigrationTargetNetwork,
89        coin_type: CoinType,
90    ) -> Result<Self> {
91        let mut tx_context = create_migration_context(&coin_type, target_network);
92        // Use a throwaway metrics registry for transaction execution.
93        let metrics = Arc::new(LimitsMetrics::new(&prometheus::Registry::new()));
94        let mut store = InMemoryStorage::new(Vec::new());
95        // We don't know the chain ID here since we haven't yet created the genesis
96        // checkpoint. However since we know there are no chain specific
97        // protocol config options in genesis, we use Chain::Unknown here.
98        let protocol_config = ProtocolConfig::get_for_version(protocol_version, Chain::Unknown);
99        // Get the correct system packages for our protocol version. If we cannot find
100        // the snapshot that means that we must be at the latest version and we
101        // should use the latest version of the framework.
102        let system_packages =
103            iota_framework_snapshot::load_bytecode_snapshot(protocol_version.as_u64())
104                .unwrap_or_else(|_| BuiltInFramework::iter_system_packages().cloned().collect());
105
106        let silent = true;
107        let executor = iota_execution::executor(&protocol_config, silent, None)
108            .expect("Creating an executor should not fail here");
109        for system_package in system_packages.into_iter() {
110            process_package(
111                &mut store,
112                executor.as_ref(),
113                &mut tx_context,
114                &system_package.modules(),
115                system_package.dependencies,
116                &protocol_config,
117                metrics.clone(),
118            )?;
119        }
120        let move_vm = Arc::new(new_move_vm(
121            all_natives(silent, &protocol_config),
122            &protocol_config,
123            None,
124        )?);
125
126        let system_packages_and_objects = store.objects().keys().copied().collect();
127        Ok(Self {
128            protocol_config,
129            tx_context,
130            store,
131            system_packages_and_objects,
132            move_vm,
133            metrics,
134            native_tokens: Default::default(),
135            coin_type,
136        })
137    }
138
139    pub fn store(&self) -> &InMemoryStorage {
140        &self.store
141    }
142
143    pub(crate) fn native_tokens(&self) -> &HashMap<TokenId, FoundryLedgerData> {
144        &self.native_tokens
145    }
146
147    /// The migration objects.
148    ///
149    /// The system packages and underlying `init` objects
150    /// are filtered out because they will be generated
151    /// in the genesis process.
152    pub(super) fn into_objects(self) -> Vec<Object> {
153        self.store
154            .into_inner()
155            .into_values()
156            .filter(|object| !self.system_packages_and_objects.contains(&object.id()))
157            .collect()
158    }
159
160    /// Load input objects from the store to be used as checked
161    /// input while executing a transaction
162    pub(crate) fn load_input_objects(
163        &self,
164        object_refs: impl IntoIterator<Item = ObjectRef> + 'static,
165    ) -> impl Iterator<Item = ObjectReadResult> + '_ {
166        object_refs.into_iter().filter_map(|object_ref| {
167            Some(ObjectReadResult::new(
168                InputObjectKind::ImmOrOwnedMoveObject(object_ref),
169                self.store.get_object(&object_ref.0)?.clone().into(),
170            ))
171        })
172    }
173
174    /// Load packages from the store to be used as checked
175    /// input while executing a transaction
176    pub(crate) fn load_packages(
177        &self,
178        object_ids: impl IntoIterator<Item = ObjectID> + 'static,
179    ) -> impl Iterator<Item = ObjectReadResult> + '_ {
180        object_ids.into_iter().filter_map(|object_id| {
181            Some(ObjectReadResult::new(
182                InputObjectKind::MovePackage(object_id),
183                self.store.get_object(&object_id)?.clone().into(),
184            ))
185        })
186    }
187
188    fn checked_system_packages(&self) -> CheckedInputObjects {
189        CheckedInputObjects::new_for_genesis(self.load_packages(PACKAGE_DEPS).collect())
190    }
191
192    pub(crate) fn execute_pt_unmetered(
193        &mut self,
194        input_objects: CheckedInputObjects,
195        pt: ProgrammableTransaction,
196    ) -> Result<InnerTemporaryStore> {
197        let input_objects = input_objects.into_inner();
198        let epoch_id = 0; // Genesis
199        let mut temporary_store = TemporaryStore::new(
200            &self.store,
201            input_objects,
202            vec![],
203            self.tx_context.digest(),
204            &self.protocol_config,
205            epoch_id,
206        );
207        let mut gas_charger = GasCharger::new_unmetered(self.tx_context.digest());
208        programmable_transactions::execution::execute::<execution_mode::Normal>(
209            &self.protocol_config,
210            self.metrics.clone(),
211            &self.move_vm,
212            &mut temporary_store,
213            &mut self.tx_context,
214            &mut gas_charger,
215            pt,
216            &mut None,
217        )?;
218        temporary_store.update_object_version_and_prev_tx();
219        Ok(temporary_store.into_inner())
220    }
221
222    /// Process the foundry outputs as follows:
223    ///
224    /// * Publish the generated packages using a tailored unmetered executor.
225    /// * For each native token, map the [`TokenId`] to the [`ObjectID`] of the
226    ///   coin that holds its total supply.
227    /// * Update the inner store with the created objects.
228    pub(super) fn create_foundries<'a>(
229        &mut self,
230        foundries: impl IntoIterator<Item = (&'a OutputHeader, &'a FoundryOutput, CompiledPackage)>,
231    ) -> Result<Vec<(OutputId, CreatedObjects)>> {
232        let mut res = Vec::new();
233        for (header, foundry, pkg) in foundries {
234            let mut created_objects = CreatedObjects::default();
235            let modules = package_module_bytes(&pkg)?;
236            let deps = self.checked_system_packages();
237            let pt = {
238                let mut builder = ProgrammableTransactionBuilder::new();
239                let upgrade_cap = builder.command(Command::Publish(modules, PACKAGE_DEPS.into()));
240                // We make a dummy transfer because the `UpgradeCap` does
241                // not have the drop ability.
242                //
243                // We ignore it in the genesis, to render the package immutable.
244                builder.transfer_arg(Default::default(), upgrade_cap);
245                builder.finish()
246            };
247            let InnerTemporaryStore { written, .. } = self.execute_pt_unmetered(deps, pt)?;
248            // Get on-chain info
249            let mut native_token_coin_id = None::<ObjectID>;
250            let mut foundry_package = None::<&MovePackage>;
251            for object in written.values() {
252                if object.is_package() {
253                    foundry_package = Some(
254                        object
255                            .data
256                            .try_as_package()
257                            .expect("already verified this is a package"),
258                    );
259                    created_objects.set_package(object.id())?;
260                } else if object.is_coin() {
261                    native_token_coin_id = Some(object.id());
262                    created_objects.set_native_token_coin(object.id())?;
263                } else if let Some(tag) = object.struct_tag() {
264                    if CoinManager::is_coin_manager(&tag) {
265                        created_objects.set_coin_manager(object.id())?;
266                    } else if CoinManagerTreasuryCap::is_coin_manager_treasury_cap(&tag) {
267                        created_objects.set_coin_manager_treasury_cap(object.id())?;
268                    }
269                }
270            }
271            let (native_token_coin_id, foundry_package) = (
272                native_token_coin_id.expect("a native token coin must have been minted"),
273                foundry_package.expect("there should be a published package"),
274            );
275            self.native_tokens.insert(
276                foundry.token_id(),
277                FoundryLedgerData::new(
278                    native_token_coin_id,
279                    foundry_package,
280                    SimpleTokenSchemeU64::try_from(foundry.token_scheme().as_simple())?,
281                ),
282            );
283
284            // Create the amount coin object.
285            let amount_coin = create_foundry_amount_coin(
286                &header.output_id(),
287                foundry,
288                &self.tx_context,
289                foundry_package.version(),
290                &self.protocol_config,
291                &self.coin_type,
292            )?;
293            created_objects.set_coin(amount_coin.id())?;
294            self.store.insert_object(amount_coin);
295
296            self.store.finish(
297                written
298                    .into_iter()
299                    // We ignore the [`UpgradeCap`] objects.
300                    .filter(|(_, object)| object.struct_tag() != Some(UpgradeCap::type_()))
301                    .collect(),
302            );
303            res.push((header.output_id(), created_objects));
304        }
305        Ok(res)
306    }
307
308    pub(super) fn create_alias_objects(
309        &mut self,
310        header: &OutputHeader,
311        alias: &AliasOutput,
312        coin_type: CoinType,
313        address_swap_map: &mut AddressSwapMap,
314    ) -> Result<CreatedObjects> {
315        let mut created_objects = CreatedObjects::default();
316
317        // Take the Alias ID set in the output or, if its zeroized, compute it from the
318        // Output ID.
319        let alias_id = ObjectID::new(*alias.alias_id().or_from_output_id(&header.output_id()));
320        let move_alias = iota_types::stardust::output::Alias::try_from_stardust(alias_id, alias)?;
321
322        // TODO: We should ensure that no circular ownership exists.
323        let alias_output_owner =
324            address_swap_map.swap_stardust_to_iota_address_owner(alias.governor_address())?;
325
326        let package_deps = InputObjects::new(self.load_packages(PACKAGE_DEPS).collect());
327        let version = package_deps.lamport_timestamp(&[]);
328
329        let move_alias_object = move_alias.to_genesis_object(
330            alias_output_owner,
331            &self.protocol_config,
332            &self.tx_context,
333            version,
334        )?;
335        let move_alias_object_ref = move_alias_object.compute_object_reference();
336
337        self.store.insert_object(move_alias_object);
338
339        let (bag, version, fields) = self.create_bag_with_pt(alias.native_tokens())?;
340        created_objects.set_native_tokens(fields)?;
341
342        let move_alias_output = iota_types::stardust::output::AliasOutput::try_from_stardust(
343            self.tx_context.fresh_id(),
344            alias,
345            bag,
346        )?;
347
348        // The bag will be wrapped into the alias output object, so
349        // by equating their versions we emulate a ptb.
350        let move_alias_output_object = move_alias_output.to_genesis_object(
351            alias_output_owner,
352            &self.protocol_config,
353            &self.tx_context,
354            version,
355            coin_type,
356        )?;
357        let move_alias_output_object_ref = move_alias_output_object.compute_object_reference();
358
359        created_objects.set_output(move_alias_output_object.id())?;
360        self.store.insert_object(move_alias_output_object);
361
362        // Attach the Alias to the Alias Output as a dynamic object field via the
363        // attach_alias convenience method.
364        let pt = {
365            let mut builder = ProgrammableTransactionBuilder::new();
366
367            let alias_output_arg =
368                builder.obj(ObjectArg::ImmOrOwnedObject(move_alias_output_object_ref))?;
369            let alias_arg = builder.obj(ObjectArg::ImmOrOwnedObject(move_alias_object_ref))?;
370
371            builder.programmable_move_call(
372                STARDUST_PACKAGE_ID,
373                ident_str!("alias_output").into(),
374                ident_str!("attach_alias").into(),
375                vec![coin_type.to_type_tag()],
376                vec![alias_output_arg, alias_arg],
377            );
378
379            builder.finish()
380        };
381
382        let input_objects = CheckedInputObjects::new_for_genesis(
383            self.load_input_objects([move_alias_object_ref, move_alias_output_object_ref])
384                .chain(self.load_packages(PACKAGE_DEPS))
385                .collect(),
386        );
387
388        let InnerTemporaryStore { written, .. } = self.execute_pt_unmetered(input_objects, pt)?;
389        self.store.finish(written);
390
391        Ok(created_objects)
392    }
393
394    /// Create a [`Bag`] of balances of native tokens executing a programmable
395    /// transaction block.
396    pub(crate) fn create_bag_with_pt(
397        &mut self,
398        native_tokens: &NativeTokens,
399    ) -> Result<(Bag, SequenceNumber, Vec<ObjectID>)> {
400        let mut object_deps = Vec::with_capacity(native_tokens.len());
401        let mut foundry_package_deps = Vec::with_capacity(native_tokens.len());
402        let pt = {
403            let mut builder = ProgrammableTransactionBuilder::new();
404            let bag = pt::bag_new(&mut builder);
405            for token in native_tokens.iter() {
406                let Some(foundry_ledger_data) = self.native_tokens.get_mut(token.token_id()) else {
407                    anyhow::bail!("foundry for native token has not been published");
408                };
409
410                let Some(foundry_coin) = self
411                    .store
412                    .get_object(&foundry_ledger_data.native_token_coin_id)
413                else {
414                    anyhow::bail!("foundry coin should exist");
415                };
416                let object_ref = foundry_coin.compute_object_reference();
417
418                object_deps.push(object_ref);
419                foundry_package_deps.push(foundry_ledger_data.package_id);
420
421                let token_type =
422                    foundry_ledger_data.to_canonical_string(/* with_prefix */ true);
423                let bag_key = foundry_ledger_data.to_canonical_string(/* with_prefix */ false);
424
425                let adjusted_amount = foundry_ledger_data
426                    .token_scheme_u64
427                    .adjust_tokens(token.amount());
428
429                foundry_ledger_data.minted_value = foundry_ledger_data
430                    .minted_value
431                    .checked_sub(adjusted_amount)
432                    .ok_or_else(|| anyhow::anyhow!("underflow splitting native token balance"))?;
433
434                let balance = pt::coin_balance_split(
435                    &mut builder,
436                    object_ref,
437                    token_type.parse()?,
438                    adjusted_amount,
439                )?;
440                pt::bag_add(&mut builder, bag, bag_key, balance, token_type)?;
441            }
442
443            // The `Bag` object does not have the `drop` ability so we have to use it
444            // in the transaction block. Therefore we transfer it to the `0x0` address.
445            //
446            // Nevertheless, we only store the contents of the object, and thus the
447            // ownership metadata are irrelevant to us. This is a dummy transfer
448            // then to satisfy the VM.
449            builder.transfer_arg(Default::default(), bag);
450            builder.finish()
451        };
452        let checked_input_objects = CheckedInputObjects::new_for_genesis(
453            self.load_packages(PACKAGE_DEPS)
454                .chain(self.load_packages(foundry_package_deps))
455                .chain(self.load_input_objects(object_deps))
456                .collect(),
457        );
458        let InnerTemporaryStore {
459            mut written,
460            input_objects,
461            ..
462        } = self.execute_pt_unmetered(checked_input_objects, pt)?;
463        let bag_object = written
464            .iter()
465            // We filter out the dynamic-field objects that are owned by the bag
466            // and we should be left with only the bag
467            .find_map(|(id, object)| {
468                (!input_objects.contains_key(id) && !object.is_child_object()).then_some(id)
469            })
470            .copied()
471            .and_then(|id| written.remove(&id))
472            .ok_or_else(|| anyhow::anyhow!("the bag should have been created"))?;
473        written.remove(&bag_object.id());
474        let field_ids = written
475            .iter()
476            .filter_map(|(id, object)| object.to_rust::<Field<String, Balance>>().map(|_| *id))
477            .collect();
478        // Save the modified coins
479        self.store.finish(written);
480        // Return bag
481        let bag = bcs::from_bytes(
482            bag_object
483                .data
484                .try_as_move()
485                .expect("this should be a move object")
486                .contents(),
487        )
488        .expect("this should be a valid Bag Move object");
489        Ok((bag, bag_object.version(), field_ids))
490    }
491
492    /// Create [`Coin`] objects representing native tokens in the ledger.
493    fn create_native_token_coins(
494        &mut self,
495        native_tokens: &NativeTokens,
496        owner: IotaAddress,
497    ) -> Result<Vec<ObjectID>> {
498        let mut object_deps = Vec::with_capacity(native_tokens.len());
499        let mut foundry_package_deps = Vec::with_capacity(native_tokens.len());
500        let mut foundry_coins = Vec::with_capacity(native_tokens.len());
501        let pt = {
502            let mut builder = ProgrammableTransactionBuilder::new();
503            for token in native_tokens.iter() {
504                let Some(foundry_ledger_data) = self.native_tokens.get_mut(token.token_id()) else {
505                    anyhow::bail!("foundry for native token has not been published");
506                };
507
508                let Some(foundry_coin) = self
509                    .store
510                    .get_object(&foundry_ledger_data.native_token_coin_id)
511                else {
512                    anyhow::bail!("foundry coin should exist");
513                };
514                let object_ref = foundry_coin.compute_object_reference();
515                foundry_coins.push(foundry_coin.id());
516
517                object_deps.push(object_ref);
518                foundry_package_deps.push(foundry_ledger_data.package_id);
519
520                // Pay using that object
521                let adjusted_amount = foundry_ledger_data
522                    .token_scheme_u64
523                    .adjust_tokens(token.amount());
524
525                foundry_ledger_data.minted_value = foundry_ledger_data
526                    .minted_value
527                    .checked_sub(adjusted_amount)
528                    .ok_or_else(|| anyhow::anyhow!("underflow splitting native token balance"))?;
529
530                builder.pay(vec![object_ref], vec![owner], vec![adjusted_amount])?;
531            }
532
533            builder.finish()
534        };
535        let checked_input_objects = CheckedInputObjects::new_for_genesis(
536            self.load_packages(PACKAGE_DEPS)
537                .chain(self.load_packages(foundry_package_deps))
538                .chain(self.load_input_objects(object_deps))
539                .collect(),
540        );
541        // Execute
542        let InnerTemporaryStore { written, .. } =
543            self.execute_pt_unmetered(checked_input_objects, pt)?;
544
545        let coin_ids = written
546            .keys()
547            // Linear search is ok due to the expected very small size of
548            // `foundry_coins`
549            .filter(|id| !foundry_coins.contains(id))
550            .copied()
551            .collect();
552
553        // Save the modified coin
554        self.store.finish(written);
555        Ok(coin_ids)
556    }
557
558    /// This implements the control flow in
559    /// crates/iota-framework/packages/stardust/basic_migration_graph.svg
560    pub(super) fn create_basic_objects(
561        &mut self,
562        header: &OutputHeader,
563        basic_output: &BasicOutput,
564        target_milestone_timestamp_sec: u32,
565        coin_type: &CoinType,
566        address_swap_map: &mut AddressSwapMap,
567    ) -> Result<CreatedObjects> {
568        let mut basic =
569            iota_types::stardust::output::BasicOutput::new(header.new_object_id(), basic_output)?;
570
571        let basic_objects_owner =
572            address_swap_map.swap_stardust_to_iota_address(basic_output.address())?;
573
574        let mut created_objects = CreatedObjects::default();
575
576        // The minimum version of the manually created objects
577        let package_deps = InputObjects::new(self.load_packages(PACKAGE_DEPS).collect());
578        let mut version = package_deps.lamport_timestamp(&[]);
579
580        let object = if basic.is_simple_coin(target_milestone_timestamp_sec) {
581            if !basic_output.native_tokens().is_empty() {
582                let coins = self
583                    .create_native_token_coins(basic_output.native_tokens(), basic_objects_owner)?;
584                created_objects.set_native_tokens(coins)?;
585            }
586            let amount_coin = basic.into_genesis_coin_object(
587                basic_objects_owner,
588                &self.protocol_config,
589                &self.tx_context,
590                version,
591                coin_type,
592            )?;
593            created_objects.set_coin(amount_coin.id())?;
594            amount_coin
595        } else {
596            if !basic_output.native_tokens().is_empty() {
597                let fields;
598                // The bag will be wrapped into the basic output object, so
599                // by equating their versions we emulate a ptb.
600                (basic.native_tokens, version, fields) =
601                    self.create_bag_with_pt(basic_output.native_tokens())?;
602                created_objects.set_native_tokens(fields)?;
603            } else {
604                // Overwrite the default 0 UID of `Bag::default()`, since we won't
605                // be creating a new bag in this code path.
606                basic.native_tokens.id = UID::new(self.tx_context.fresh_id());
607            }
608            let object = basic.to_genesis_object(
609                basic_objects_owner,
610                &self.protocol_config,
611                &self.tx_context,
612                version,
613                coin_type,
614            )?;
615            created_objects.set_output(object.id())?;
616            object
617        };
618
619        self.store.insert_object(object);
620        Ok(created_objects)
621    }
622
623    /// Creates [`TimeLock<Balance<IOTA>>`] objects which represent vested
624    /// rewards that were created during the stardust upgrade on IOTA
625    /// mainnet.
626    pub(super) fn create_timelock_object(
627        &mut self,
628        output_id: OutputId,
629        basic_output: &BasicOutput,
630        target_milestone_timestamp: u32,
631        address_swap_map: &mut AddressSwapMap,
632    ) -> Result<CreatedObjects> {
633        let mut created_objects = CreatedObjects::default();
634
635        let basic_output_owner =
636            address_swap_map.swap_stardust_to_iota_address(basic_output.address())?;
637
638        let package_deps = InputObjects::new(self.load_packages(PACKAGE_DEPS).collect());
639        let version = package_deps.lamport_timestamp(&[]);
640
641        let timelock =
642            timelock::try_from_stardust(output_id, basic_output, target_milestone_timestamp)?;
643
644        let object = timelock::to_genesis_object(
645            timelock,
646            basic_output_owner,
647            &self.protocol_config,
648            &self.tx_context,
649            version,
650        )?;
651
652        created_objects.set_output(object.id())?;
653
654        self.store.insert_object(object);
655        Ok(created_objects)
656    }
657
658    pub(super) fn create_nft_objects(
659        &mut self,
660        header: &OutputHeader,
661        nft: &NftOutput,
662        coin_type: CoinType,
663        address_swap_map: &mut AddressSwapMap,
664    ) -> Result<CreatedObjects> {
665        let mut created_objects = CreatedObjects::default();
666
667        // Take the Nft ID set in the output or, if its zeroized, compute it from the
668        // Output ID.
669        let nft_id = ObjectID::new(*nft.nft_id().or_from_output_id(&header.output_id()));
670        let move_nft = Nft::try_from_stardust(nft_id, nft)?;
671
672        // TODO: We should ensure that no circular ownership exists.
673        let nft_output_owner_address =
674            address_swap_map.swap_stardust_to_iota_address(nft.address())?;
675
676        let nft_output_owner =
677            address_swap_map.swap_stardust_to_iota_address_owner(nft.address())?;
678
679        let package_deps = InputObjects::new(self.load_packages(PACKAGE_DEPS).collect());
680        let version = package_deps.lamport_timestamp(&[]);
681        let move_nft_object = move_nft.to_genesis_object(
682            nft_output_owner,
683            &self.protocol_config,
684            &self.tx_context,
685            version,
686        )?;
687
688        let move_nft_object_ref = move_nft_object.compute_object_reference();
689        self.store.insert_object(move_nft_object);
690
691        let (bag, version, fields) = self.create_bag_with_pt(nft.native_tokens())?;
692        created_objects.set_native_tokens(fields)?;
693        let move_nft_output = iota_types::stardust::output::NftOutput::try_from_stardust(
694            self.tx_context.fresh_id(),
695            nft,
696            bag,
697        )?;
698
699        // The bag will be wrapped into the nft output object, so
700        // by equating their versions we emulate a ptb.
701        let move_nft_output_object = move_nft_output.to_genesis_object(
702            nft_output_owner_address,
703            &self.protocol_config,
704            &self.tx_context,
705            version,
706            coin_type,
707        )?;
708        let move_nft_output_object_ref = move_nft_output_object.compute_object_reference();
709        created_objects.set_output(move_nft_output_object.id())?;
710        self.store.insert_object(move_nft_output_object);
711
712        // Attach the Nft to the Nft Output as a dynamic object field via the attach_nft
713        // convenience method.
714        let pt = {
715            let mut builder = ProgrammableTransactionBuilder::new();
716
717            let nft_output_arg =
718                builder.obj(ObjectArg::ImmOrOwnedObject(move_nft_output_object_ref))?;
719            let nft_arg = builder.obj(ObjectArg::ImmOrOwnedObject(move_nft_object_ref))?;
720            builder.programmable_move_call(
721                STARDUST_PACKAGE_ID,
722                ident_str!("nft_output").into(),
723                ident_str!("attach_nft").into(),
724                vec![coin_type.to_type_tag()],
725                vec![nft_output_arg, nft_arg],
726            );
727
728            builder.finish()
729        };
730
731        let input_objects = CheckedInputObjects::new_for_genesis(
732            self.load_input_objects([move_nft_object_ref, move_nft_output_object_ref])
733                .chain(self.load_packages(PACKAGE_DEPS))
734                .collect(),
735        );
736
737        let InnerTemporaryStore { written, .. } = self.execute_pt_unmetered(input_objects, pt)?;
738        self.store.finish(written);
739
740        Ok(created_objects)
741    }
742}
743
744#[cfg(test)]
745impl Executor {
746    /// Set the [`TxContext`] of the [`Executor`].
747    pub(crate) fn with_tx_context(mut self, tx_context: TxContext) -> Self {
748        self.tx_context = tx_context;
749        self
750    }
751
752    /// Set the [`InMemoryStorage`] of the [`Executor`].
753    pub(crate) fn with_store(mut self, store: InMemoryStorage) -> Self {
754        self.store = store;
755        self
756    }
757}
758
759mod pt {
760    use super::*;
761    use crate::stardust::migration::NATIVE_TOKEN_BAG_KEY_TYPE;
762
763    pub fn coin_balance_split(
764        builder: &mut ProgrammableTransactionBuilder,
765        foundry_coin_ref: ObjectRef,
766        token_type_tag: TypeTag,
767        amount: u64,
768    ) -> Result<Argument> {
769        let foundry_coin_ref = builder.obj(ObjectArg::ImmOrOwnedObject(foundry_coin_ref))?;
770        let amount = builder.pure(amount)?;
771        let coin = builder.programmable_move_call(
772            IOTA_FRAMEWORK_PACKAGE_ID,
773            ident_str!("coin").into(),
774            ident_str!("split").into(),
775            vec![token_type_tag.clone()],
776            vec![foundry_coin_ref, amount],
777        );
778        Ok(builder.programmable_move_call(
779            IOTA_FRAMEWORK_PACKAGE_ID,
780            ident_str!("coin").into(),
781            ident_str!("into_balance").into(),
782            vec![token_type_tag],
783            vec![coin],
784        ))
785    }
786
787    pub fn bag_add(
788        builder: &mut ProgrammableTransactionBuilder,
789        bag: Argument,
790        bag_key: String,
791        balance: Argument,
792        token_type: String,
793    ) -> Result<()> {
794        let key_type: StructTag = NATIVE_TOKEN_BAG_KEY_TYPE.parse()?;
795        let value_type = Balance::type_(token_type.parse::<TypeTag>()?);
796        let bag_key_arg = builder.pure(bag_key)?;
797        builder.programmable_move_call(
798            IOTA_FRAMEWORK_PACKAGE_ID,
799            ident_str!("bag").into(),
800            ident_str!("add").into(),
801            vec![key_type.into(), value_type.into()],
802            vec![bag, bag_key_arg, balance],
803        );
804        Ok(())
805    }
806
807    pub fn bag_new(builder: &mut ProgrammableTransactionBuilder) -> Argument {
808        builder.programmable_move_call(
809            IOTA_FRAMEWORK_PACKAGE_ID,
810            ident_str!("bag").into(),
811            ident_str!("new").into(),
812            vec![],
813            vec![],
814        )
815    }
816}
817
818/// On-chain data about the objects created while
819/// publishing foundry packages
820pub(crate) struct FoundryLedgerData {
821    pub(crate) native_token_coin_id: ObjectID,
822    pub(crate) coin_type_origin: TypeOrigin,
823    pub(crate) package_id: ObjectID,
824    pub(crate) token_scheme_u64: SimpleTokenSchemeU64,
825    pub(crate) minted_value: u64,
826}
827
828impl FoundryLedgerData {
829    /// Store the minted coin `ObjectID` and derive data from the foundry
830    /// package.
831    ///
832    /// # Panic
833    ///
834    /// Panics if the package does not contain any [`TypeOrigin`].
835    fn new(
836        native_token_coin_id: ObjectID,
837        foundry_package: &MovePackage,
838        token_scheme_u64: SimpleTokenSchemeU64,
839    ) -> Self {
840        Self {
841            native_token_coin_id,
842            // There must be only one type created in the foundry package.
843            coin_type_origin: foundry_package.type_origin_table()[0].clone(),
844            package_id: foundry_package.id(),
845            minted_value: token_scheme_u64.circulating_supply(),
846            token_scheme_u64,
847        }
848    }
849
850    pub(crate) fn to_canonical_string(&self, with_prefix: bool) -> String {
851        format!(
852            "{}::{}::{}",
853            self.coin_type_origin
854                .package
855                .to_canonical_string(with_prefix),
856            self.coin_type_origin.module_name,
857            self.coin_type_origin.datatype_name
858        )
859    }
860}