diff --git a/Cargo.lock b/Cargo.lock index a9930376e6..acea173ccd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1785,6 +1785,12 @@ dependencies = [ "url", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "convert_case" version = "0.7.1" @@ -2107,8 +2113,10 @@ version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ + "convert_case 0.4.0", "proc-macro2", "quote", + "rustc_version 0.4.1", "syn 2.0.110", ] @@ -2147,7 +2155,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ - "convert_case", + "convert_case 0.7.1", "proc-macro2", "quote", "syn 2.0.110", @@ -2398,6 +2406,26 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "env_home" version = "0.1.0" @@ -2444,7 +2472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3649,7 +3677,7 @@ dependencies = [ "scale-info", "sp-io 42.0.0", "sp-runtime-interface 31.0.0", - "staging-xcm", + "staging-xcm 18.0.0", "trybuild", ] @@ -3776,7 +3804,7 @@ dependencies = [ "secp256k1 0.31.1", "sha2 0.10.9", "sha3", - "staging-xcm", + "staging-xcm 18.0.0", "static_assertions", ] @@ -3948,9 +3976,12 @@ dependencies = [ "pallet-assets", "pallet-assets-precompiles", "pallet-balances", + "pallet-nfts", "pallet-revive", "pallet-timestamp", "pallet-transaction-payment", + "pallet-xcm", + "pallet-xcm-precompiles", "parity-scale-codec", "paste", "scale-info", @@ -3959,6 +3990,9 @@ dependencies = [ "sp-externalities 0.25.0", "sp-io 30.0.0", "sp-runtime 43.0.0", + "staging-xcm 7.0.1", + "staging-xcm-builder", + "staging-xcm-executor", "subxt-metadata 0.44.0", "thiserror 2.0.17", ] @@ -4592,7 +4626,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4781,7 +4815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.45.0", ] [[package]] @@ -4796,6 +4830,24 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "pallet-asset-conversion" +version = "10.0.0" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +dependencies = [ + "frame-benchmarking", + "frame-support 28.0.0", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-api 26.0.0", + "sp-arithmetic 23.0.0", + "sp-core 28.0.0", + "sp-io 30.0.0", + "sp-runtime 31.0.1", +] + [[package]] name = "pallet-assets" version = "29.1.0" @@ -4839,6 +4891,23 @@ dependencies = [ "sp-runtime 31.0.1", ] +[[package]] +name = "pallet-nfts" +version = "22.0.0" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +dependencies = [ + "enumflags2", + "frame-benchmarking", + "frame-support 28.0.0", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-core 28.0.0", + "sp-io 30.0.0", + "sp-runtime 31.0.1", +] + [[package]] name = "pallet-revive" version = "0.1.0" @@ -4985,6 +5054,44 @@ dependencies = [ "sp-runtime 31.0.1", ] +[[package]] +name = "pallet-xcm" +version = "7.0.0" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +dependencies = [ + "bounded-collections", + "frame-benchmarking", + "frame-support 28.0.0", + "frame-system", + "hex-literal 0.4.1", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core 28.0.0", + "sp-io 30.0.0", + "sp-runtime 31.0.1", + "staging-xcm 7.0.1", + "staging-xcm-builder", + "staging-xcm-executor", + "tracing", + "xcm-runtime-apis", +] + +[[package]] +name = "pallet-xcm-precompiles" +version = "0.1.0" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +dependencies = [ + "frame-support 28.0.0", + "pallet-revive", + "pallet-xcm", + "parity-scale-codec", + "staging-xcm 7.0.1", + "staging-xcm-executor", + "tracing", +] + [[package]] name = "parity-bip39" version = "2.0.1" @@ -5216,6 +5323,34 @@ dependencies = [ "spki", ] +[[package]] +name = "polkadot-core-primitives" +version = "7.0.0" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +dependencies = [ + "parity-scale-codec", + "scale-info", + "sp-core 28.0.0", + "sp-runtime 31.0.1", +] + +[[package]] +name = "polkadot-parachain-primitives" +version = "6.0.0" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +dependencies = [ + "array-bytes", + "bounded-collections", + "derive_more 0.99.20", + "parity-scale-codec", + "polkadot-core-primitives", + "scale-info", + "serde", + "sp-core 28.0.0", + "sp-runtime 31.0.1", + "sp-weights 27.0.0", +] + [[package]] name = "polkavm" version = "0.29.1" @@ -6175,7 +6310,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8200,6 +8335,27 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "staging-xcm" +version = "7.0.1" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +dependencies = [ + "array-bytes", + "bounded-collections", + "derive-where", + "environmental", + "frame-support 28.0.0", + "hex-literal 0.4.1", + "impl-trait-for-tuples", + "parity-scale-codec", + "scale-info", + "serde", + "sp-runtime 31.0.1", + "sp-weights 27.0.0", + "tracing", + "xcm-procedural 7.0.0", +] + [[package]] name = "staging-xcm" version = "18.0.0" @@ -8219,7 +8375,51 @@ dependencies = [ "serde", "sp-runtime 43.0.0", "sp-weights 33.1.0", - "xcm-procedural", + "xcm-procedural 11.0.2", +] + +[[package]] +name = "staging-xcm-builder" +version = "7.0.0" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +dependencies = [ + "environmental", + "frame-support 28.0.0", + "frame-system", + "impl-trait-for-tuples", + "pallet-asset-conversion", + "pallet-transaction-payment", + "parity-scale-codec", + "polkadot-parachain-primitives", + "scale-info", + "sp-arithmetic 23.0.0", + "sp-core 28.0.0", + "sp-io 30.0.0", + "sp-runtime 31.0.1", + "sp-weights 27.0.0", + "staging-xcm 7.0.1", + "staging-xcm-executor", + "tracing", +] + +[[package]] +name = "staging-xcm-executor" +version = "7.0.0" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +dependencies = [ + "environmental", + "frame-benchmarking", + "frame-support 28.0.0", + "impl-trait-for-tuples", + "parity-scale-codec", + "scale-info", + "sp-arithmetic 23.0.0", + "sp-core 28.0.0", + "sp-io 30.0.0", + "sp-runtime 31.0.1", + "sp-weights 27.0.0", + "staging-xcm 7.0.1", + "tracing", ] [[package]] @@ -8678,7 +8878,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -9565,7 +9765,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -9978,6 +10178,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xcm-procedural" +version = "7.0.0" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "xcm-procedural" version = "11.0.2" @@ -9990,6 +10201,20 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "xcm-runtime-apis" +version = "0.1.1" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +dependencies = [ + "frame-support 28.0.0", + "parity-scale-codec", + "scale-info", + "sp-api 26.0.0", + "sp-weights 27.0.0", + "staging-xcm 7.0.1", + "staging-xcm-executor", +] + [[package]] name = "xxhash-rust" version = "0.8.15" diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 766f2b1929..18dcd4da10 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -17,6 +17,9 @@ frame-support = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cb pallet-assets = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } pallet-assets-precompiles = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } pallet-balances = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } +pallet-nfts = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } +pallet-xcm = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } +pallet-xcm-precompiles = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } pallet-transaction-payment = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } pallet-revive = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } pallet-timestamp = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } @@ -25,6 +28,9 @@ sp-core = { workspace = true, default-features = false } sp-externalities = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } sp-runtime = { version = "43.0.0", default-features = false } sp-io = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } +xcm = { package = "staging-xcm", git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } +xcm-builder = { package = "staging-xcm-builder", git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } +xcm-executor = { package = "staging-xcm-executor", git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } ink_primitives = { workspace = true } ink_revive_types = { workspace = true } @@ -56,11 +62,20 @@ std = [ "pallet-transaction-payment/std", "pallet-revive/std", "pallet-timestamp/std", + "pallet-nfts/std", + "pallet-xcm/std", + "pallet-xcm-precompiles/std", "scale/std", "scale-info/std", "sp-core/std", "sp-externalities/std", "sp-io/std", "ink_e2e_macro/std", - "sp-runtime/std" + "sp-runtime/std", + "xcm/std", + "xcm-builder/std", + "xcm-executor/std", ] + +# Enable XCM + pallet-nfts in the test runtime. +xcm = [] diff --git a/crates/runtime/src/api.rs b/crates/runtime/src/api.rs index 5ce3d23047..dcd0c2a061 100644 --- a/crates/runtime/src/api.rs +++ b/crates/runtime/src/api.rs @@ -1,5 +1,6 @@ pub mod assets_api; pub mod balance_api; +pub mod nfts_api; pub mod revive_api; pub mod system_api; pub mod timestamp_api; @@ -8,6 +9,7 @@ pub mod prelude { pub use super::{ assets_api::AssetsAPI, balance_api::BalanceAPI, + nfts_api::NftsAPI, revive_api::ContractAPI, system_api::SystemAPI, timestamp_api::TimestampAPI, diff --git a/crates/runtime/src/api/nfts_api.rs b/crates/runtime/src/api/nfts_api.rs new file mode 100644 index 0000000000..d302c9e545 --- /dev/null +++ b/crates/runtime/src/api/nfts_api.rs @@ -0,0 +1,297 @@ +use crate::{ + AccountIdFor, + IntoAccountId, + RuntimeEnv, +}; +use frame_support::{ + pallet_prelude::DispatchError, + traits::tokens::nonfungibles_v2::{ + Create, + Inspect, + Mutate, + Transfer, + }, +}; +use pallet_nfts::{ + CollectionConfigFor, + CollectionDetailsFor, + ItemConfig, + ItemSettings, +}; + +type CollectionIdOf = >::CollectionId; +type ItemIdOf = >::ItemId; + +/// NFTs API for the runtime. +/// +/// Provides helpers to create collections and manipulate items in `pallet-nfts` +/// when running against the in-memory runtime backend. +pub trait NftsAPI +where + T: RuntimeEnv, + T::Runtime: pallet_nfts::Config, + I: 'static, +{ + /// Creates a new collection owned by `owner` and administered by `admin`. + /// + /// This uses the pallet's `Create` implementation directly, so the caller + /// must provide a config where `CollectionSetting::DepositRequired` is not + /// disabled. + fn create_collection( + &mut self, + owner: impl IntoAccountId>, + admin: impl IntoAccountId>, + config: CollectionConfigFor, + ) -> Result, DispatchError>; + + /// Mints an item into `beneficiary`. + /// + /// `deposit_to_collection_owner` controls whether the deposit is taken from + /// the collection owner instead of the beneficiary. + fn mint_into( + &mut self, + collection: &CollectionIdOf, + item: ItemIdOf, + beneficiary: impl IntoAccountId>, + deposit_to_collection_owner: bool, + ) -> Result<(), DispatchError>; + + /// Transfers ownership of an item. + fn transfer( + &mut self, + collection: &CollectionIdOf, + item: ItemIdOf, + destination: impl IntoAccountId>, + ) -> Result<(), DispatchError>; + + /// Burns an item, optionally enforcing ownership. + fn burn( + &mut self, + collection: &CollectionIdOf, + item: ItemIdOf, + maybe_check_owner: Option>>, + ) -> Result<(), DispatchError>; + + /// Returns the owner of an item, if any. + fn owner_of( + &mut self, + collection: &CollectionIdOf, + item: &ItemIdOf, + ) -> Option>; + + /// Returns collection details, if the collection exists. + fn collection_details( + &mut self, + collection: &CollectionIdOf, + ) -> Option>; + + /// Returns the config for a collection, if it exists. + fn collection_config( + &mut self, + collection: &CollectionIdOf, + ) -> Option>; + + /// Returns the next collection ID. + fn next_collection_id(&mut self) -> Option>; + + /// Checks if a collection exists. + fn collection_exists(&mut self, collection: &CollectionIdOf) -> bool; + + /// Checks if an item exists inside a collection. + fn item_exists( + &mut self, + collection: &CollectionIdOf, + item: &ItemIdOf, + ) -> bool; +} + +impl NftsAPI for T +where + T: RuntimeEnv, + T::Runtime: pallet_nfts::Config, + I: 'static, +{ + fn create_collection( + &mut self, + owner: impl IntoAccountId>, + admin: impl IntoAccountId>, + config: CollectionConfigFor, + ) -> Result, DispatchError> { + let owner = owner.into_account_id(); + let admin = admin.into_account_id(); + self.execute_with(|| { + as Create< + AccountIdFor, + CollectionConfigFor, + >>::create_collection(&owner, &admin, &config) + }) + } + + fn mint_into( + &mut self, + collection: &CollectionIdOf, + item: ItemIdOf, + beneficiary: impl IntoAccountId>, + deposit_to_collection_owner: bool, + ) -> Result<(), DispatchError> { + let beneficiary = beneficiary.into_account_id(); + let item_config = ItemConfig { + settings: ItemSettings::all_enabled(), + }; + self.execute_with(|| { + as Mutate< + AccountIdFor, + ItemConfig, + >>::mint_into( + collection, + &item, + &beneficiary, + &item_config, + deposit_to_collection_owner, + ) + }) + } + + fn transfer( + &mut self, + collection: &CollectionIdOf, + item: ItemIdOf, + destination: impl IntoAccountId>, + ) -> Result<(), DispatchError> { + let destination = destination.into_account_id(); + self.execute_with(|| { + as Transfer< + AccountIdFor, + >>::transfer(collection, &item, &destination) + }) + } + + fn burn( + &mut self, + collection: &CollectionIdOf, + item: ItemIdOf, + maybe_check_owner: Option>>, + ) -> Result<(), DispatchError> { + let maybe_owner = maybe_check_owner.map(|owner| owner.into_account_id()); + self.execute_with(|| { + as Mutate< + AccountIdFor, + ItemConfig, + >>::burn(collection, &item, maybe_owner.as_ref()) + }) + } + + fn owner_of( + &mut self, + collection: &CollectionIdOf, + item: &ItemIdOf, + ) -> Option> { + self.execute_with(|| { + as Inspect>>::owner( + collection, item, + ) + }) + } + + fn collection_details( + &mut self, + collection: &CollectionIdOf, + ) -> Option> { + self.execute_with(|| pallet_nfts::Collection::::get(collection)) + } + + fn collection_config( + &mut self, + collection: &CollectionIdOf, + ) -> Option> { + self.execute_with(|| { + pallet_nfts::CollectionConfigOf::::get(collection) + }) + } + + fn next_collection_id(&mut self) -> Option> { + self.execute_with(|| pallet_nfts::NextCollectionId::::get()) + } + + fn collection_exists(&mut self, collection: &CollectionIdOf) -> bool { + self.execute_with(|| { + pallet_nfts::Collection::::contains_key(collection) + }) + } + + fn item_exists( + &mut self, + collection: &CollectionIdOf, + item: &ItemIdOf, + ) -> bool { + self.execute_with(|| { + pallet_nfts::Item::::contains_key(collection, item) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::DefaultRuntime; + use pallet_nfts::{ + CollectionConfig, + CollectionSettings, + MintSettings, + }; + + type Runtime = ::Runtime; + + fn simple_config() -> CollectionConfigFor { + CollectionConfig { + settings: CollectionSettings::all_enabled(), + max_supply: None, + mint_settings: MintSettings::default(), + } + } + + #[test] + fn create_and_mint_work() { + let mut runtime = DefaultRuntime::default(); + let owner = DefaultRuntime::default_actor(); + let collection_id = runtime + .create_collection(&owner, &owner, simple_config()) + .expect("create failed"); + + assert_eq!(collection_id, 0); + assert!(runtime.collection_exists(&collection_id)); + + runtime + .mint_into(&collection_id, 1u32, &owner, false) + .expect("mint failed"); + + assert_eq!(runtime.owner_of(&collection_id, &1u32), Some(owner)); + } + + #[test] + fn transfer_and_burn_work() { + let mut runtime = DefaultRuntime::default(); + let owner = DefaultRuntime::default_actor(); + let recipient = ink_e2e::bob().into_account_id(); + + let collection = runtime + .create_collection(&owner, &owner, simple_config()) + .expect("create failed"); + runtime + .mint_into(&collection, 7u32, &owner, false) + .expect("mint failed"); + + runtime + .transfer(&collection, 7u32, &recipient) + .expect("transfer failed"); + assert_eq!( + runtime.owner_of(&collection, &7u32), + Some(recipient.clone()) + ); + + runtime + .burn(&collection, 7u32, None::<&AccountIdFor>) + .expect("burn failed"); + assert!(!runtime.item_exists(&collection, &7u32)); + } +} diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index e86440cd6e..87c123a5b4 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -53,9 +53,12 @@ pub use { pallet_assets, pallet_assets_precompiles, pallet_balances, + pallet_nfts, pallet_revive, pallet_timestamp, pallet_transaction_payment, + pallet_xcm, + pallet_xcm_precompiles, paste, scale, sp_core::crypto::Ss58Codec, @@ -64,6 +67,9 @@ pub use { Extension, }, sp_io::TestExternalities, + xcm, + xcm_builder, + xcm_executor, }; pub use client::{ diff --git a/crates/runtime/src/macros.rs b/crates/runtime/src/macros.rs index df0714a260..9ae2339db6 100644 --- a/crates/runtime/src/macros.rs +++ b/crates/runtime/src/macros.rs @@ -126,6 +126,52 @@ macro_rules! assert_noop { }}; } +/// Asserts that a contract call reverted with an error containing a substring. +/// +/// Similar to `assert_noop!`, but uses substring matching instead of exact match. +/// This is useful when the exact error format may vary or when checking for partial +/// error information. +/// +/// # Behavior +/// +/// - Takes a `CallResult` and an expected error substring as input +/// - Checks if `dry_run.did_revert()` is `true` +/// - Panics if the call succeeded (did not revert) +/// - Extracts the error and checks if it contains the expected substring +/// - Returns the `CallResult` if both checks pass +/// +/// # Examples +/// +/// ```ignore +/// let result = client.call(&alice, &failing_call).submit().await?; +/// assert_noop_contains!(result, "revert"); +/// ``` +#[macro_export] +macro_rules! assert_noop_contains { + ($result:expr, $expected_substr:expr) => {{ + let result = $result; + if !result.dry_run.did_revert() { + panic!( + "Expected call to revert with error containing '{}' but it succeeded", + $expected_substr + ); + } + + let actual_error = result.extract_error(); + match &actual_error { + Some(err) if err.contains($expected_substr) => {} + _ => { + panic!( + "Expected error containing '{}' but got {:?}", + $expected_substr, actual_error + ); + } + } + + result + }}; +} + /// Asserts that the latest contract event matches an expected event. /// /// This macro verifies that the last emitted contract event from the runtime @@ -248,7 +294,7 @@ mod construct_runtime { AccountId32, Perbill, FixedU128, }, - traits::{ConstBool, ConstU8, ConstU128, ConstU32, ConstU64, Currency}, + traits::{ConstBool, ConstU8, ConstU128, ConstU32, ConstU64, Currency, Everything, Nothing}, weights::{Weight, IdentityFee}, }; @@ -258,7 +304,7 @@ mod construct_runtime { pub type Balance = u128; - // Define the runtime type as a collection of pallets + #[cfg(feature = "xcm")] construct_runtime!( pub enum $runtime { System: $crate::frame_system, @@ -267,6 +313,24 @@ mod construct_runtime { Assets: $crate::pallet_assets::, Revive: $crate::pallet_revive, TransactionPayment: $crate::pallet_transaction_payment, + Nfts: $crate::pallet_nfts, + PolkadotXcm: $crate::pallet_xcm, + $( + $pallet_name: $pallet, + )* + } + ); + + #[cfg(not(feature = "xcm"))] + construct_runtime!( + pub enum $runtime { + System: $crate::frame_system, + Balances: $crate::pallet_balances, + Timestamp: $crate::pallet_timestamp, + Assets: $crate::pallet_assets::, + Revive: $crate::pallet_revive, + TransactionPayment: $crate::pallet_transaction_payment, + Nfts: $crate::pallet_nfts, $( $pallet_name: $pallet, )* @@ -341,6 +405,198 @@ mod construct_runtime { type WeightInfo = $crate::pallet_transaction_payment::weights::SubstrateWeight<$runtime>; } + // Configure pallet-nfts + pub type NftsCollectionId = u32; + pub type NftsItemId = u32; + + parameter_types! { + pub const NftsCollectionDeposit: Balance = 2; + pub const NftsItemDeposit: Balance = 1; + pub const NftsMetadataDepositBase: Balance = 1; + pub const NftsAttributeDepositBase: Balance = 1; + pub const NftsDepositPerByte: Balance = 1; + pub const NftsStringLimit: u32 = 50; + pub const NftsKeyLimit: u32 = 50; + pub const NftsValueLimit: u32 = 50; + pub const NftsApprovalsLimit: u32 = 10; + pub const NftsItemAttributesApprovalsLimit: u32 = 2; + pub const NftsMaxTips: u32 = 10; + pub const NftsMaxDeadlineDuration: u32 = 10_000; + pub const NftsMaxAttributesPerCall: u32 = 2; + pub NftsFeatures: $crate::pallet_nfts::PalletFeatures = $crate::pallet_nfts::PalletFeatures::all_enabled(); + } + + impl $crate::pallet_nfts::Config for $runtime { + type RuntimeEvent = RuntimeEvent; + type CollectionId = NftsCollectionId; + type ItemId = NftsItemId; + type Currency = Balances; + type CreateOrigin = $crate::frame_support::traits::AsEnsureOriginWithArg<$crate::frame_system::EnsureSigned>; + type ForceOrigin = $crate::frame_system::EnsureRoot; + type Locker = (); + type CollectionDeposit = NftsCollectionDeposit; + type ItemDeposit = NftsItemDeposit; + type MetadataDepositBase = NftsMetadataDepositBase; + type AttributeDepositBase = NftsAttributeDepositBase; + type DepositPerByte = NftsDepositPerByte; + type StringLimit = NftsStringLimit; + type KeyLimit = NftsKeyLimit; + type ValueLimit = NftsValueLimit; + type ApprovalsLimit = NftsApprovalsLimit; + type ItemAttributesApprovalsLimit = NftsItemAttributesApprovalsLimit; + type MaxTips = NftsMaxTips; + type MaxDeadlineDuration = NftsMaxDeadlineDuration; + type MaxAttributesPerCall = NftsMaxAttributesPerCall; + type Features = NftsFeatures; + type OffchainSignature = $crate::frame_support::sp_runtime::MultiSignature; + type OffchainPublic = ::Signer; + #[cfg(feature = "runtime-benchmarks")] + type Helper = (); + type WeightInfo = (); + type BlockNumberProvider = $crate::frame_system::Pallet<$runtime>; + } + + // =============================================================================== + // XCM Configuration (only when "xcm" feature is enabled) + // =============================================================================== + #[cfg(feature = "xcm")] + mod xcm_config { + use super::{ + Balances, PolkadotXcm, RuntimeCall, RuntimeOrigin, AllPalletsWithSystem, + Weight, ConstU32, Everything, Nothing, + }; + // Use frame_support's AccountId32, not XCM's AccountId32 junction + use $crate::frame_support::sp_runtime::AccountId32 as RuntimeAccountId32; + use $crate::xcm::latest::prelude::{ + Location, NetworkId, InteriorLocation, Junctions, + }; + use $crate::xcm_builder::{ + AccountId32Aliases, + AllowExplicitUnpaidExecutionFrom, + AllowTopLevelPaidExecutionFrom, + FixedWeightBounds, + FrameTransactionalProcessor, + SignedAccountId32AsNative, + SignedToAccountId32, + SovereignSignedViaLocation, + TakeWeightCredit, + WithComputedOrigin, + }; + use $crate::pallet_xcm::XcmPassthrough; + use $crate::frame_support::parameter_types; + + parameter_types! { + pub const TokenLocation: Location = Location::here(); + pub const RelayNetwork: Option = None; + pub UniversalLocation: InteriorLocation = Junctions::Here; + // One XCM operation is 1_000_000_000 weight - conservative estimate + pub UnitWeightCost: Weight = Weight::from_parts(1_000_000_000, 64 * 1024); + pub const MaxInstructions: u32 = 100; + pub const MaxAssetsIntoHolding: u32 = 64; + } + + /// Type for specifying how a `Location` can be converted into an `AccountId`. + pub type LocationToAccountId = AccountId32Aliases; + + /// Means for transacting the native currency on this chain. + #[allow(deprecated)] + pub type LocalAssetTransactor = $crate::xcm_builder::CurrencyAdapter< + Balances, + $crate::xcm_builder::IsConcrete, + LocationToAccountId, + RuntimeAccountId32, + (), + >; + + /// This is the type we use to convert an (incoming) XCM origin into a local `Origin` instance. + pub type XcmOriginToTransactDispatchOrigin = ( + SovereignSignedViaLocation, + SignedAccountId32AsNative, + XcmPassthrough, + ); + + /// Barrier that allows everything - suitable for testing. + pub type Barrier = ( + TakeWeightCredit, + WithComputedOrigin< + ( + AllowTopLevelPaidExecutionFrom, + AllowExplicitUnpaidExecutionFrom, + ), + UniversalLocation, + ConstU32<8>, + >, + ); + + pub struct XcmConfig; + impl $crate::xcm_executor::Config for XcmConfig { + type RuntimeCall = RuntimeCall; + type XcmSender = (); // No sending for test runtime + type XcmEventEmitter = PolkadotXcm; + type AssetTransactor = LocalAssetTransactor; + type OriginConverter = XcmOriginToTransactDispatchOrigin; + type IsReserve = (); + type IsTeleporter = (); + type UniversalLocation = UniversalLocation; + type Barrier = Barrier; + type Weigher = FixedWeightBounds; + type Trader = (); + type ResponseHandler = PolkadotXcm; + type AssetTrap = PolkadotXcm; + type AssetClaims = PolkadotXcm; + type SubscriptionService = PolkadotXcm; + type PalletInstancesInfo = AllPalletsWithSystem; + type MaxAssetsIntoHolding = MaxAssetsIntoHolding; + type AssetLocker = (); + type AssetExchanger = (); + type FeeManager = (); + type MessageExporter = (); + type UniversalAliases = Nothing; + type CallDispatcher = RuntimeCall; + type SafeCallFilter = Everything; + type Aliasers = Nothing; + type TransactionalProcessor = FrameTransactionalProcessor; + type HrmpNewChannelOpenRequestHandler = (); + type HrmpChannelAcceptedHandler = (); + type HrmpChannelClosingHandler = (); + type XcmRecorder = PolkadotXcm; + } + + /// Local origins on this chain are allowed to dispatch XCM sends/executions. + pub type LocalOriginToLocation = SignedToAccountId32; + } + + #[cfg(feature = "xcm")] + pub use xcm_config::*; + + #[cfg(feature = "xcm")] + impl $crate::pallet_xcm::Config for $runtime { + type RuntimeEvent = RuntimeEvent; + type SendXcmOrigin = $crate::xcm_builder::EnsureXcmOrigin; + type XcmRouter = (); // No routing for test runtime + type ExecuteXcmOrigin = $crate::xcm_builder::EnsureXcmOrigin; + type XcmExecuteFilter = Everything; + type XcmExecutor = $crate::xcm_executor::XcmExecutor; + type XcmTeleportFilter = Everything; + type XcmReserveTransferFilter = Nothing; + type Weigher = $crate::xcm_builder::FixedWeightBounds; + type UniversalLocation = UniversalLocation; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = $crate::pallet_xcm::CurrentXcmVersion; + type Currency = Balances; + type CurrencyMatcher = (); + type TrustedLockers = (); + type SovereignAccountOf = LocationToAccountId; + type MaxLockers = ConstU32<8>; + type WeightInfo = $crate::pallet_xcm::TestWeightInfo; + type AdminOrigin = $crate::frame_system::EnsureRoot<$crate::frame_support::sp_runtime::AccountId32>; + type MaxRemoteLockConsumers = ConstU32<0>; + type RemoteLockConsumerIdentifier = (); + type AuthorizedAliasConsideration = $crate::frame_support::traits::Disabled; + } + // Configure `pallet-revive` type BalanceOf = >::Balance; impl Convert for $runtime { @@ -363,6 +619,19 @@ mod construct_runtime { pub const DepositPerChildTrieItem: Balance = deposit(1, 0) / 100; } + // Precompiles type alias - compose assets/xcm precompiles based on enabled features. + // Precompiles type alias - conditionally includes XcmPrecompile when "xcm" feature is enabled + #[cfg(not(feature = "xcm"))] + pub type RevivePrecompiles = ( + $crate::pallet_assets_precompiles::ERC20<$runtime, $crate::pallet_assets_precompiles::InlineIdConfig<{ 0x0120 }>, TrustBackedAssetsInstance>, + ); + + #[cfg(feature = "xcm")] + pub type RevivePrecompiles = ( + $crate::pallet_assets_precompiles::ERC20<$runtime, $crate::pallet_assets_precompiles::InlineIdConfig<{ 0x0120 }>, TrustBackedAssetsInstance>, + $crate::pallet_xcm_precompiles::XcmPrecompile<$runtime>, + ); + impl $crate::pallet_revive::Config for $runtime { type AddressMapper = $crate::pallet_revive::AccountId32Mapper; type ChainId = ConstU64<1>; @@ -385,10 +654,7 @@ mod construct_runtime { type UploadOrigin = $crate::frame_system::EnsureSigned; type InstantiateOrigin = $crate::frame_system::EnsureSigned; type FindAuthor = (); - type Precompiles = ( - $crate::pallet_assets_precompiles::ERC20, TrustBackedAssetsInstance>, - // todo add `PoolAssetsInstance` - ); + type Precompiles = RevivePrecompiles; type AllowEVMBytecode = ConstBool; type FeeInfo = (); type MaxEthExtrinsicWeight = MaxEthExtrinsicWeight; @@ -491,10 +757,18 @@ mod construct_runtime { } // Export runtime type itself, pallets and useful types from the auxiliary module +#[cfg(not(feature = "xcm"))] +pub use construct_runtime::{ + $runtime_env, $runtime, Assets, AssetIdForTrustBackedAssets, Balances, Nfts, + NftsCollectionId, NftsItemId, Revive, PalletInfo, RuntimeCall, RuntimeEvent, RuntimeHoldReason, + RuntimeOrigin, System, Timestamp, TrustBackedAssetsInstance, +}; + +#[cfg(feature = "xcm")] pub use construct_runtime::{ - $runtime_env, $runtime, Assets, AssetIdForTrustBackedAssets, Balances, Revive, PalletInfo, - RuntimeCall, RuntimeEvent, RuntimeHoldReason, RuntimeOrigin, System, Timestamp, - TrustBackedAssetsInstance, + $runtime_env, $runtime, Assets, AssetIdForTrustBackedAssets, Balances, Nfts, + NftsCollectionId, NftsItemId, Revive, PalletInfo, PolkadotXcm, RuntimeCall, RuntimeEvent, RuntimeHoldReason, + RuntimeOrigin, System, Timestamp, TrustBackedAssetsInstance, }; }; } diff --git a/integration-tests/public/nfts-call-runtime/Cargo.toml b/integration-tests/public/nfts-call-runtime/Cargo.toml new file mode 100644 index 0000000000..5d4f9d598b --- /dev/null +++ b/integration-tests/public/nfts-call-runtime/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "nfts_call_runtime" +version = "6.0.0-beta.1" +authors = ["Use Ink "] +edition = "2024" +publish = false + +[dependencies] +ink = { path = "../../../crates/ink", default-features = false, features = ["xcm"] } +scale = { package = "parity-scale-codec", version = "3.7.4", default-features = false, features = ["derive"] } +scale-info = { version = "2.11", default-features = false, features = ["derive"] } + +[dev-dependencies] +ink_e2e = { path = "../../../crates/e2e" } +ink_runtime = { path = "../../../crates/runtime", features = ["xcm"] } +pallet-nfts = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false, features = ["std"] } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "ink/xcm", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] +e2e-tests = [] + +[package.metadata.ink-lang] +abi = "ink" + +[lints.rust.unexpected_cfgs] +level = "warn" +check-cfg = [ + 'cfg(ink_abi, values("ink", "sol", "all"))' +] diff --git a/integration-tests/public/nfts-call-runtime/lib.rs b/integration-tests/public/nfts-call-runtime/lib.rs new file mode 100644 index 0000000000..c014f14a5e --- /dev/null +++ b/integration-tests/public/nfts-call-runtime/lib.rs @@ -0,0 +1,500 @@ +//! # NFTs Call Runtime Example +//! +//! This contract demonstrates how to call `pallet-nfts` runtime functions from +//! an ink! contract using `xcm_execute` with the `Transact` instruction. +//! +//! ## How It Works +//! +//! The contract uses `xcm_execute` with a `Transact` instruction to dispatch +//! calls to `pallet-nfts`. The call arguments are passed as pre-encoded bytes, +//! keeping the contract minimal while the caller (e.g., e2e tests) handles encoding. +//! +//! ## Error Handling Limitation +//! +//! **Important**: Specific pallet errors (e.g., `UnknownItem`, `NotOwner`) are NOT +//! propagated through the XCM error path. The contract only knows if the call +//! succeeded or failed, not WHY it failed. Use e2e tests with `RuntimeAPI` to +//! verify specific pallet errors during development. + +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +mod nfts_call_runtime { + use ink::{ + prelude::vec::Vec, + xcm::{ + DoubleEncoded, + prelude::*, + }, + }; + + pub type CollectionId = u32; + pub type ItemId = u32; + + // Pallet and call indices in the paseo asset hub runtime. + // Find these by inspecting the runtime's `construct_runtime!` macro. + const NFTS_PALLET_INDEX: u8 = 6; + const CREATE_COLLECTION: u8 = 0; + const MINT: u8 = 3; + const BURN: u8 = 5; + const TRANSFER: u8 = 6; + + /// Error type for runtime call failures. + #[ink::error] + pub enum RuntimeCallError { + /// Failed to weigh the XCM message. + WeighFailed, + /// The runtime call failed (XCM execution error). + ExecuteFailed, + /// Caller is not the contract owner. + NotOwner, + } + + #[ink(storage)] + pub struct NftsRuntimeCaller { + /// Trusted collection id supplied by the deployer. The contract cannot verify + /// that the encoded `create` call actually creates this id; keep the two + /// in sync. + collection: CollectionId, + next_item: ItemId, + owner: Address, + } + + impl NftsRuntimeCaller { + /// Creates a collection in the runtime. + /// + /// The `encoded_call_args` should be SCALE-encoded arguments for + /// `pallet_nfts::Call::create { admin, config }`. + /// The caller must ensure `collection` matches the id the runtime will + /// assign; the contract cannot validate this. + /// + /// The constructor is payable because the runtime will reserve deposits for + /// the collection on the contract account. + #[ink(constructor, payable)] + pub fn new( + collection: CollectionId, + encoded_call_args: Vec, + ) -> Result { + let instance = Self { + collection, + next_item: 0, + owner: Self::env().caller(), + }; + instance.call_runtime(CREATE_COLLECTION, encoded_call_args)?; + Ok(instance) + } + + /// Returns the configured collection id. + #[ink(message)] + pub fn collection_id(&self) -> CollectionId { + self.collection + } + + /// Returns the owner of this contract. + #[ink(message)] + pub fn owner(&self) -> Address { + self.owner + } + + /// Returns the next item id that will be minted. + #[ink(message)] + pub fn next_item_id(&self) -> ItemId { + self.next_item + } + + /// Mints an NFT into the runtime using `pallet-nfts`. + /// + /// The `encoded_call_args` should be SCALE-encoded arguments for + /// `pallet_nfts::Call::mint { collection, item, mint_to, witness_data }`. + /// + /// The mint is payable to cover item deposits in the runtime. + #[ink(message, payable)] + pub fn mint( + &mut self, + encoded_call_args: Vec, + ) -> Result { + self.ensure_owner()?; + let item = self.next_item; + self.call_runtime(MINT, encoded_call_args)?; + self.next_item = self.next_item.saturating_add(1); + Ok(item) + } + + /// Transfers an item owned by the contract to another account. + /// + /// The `encoded_call_args` should be SCALE-encoded arguments for + /// `pallet_nfts::Call::transfer { collection, item, dest }`. + #[ink(message)] + pub fn transfer( + &mut self, + encoded_call_args: Vec, + ) -> Result<(), RuntimeCallError> { + self.ensure_owner()?; + self.call_runtime(TRANSFER, encoded_call_args) + } + + /// Burns an item in the runtime. + /// + /// The `encoded_call_args` should be SCALE-encoded arguments for + /// `pallet_nfts::Call::burn { collection, item }`. + #[ink(message)] + pub fn burn( + &mut self, + encoded_call_args: Vec, + ) -> Result<(), RuntimeCallError> { + self.ensure_owner()?; + self.call_runtime(BURN, encoded_call_args) + } + + /// Execute a runtime call to pallet-nfts via XCM Transact. + /// + /// This builds an XCM message with: + /// 1. `Transact` - executes the SCALE-encoded pallet call + /// 2. `ExpectTransactStatus(Success)` - fails if the pallet call fails + fn call_runtime( + &self, + call_index: u8, + encoded_args: Vec, + ) -> Result<(), RuntimeCallError> { + // Build the full call: [pallet_index, call_index, encoded_args...] + let mut encoded_call = Vec::with_capacity(2 + encoded_args.len()); + encoded_call.push(NFTS_PALLET_INDEX); + encoded_call.push(call_index); + encoded_call.extend(encoded_args); + + // Build XCM message with Transact + ExpectTransactStatus + let call: DoubleEncoded<()> = encoded_call.into(); + let xcm_msg: Xcm<()> = Xcm::builder_unsafe() + .transact( + OriginKind::SovereignAccount, + Weight::from_parts(u64::MAX, u64::MAX), + call, + ) + .expect_transact_status(MaybeErrorCode::Success) + .build(); + + // Weigh and execute + let versioned_msg = VersionedXcm::from(xcm_msg); + let weight = self + .env() + .xcm_weigh(&versioned_msg) + .map_err(|_| RuntimeCallError::WeighFailed)?; + self.env() + .xcm_execute(&versioned_msg, weight) + .map_err(|_| RuntimeCallError::ExecuteFailed) + } + + fn ensure_owner(&self) -> Result<(), RuntimeCallError> { + if self.env().caller() != self.owner { + return Err(RuntimeCallError::NotOwner); + } + Ok(()) + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + use super::*; + use ink::scale::Encode; + use ink_e2e::{ + ContractsBackend, + alice, + bob, + }; + use ink_runtime::{ + AccountId32, + IntoAccountId, + api::{ + prelude::NftsAPI, + revive_api::ContractAPI, + }, + assert_ok, + }; + use pallet_nfts::{ + CollectionConfig, + CollectionSettings, + MintSettings, + MintType, + MintWitness, + }; + + type E2EResult = std::result::Result; + type Balance = u128; + type BlockNumber = u32; + type RuntimeCollectionConfig = + CollectionConfig; + type RuntimeMintSettings = MintSettings; + type RuntimeMintWitness = MintWitness; + + /// Deployment creates a collection and honors config (e.g., max supply). + #[ink_e2e::test(runtime)] + async fn constructor_creates_collection_with_config( + mut client: Client, + ) -> E2EResult<()> { + let admin = alice().into_account_id(); + let max_supply = Some(42u32); + let create_args = + encode_create_args(admin, default_collection_config(max_supply)); + let mut constructor = NftsRuntimeCallerRef::new(0, create_args); + + let contract = client + .instantiate("nfts_call_runtime", &alice(), &mut constructor) + .value(1_000_000_000_000u128) + .submit() + .await?; + + let calls = contract.call_builder::(); + + let collection_id = client + .call(&alice(), &calls.collection_id()) + .dry_run() + .await?; + assert_eq!(collection_id.return_value(), 0); + + let next_item = client + .call(&alice(), &calls.next_item_id()) + .dry_run() + .await?; + assert_eq!(next_item.return_value(), 0); + + let config = client + .runtime() + .collection_config(&0u32) + .expect("collection config should exist"); + assert_eq!(config.max_supply, max_supply); + assert!(client.runtime().collection_exists(&0u32)); + + Ok(()) + } + + /// Happy path that mints, transfers, and checks runtime state. + #[ink_e2e::test(runtime)] + async fn mint_and_transfer_happy_path(mut client: Client) -> E2EResult<()> { + let admin = alice().into_account_id(); + let create_args = encode_create_args(admin, default_collection_config(None)); + let mut constructor = NftsRuntimeCallerRef::new(0, create_args); + + let contract = client + .instantiate("nfts_call_runtime", &alice(), &mut constructor) + .value(1_000_000_000_000u128) + .submit() + .await?; + + let _ = client.runtime().map_account(&bob()); + let bob_account = bob().into_account_id(); + let mut calls = contract.call_builder::(); + + // Mint item 0 to the contract + let mint_args = + encode_mint_args(0, 0, to_runtime_account_id(contract.account_id)); + let mint0 = client + .call(&alice(), &calls.mint(mint_args)) + .submit() + .await?; + assert_ok!(mint0); + + // Mint item 1 to bob + let mint_args = encode_mint_args(0, 1, bob_account.clone()); + let mint1 = client + .call(&alice(), &calls.mint(mint_args)) + .submit() + .await?; + assert_ok!(mint1); + + // Transfer item 0 to bob + let transfer_args = encode_transfer_args(0, 0, bob_account.clone()); + let transfer = client + .call(&alice(), &calls.transfer(transfer_args)) + .submit() + .await?; + assert_ok!(transfer); + + // Runtime-side assertions + assert_eq!( + client.runtime().owner_of(&0u32, &0u32), + Some(bob_account.clone()) + ); + assert_eq!( + client.runtime().owner_of(&0u32, &1u32), + Some(bob_account.clone()) + ); + assert!(client.runtime().item_exists(&0u32, &0u32)); + assert!(client.runtime().item_exists(&0u32, &1u32)); + + let details = client + .runtime() + .collection_details(&0u32) + .expect("collection should exist"); + assert_eq!(details.items, 2); + + let next_item = client + .call(&alice(), &calls.next_item_id()) + .dry_run() + .await?; + assert_eq!(next_item.return_value(), 2); + + Ok(()) + } + + /// Burning clears ownership but item IDs continue to advance when minting again. + #[ink_e2e::test(runtime)] + async fn burn_and_reissue(mut client: Client) -> E2EResult<()> { + let admin = alice().into_account_id(); + let create_args = encode_create_args(admin, default_collection_config(None)); + let mut constructor = NftsRuntimeCallerRef::new(0, create_args); + + let contract = client + .instantiate("nfts_call_runtime", &alice(), &mut constructor) + .value(1_000_000_000_000u128) + .submit() + .await?; + + let mut calls = contract.call_builder::(); + + let mint_args = encode_mint_args( + 0, + 0, + to_runtime_account_id(contract.account_id.clone()), + ); + let mint0 = client + .call(&alice(), &calls.mint(mint_args)) + .submit() + .await?; + assert_ok!(mint0); + assert!(client.runtime().item_exists(&0u32, &0u32)); + + let burn_args = encode_burn_args(0, 0); + let burn0 = client + .call(&alice(), &calls.burn(burn_args)) + .submit() + .await?; + assert_ok!(burn0); + assert!(!client.runtime().item_exists(&0u32, &0u32)); + assert!(client.runtime().owner_of(&0u32, &0u32).is_none()); + + let mint_args = encode_mint_args( + 0, + 1, + to_runtime_account_id(contract.account_id.clone()), + ); + let mint1 = client + .call(&alice(), &calls.mint(mint_args)) + .submit() + .await?; + assert_ok!(mint1); + + assert!(!client.runtime().item_exists(&0u32, &0u32)); + assert!(client.runtime().item_exists(&0u32, &1u32)); + + let details = client + .runtime() + .collection_details(&0u32) + .expect("collection should exist"); + assert_eq!(details.items, 1); + + let next_item = client + .call(&alice(), &calls.next_item_id()) + .dry_run() + .await?; + assert_eq!(next_item.return_value(), 2); + + Ok(()) + } + + /// Transfer should fail for a non-existent item, with pallet error observed via + /// runtime API. + #[ink_e2e::test(runtime)] + async fn transfer_nonexistent_item_fails(mut client: Client) -> E2EResult<()> { + let admin = alice().into_account_id(); + let create_args = encode_create_args(admin, default_collection_config(None)); + let mut constructor = NftsRuntimeCallerRef::new(0, create_args); + + let contract = client + .instantiate("nfts_call_runtime", &alice(), &mut constructor) + .value(1_000_000_000_000u128) + .submit() + .await?; + + let _ = client.runtime().map_account(&bob()); + let bob_account = bob().into_account_id(); + + let mut calls = contract.call_builder::(); + + // Verify the contract call fails. + // Note: The specific pallet error (UnknownItem) is NOT propagated through + // the XCM error path. The contract only sees a generic revert. + let transfer_args = encode_transfer_args(0, 42, bob_account); + let transfer_res = client + .call(&alice(), &calls.transfer(transfer_args)) + .submit() + .await?; + assert!(transfer_res.dry_run.did_revert()); + + // Verify the pallet error directly via runtime API.e the specific error. + let runtime_err = client + .runtime() + .transfer(&0u32, 42u32, &bob()) + .expect_err("transfer of non-existent item should fail"); + assert!(format!("{:?}", runtime_err).contains("UnknownItem")); + + Ok(()) + } + + #[derive(Encode)] + enum MultiAddress { + Id(AccountId32), + } + + fn unlookup(account: AccountId32) -> MultiAddress { + MultiAddress::Id(account) + } + + fn encode_create_args( + admin: AccountId32, + config: RuntimeCollectionConfig, + ) -> Vec { + (unlookup(admin), config).encode() + } + + fn encode_mint_args( + collection: CollectionId, + item: ItemId, + mint_to: AccountId32, + ) -> Vec { + ( + collection, + item, + unlookup(mint_to), + Option::::None, + ) + .encode() + } + + fn encode_transfer_args( + collection: CollectionId, + item: ItemId, + dest: AccountId32, + ) -> Vec { + (collection, item, unlookup(dest)).encode() + } + + fn encode_burn_args(collection: CollectionId, item: ItemId) -> Vec { + (collection, item).encode() + } + + fn default_collection_config(max_supply: Option) -> RuntimeCollectionConfig { + RuntimeCollectionConfig { + settings: CollectionSettings::all_enabled(), + max_supply, + mint_settings: RuntimeMintSettings { + mint_type: MintType::Public, + ..Default::default() + }, + } + } + + fn to_runtime_account_id(id: AccountId) -> AccountId32 { + AccountId32::from(*AsRef::<[u8; 32]>::as_ref(&id)) + } + } +}