iota_genesis_builder/stardust/migration/verification/
nft.rs

1// Copyright (c) 2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5
6use anyhow::{anyhow, ensure};
7use iota_sdk::types::block::output::{NftOutput, OutputId, TokenId};
8use iota_types::{
9    TypeTag,
10    balance::Balance,
11    base_types::ObjectID,
12    dynamic_field::{DynamicFieldInfo, Field, derive_dynamic_field_id},
13    in_memory_storage::InMemoryStorage,
14    object::Owner,
15    stardust::output::{NFT_DYNAMIC_OBJECT_FIELD_KEY, NFT_DYNAMIC_OBJECT_FIELD_KEY_TYPE},
16};
17
18use crate::stardust::{
19    migration::{
20        executor::FoundryLedgerData,
21        verification::{
22            created_objects::CreatedObjects,
23            util::{
24                TokensAmountCounter, verify_address_owner, verify_expiration_unlock_condition,
25                verify_issuer_feature, verify_metadata_feature, verify_native_tokens,
26                verify_parent, verify_sender_feature, verify_storage_deposit_unlock_condition,
27                verify_tag_feature, verify_timelock_unlock_condition,
28            },
29        },
30    },
31    types::address_swap_map::AddressSwapMap,
32};
33
34pub(super) fn verify_nft_output(
35    output_id: OutputId,
36    output: &NftOutput,
37    created_objects: &CreatedObjects,
38    foundry_data: &HashMap<TokenId, FoundryLedgerData>,
39    storage: &InMemoryStorage,
40    tokens_counter: &mut TokensAmountCounter,
41    address_swap_map: &AddressSwapMap,
42) -> anyhow::Result<()> {
43    let created_output_obj = created_objects.output().and_then(|id| {
44        storage
45            .get_object(id)
46            .ok_or_else(|| anyhow!("missing nft output object for {output_id}"))
47    })?;
48    let created_output = created_output_obj
49        .to_rust::<iota_types::stardust::output::NftOutput>()
50        .ok_or_else(|| anyhow!("invalid nft output object for {output_id}"))?;
51
52    let created_nft_obj = storage
53        .get_object(&ObjectID::new(*output.nft_id_non_null(&output_id)))
54        .ok_or_else(|| anyhow!("missing nft object for {output_id}"))?;
55    let created_nft = created_nft_obj
56        .to_rust::<iota_types::stardust::output::Nft>()
57        .ok_or_else(|| anyhow!("invalid nft object for {output_id}"))?;
58
59    // Output Owner
60    // If there is an expiration unlock condition, the NFT is shared.
61    if output.unlock_conditions().expiration().is_some() {
62        ensure!(
63            matches!(created_output_obj.owner, Owner::Shared { .. }),
64            "nft output owner mismatch: found {:?}, expected Shared",
65            created_output_obj.owner,
66        );
67    } else {
68        verify_address_owner(
69            output.address(),
70            created_output_obj,
71            "nft output",
72            address_swap_map,
73        )?;
74    }
75
76    // NFT Owner
77    let expected_nft_owner = Owner::ObjectOwner(
78        derive_dynamic_field_id(
79            created_output_obj.id(),
80            &DynamicFieldInfo::dynamic_object_field_wrapper(
81                NFT_DYNAMIC_OBJECT_FIELD_KEY_TYPE.parse::<TypeTag>()?,
82            )
83            .into(),
84            &bcs::to_bytes(NFT_DYNAMIC_OBJECT_FIELD_KEY)?,
85        )?
86        .into(),
87    );
88
89    ensure!(
90        created_nft_obj.owner == expected_nft_owner,
91        "nft owner mismatch: found {}, expected {}",
92        created_nft_obj.owner,
93        expected_nft_owner
94    );
95
96    // Amount
97    ensure!(
98        created_output.balance.value() == output.amount(),
99        "amount mismatch: found {}, expected {}",
100        created_output.balance.value(),
101        output.amount()
102    );
103    tokens_counter.update_total_value_for_iota(created_output.balance.value());
104
105    // Native Tokens
106    verify_native_tokens::<Field<String, Balance>>(
107        output.native_tokens(),
108        foundry_data,
109        created_output.native_tokens,
110        created_objects.native_tokens().ok(),
111        storage,
112        tokens_counter,
113    )?;
114
115    // Storage Deposit Return Unlock Condition
116    verify_storage_deposit_unlock_condition(
117        output.unlock_conditions().storage_deposit_return(),
118        created_output.storage_deposit_return.as_ref(),
119    )?;
120
121    // Timelock Unlock Condition
122    verify_timelock_unlock_condition(
123        output.unlock_conditions().timelock(),
124        created_output.timelock.as_ref(),
125    )?;
126
127    // Expiration Unlock Condition
128    verify_expiration_unlock_condition(
129        output.unlock_conditions().expiration(),
130        created_output.expiration.as_ref(),
131        output.address(),
132    )?;
133
134    // Metadata Feature
135    verify_metadata_feature(output.features().metadata(), created_nft.metadata.as_ref())?;
136
137    // Tag Feature
138    verify_tag_feature(output.features().tag(), created_nft.tag.as_ref())?;
139
140    // Sender Feature
141    verify_sender_feature(output.features().sender(), created_nft.legacy_sender)?;
142
143    // Issuer Feature
144    verify_issuer_feature(
145        output.immutable_features().issuer(),
146        created_nft.immutable_issuer,
147    )?;
148
149    // Immutable Metadata Feature
150    ensure!(
151        iota_types::stardust::output::Nft::convert_immutable_metadata(output)?
152            == created_nft.immutable_metadata,
153        "metadata mismatch: found {:x?}, expected {:x?}",
154        iota_types::stardust::output::Nft::convert_immutable_metadata(output)?,
155        created_nft.immutable_metadata
156    );
157
158    verify_parent(&output_id, output.address(), storage)?;
159
160    ensure!(created_objects.coin().is_err(), "unexpected coin found");
161
162    ensure!(
163        created_objects.native_token_coin().is_err(),
164        "unexpected native token coin found"
165    );
166
167    ensure!(
168        created_objects.coin_manager().is_err(),
169        "unexpected coin manager found"
170    );
171
172    ensure!(
173        created_objects.coin_manager_treasury_cap().is_err(),
174        "unexpected coin manager treasury cap found"
175    );
176
177    ensure!(
178        created_objects.package().is_err(),
179        "unexpected package found"
180    );
181
182    Ok(())
183}