--- a/Cargo.lock +++ b/Cargo.lock @@ -382,9 +382,9 @@ [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" @@ -634,9 +634,9 @@ [[package]] name = "block-buffer" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03588e54c62ae6d763e2a80090d50353b785795361b4ff5b3bf0a5097fc31c0b" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ "generic-array 0.14.5", ] @@ -1097,9 +1097,9 @@ [[package]] name = "core-foundation" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ "core-foundation-sys", "libc", @@ -1229,9 +1229,9 @@ [[package]] name = "crc32fast" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2209c310e29876f7f0b2721e7e26b84aff178aa3da5d091f9bfbf47669e60e3" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if 1.0.0", ] @@ -1288,9 +1288,9 @@ [[package]] name = "crypto-common" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +checksum = "a4600d695eb3f6ce1cd44e6e291adceb2cc3ab12f20a33777ecd0bf6eba34e06" dependencies = [ "generic-array 0.14.5", ] @@ -1898,13 +1898,12 @@ [[package]] name = "digest" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +checksum = "8cb780dce4f9a8f5c087362b3a4595936b2019e7c8b30f2c3e9a7e94e6ae9837" dependencies = [ - "block-buffer 0.10.1", + "block-buffer 0.10.2", "crypto-common", - "generic-array 0.14.5", ] [[package]] @@ -3241,9 +3240,9 @@ [[package]] name = "httparse" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" +checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" [[package]] name = "httpdate" @@ -4664,9 +4663,9 @@ [[package]] name = "memmap2" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe3179b85e1fd8b14447cbebadb75e45a1002f541b925f0bfec366d56a81c56d" +checksum = "057a3db23999c867821a7a59feb06a578fcb03685e983dff90daf9e7d24ac08f" dependencies = [ "libc", ] @@ -8805,7 +8804,7 @@ source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.16#4aeb95f7f38fcd519e2628f32f79044a8fef99d5" dependencies = [ "impl-trait-for-tuples", - "memmap2 0.5.2", + "memmap2 0.5.3", "parity-scale-codec", "sc-chain-spec-derive", "sc-network", @@ -9912,7 +9911,7 @@ dependencies = [ "cfg-if 1.0.0", "cpufeatures 0.2.1", - "digest 0.10.1", + "digest 0.10.2", ] [[package]] @@ -11644,7 +11643,7 @@ [[package]] name = "unique-node" -version = "0.9.14" +version = "0.9.16" dependencies = [ "cumulus-client-cli", "cumulus-client-collator", --- a/Dockerfile-parachain +++ b/Dockerfile-parachain @@ -90,7 +90,7 @@ nvm install v15.5.0 && \ nvm use v15.5.0 -RUN git clone https://github.com/PureStake/polkadot-launch -b jlm-use-para-id-in-specs +RUN git clone https://github.com/paritytech/polkadot-launch RUN export NVM_DIR="$HOME/.nvm" && \ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" && \ --- a/README.md +++ b/README.md @@ -29,26 +29,11 @@ Please see our [walk-thorugh instructions](doc/hackusama_walk_through.md) to try everything out! -## Hackusama Update - -During the Kusama Hackaphon the following changes were made: - -- Enabled Smart Contracts Pallet -- Enabled integration between Smart Contracts and Unique Pallet (required special edition of RC4 Substrate version) -- Fixed misc. bugs in Unique Pallet -- Deployed Unique TestNet. Public node available at wss://unique.usetech.com, custom UI types - see below in this README. -- New Features: - - Re-Fungible Token Mode - - Off-Chain Schema to store token image URLs - - Alternative economic model - - Allow Lists and Public Mint Permission -- Use example: [SubstraPunks Game](https://github.com/usetech-llc/substrapunks), fully hosted on IPFS and Unique Testnet - Blockchain. - ## Application Development If you are building an application that operates NFT tokens, use [this document](doc/application_development.md). + ## Building Building Unique chain requires special versions of Rust and toolchain. We don't use the most recent versions of everything @@ -57,8 +42,8 @@ 1. Install Rust: ```bash +sudo apt-get install git curl libssl-dev llvm pkg-config libclang-dev clang curl https://sh.rustup.rs -sSf | sh -sudo apt-get install libssl-dev pkg-config libclang-dev clang ``` 2. Remove all installed toolchains with `rustup toolchain list` and `rustup toolchain uninstall `. @@ -86,111 +71,52 @@ cargo build --release ``` -## Run - -You can start a development chain with: - -```bash -cargo run -- --dev -``` - -Detailed logs may be shown by running the node with the following environment variables set: -`RUST_LOG=debug RUST_BACKTRACE=1 cargo run -- --dev`. +## Building as Parachain locally -If you want to see the multi-node consensus algorithm in action locally, then you can create a local testnet with two -validator nodes for Alice and Bob, who are the initial authorities of the genesis chain that have been endowed with -testnet units. Give each node a name and expose them so they are listed on the Polkadot -[telemetry site](https://telemetry.polkadot.io/#/Local%20Testnet). You'll need two terminal windows open. +Note: checkout this project and all related projects (see below) in the sibling folders (both under the same folder) -We'll start Alice's substrate node first on default TCP port 30333 with her chain database stored locally at -`/tmp/alice`. The bootnode ID of her node is `QmQZ8TjTqeDj3ciwr93EJ95hxfDsb9pEYDizUAbWpigtQN`, which is generated from -the `--node-key` value that we specify below: +### Polkadot launch utility -```bash -cargo run -- \ - --base-path /tmp/alice \ - --chain=local \ - --alice \ - --node-key 0000000000000000000000000000000000000000000000000000000000000001 \ - --telemetry-url ws://telemetry.polkadot.io:1024 \ - --validator ``` - -In the second terminal, we'll start Bob's substrate node on a different TCP port of 30334, and with his chain database -stored locally at `/tmp/bob`. We'll specify a value for the `--bootnodes` option that will connect his node to Alice's -bootnode ID on TCP port 30333: - -```bash -cargo run -- \ - --base-path /tmp/bob \ - --bootnodes /ip4/127.0.0.1/tcp/30333/p2p/QmQZ8TjTqeDj3ciwr93EJ95hxfDsb9pEYDizUAbWpigtQN \ - --chain=local \ - --bob \ - --port 30334 \ - --telemetry-url ws://telemetry.polkadot.io:1024 \ - --validator +git clone https://github.com/paritytech/polkadot-launch ``` - -Additional CLI usage options are available and may be shown by running `cargo run -- --help`. - -## Building and Running as Parachain locally - -Rust toolchain: nightly-2021-11-11 -Note: checkout this project and polkadot project (see below) in the sibling folders (both under the same folder) ### Build relay ``` git clone https://github.com/paritytech/polkadot.git cd polkadot -git checkout release-v0.9.9 +git checkout release-v0.9.16 cargo build --release ``` -### Build parachain +### Build Unique parachain Run in the root of this project: ``` -cargo --build +cargo build --release ``` -### Run 4-node Relay - -1. Download `rococo-custom-4.json` chain spec here: https://substrate.dev/cumulus-workshop/shared/chainspecs/rococo-custom-4.json -2. Use these instructions to launch 4 nodes: https://substrate.dev/cumulus-workshop/#/en/2-relay-chain/1-launch +### Build Acala parachain (optional, full config only) -Example (Run in polkadot folder. Replace `12D3KooWNLAmKcyee3oqSgTMthaQVXaAcXeo8RrGCzMfMVA3B5on` with output from Alice node): ``` -./target/release/polkadot --alice --validator --base-path ./cumulus_relay0 --chain rococo-custom-4.json --port 50555 --ws-port 9944 -./target/release/polkadot --bob --validator --base-path ./cumulus_relay1 --chain rococo-custom-4.json --bootnodes /ip4/127.0.0.1/tcp/30333/p2p/12D3KooWNLAmKcyee3oqSgTMthaQVXaAcXeo8RrGCzMfMVA3B5on --port 50556 --ws-port 9945 -./target/release/polkadot --charlie --validator --base-path ./cumulus_relay1 --chain rococo-custom-4.json --bootnodes /ip4/127.0.0.1/tcp/30333/p2p/12D3KooWNLAmKcyee3oqSgTMthaQVXaAcXeo8RrGCzMfMVA3B5on --port 50557 --ws-port 9946 -./target/release/polkadot --dave --validator --base-path ./cumulus_relay1 --chain rococo-custom-4.json --bootnodes /ip4/127.0.0.1/tcp/30333/p2p/12D3KooWNLAmKcyee3oqSgTMthaQVXaAcXeo8RrGCzMfMVA3B5on --port 50558 --ws-port 9947 - +git clone https://github.com/AcalaNetwork/Acala +cd Acala +git checkout 54db3acd409a0b787f116f20e163a3b24101ce38 +make build-release ``` -3. Export genesis state and runtime wasm from Unique parachain: +## Running as Parachain locally -Run from this project root: ``` -./target/release/unique-collator export-genesis-state --parachain-id 2000 > ./resources/para-2000-genesis -./target/release/unique-collator export-genesis-wasm > ./resources/para-2000-wasm +./launch-testnet.sh ``` -4. Run two parachain nodes: - -Replace `12D3KooWN1ah2bFQSysEFnwZqcmcVpDDR8UedXyo6xfzV1zDNMNg` with Alice or Bob relay ID - -Run from this project root: +Optional, full setup with Acala and Statemint ``` -./target/release/unique-collator --alice --collator --force-authoring --base-path ./tmp/parachain-alice --parachain-id 2000 --port 40333 --ws-port 9844 -- --execution wasm --chain ../polkadot/rococo-custom-4.json --port 30343 --ws-port 9977 -./target/release/unique-collator --bob --collator --force-authoring --parachain-id 2000 --base-path ./tmp/parachain/bob --port 40334 --ws-port 9845 -- --execution wasm --chain ../polkadot/rococo-custom-4.json --port 30344 --ws-port 9978 --bootnodes /ip4/127.0.0.1/tcp/50556/p2p/12D3KooWN1ah2bFQSysEFnwZqcmcVpDDR8UedXyo6xfzV1zDNMNg +./launch-testnet-full.sh ``` - -4. Reserve parachain ID as described here: https://substrate.dev/cumulus-workshop/#/en/2-relay-chain/2-reserve -5. Register parachain in relay as described here: https://substrate.dev/cumulus-workshop/#/en/3-parachains/2-register - - ## Run Integration Tests 1. Install all needed dependecies @@ -202,40 +128,11 @@ 2. Run tests ``` yarn test -``` - -## Benchmarks - -First of all, add rust toolchain and make it default. -```bash -rustup target add wasm32-unknown-unknown --toolchain nightly-2021-11-11 -``` - -Then in "/node/src" run build command below -```bash -cargo +nightly-2021-11-11 build --release --features runtime-benchmarks -``` - -Run benchmark -```bash -target/release/unique-collator benchmark --chain dev --pallet "pallet_unique" --extrinsic "*" --repeat 1 ``` - -## UI custom types - -Moved to [runtime_types.json](./runtime_types.json). - -## Running Integration Tests -See [tests/README.md](./tests/README.md). ## Code Formatting -### Get formatter and linter settings into your branch (if you forked before they were introduced) -```bash -git cherry-pick -n 8ff77c21b0d30b2a4648fa35dbf61dfa9d3948a7 -``` - ### Apply formatting and clippy fixes ```bash cargo clippy @@ -252,84 +149,57 @@ cd tests && yarn eslint --ext .ts,.js src/ ``` -## Re-Enabling Ink! Contracts -Uncomment following lies: -1. In node/rpc/Cargo.toml -``` -# pallet-contracts-rpc = { version = "3.0", git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.9' } -``` +## Karura token transfer -2. In node/rpc/src/lib.rs -``` -// C::Api: pallet_contracts_rpc::ContractsRuntimeApi, -... -// use pallet_contracts_rpc::{Contracts, ContractsApi}; -... -// io.extend_with(ContractsApi::to_delegate(Contracts::new(client.clone()))); +To get started, you need to open inbound and outbound hrmp channels. +### Next, we need to register our asset at Karura. ``` +assetRegistry -> registerForeignAsset(location, metadata) +location: + V0(X2(Parent, Parachain(PARA_ID))) +metadata: + name OPL + symbol OPL + decimals 18 +minimalBalance 1 +``` -3. In runtime/Cargo.toml +### Next, we can send tokens from Opal to Karura: ``` - # 'pallet-contracts/std', - # 'pallet-contracts-primitives/std', - # 'pallet-contracts-rpc-runtime-api/std', - # 'pallet-contract-helpers/std', -... - # [dependencies.pallet-contracts] - # git = 'https://github.com/paritytech/substrate.git' - # default-features = false - # branch = 'polkadot-v0.9.9' - # version = '3.0.0' +polkadotXcm -> reserveTransferAssets +dest: + V0(X2(Parent, Parachain())) +beneficiary: + X1(AccountId(Any, )) +assets: + V1(Concrete(0,Here), Fungible()) +feeAssetItem: + 0 +weightLimit: + +``` - # [dependencies.pallet-contracts-primitives] - # git = 'https://github.com/paritytech/substrate.git' - # default-features = false - # branch = 'polkadot-v0.9.9' - # version = '3.0.0' +The result will be displayed in ChainState +tokens -> accounts - # [dependencies.pallet-contracts-rpc-runtime-api] - # git = 'https://github.com/paritytech/substrate.git' - # default-features = false - # branch = 'polkadot-v0.9.9' - # version = '3.0.0' -... - # pallet-contract-helpers = { path = '../pallets/contract-helpers', default-features = false, version = '0.1.0' } +### To send tokens from Karura to Opal: ``` +xtokens -> transfer -4. runtime/src/lib.rs -``` -// use pallet_contracts::weights::WeightInfo; -... -// pub use pallet_timestamp::Call as TimestampCall; -... -// mod chain_extension; -// use crate::chain_extension::{NFTExtension, Imbalance}; -... -/* -parameter_types! { - pub TombstoneDeposit: Balance = deposit( - ... -} -*/ -... -//pallet_contract_helpers::ContractSponsorshipHandler, -... -// impl pallet_contract_helpers::Config for Runtime {} -... -// Contracts: pallet_contracts::{Pallet, Call, Storage, Event}, -... -// ContractHelpers: pallet_contract_helpers::{Pallet, Call, Storage}, -... -//pallet_contract_helpers::ContractHelpersExtension, -... -/* - impl pallet_contracts_rpc_runtime_api::ContractsApi - for Runtime - { - ... - } -*/ +currencyId: + ForeingAsset + -``` +amount: + +dest: + V1 + ( + Parents:1, + X2(Parachain(), AccountId(Any, ) + ) +destWeight: + +``` \ No newline at end of file --- a/client/rpc/src/lib.rs +++ b/client/rpc/src/lib.rs @@ -4,7 +4,7 @@ use jsonrpc_core::{Error as RpcError, ErrorCode, Result}; use jsonrpc_derive::rpc; use up_data_structs::{Collection, CollectionId, CollectionStats, TokenId}; -use sp_api::{BlockId, BlockT, ProvideRuntimeApi}; +use sp_api::{BlockId, BlockT, ProvideRuntimeApi, ApiExt}; use sp_blockchain::HeaderBackend; use up_rpc::UniqueApi as UniqueRuntimeApi; @@ -132,7 +132,10 @@ } macro_rules! pass_method { - ($method_name:ident($($name:ident: $ty:ty),* $(,)?) -> $result:ty $(=> $mapper:expr)?) => { + ( + $method_name:ident($($name:ident: $ty:ty),* $(,)?) -> $result:ty $(=> $mapper:expr)? + $(; changed_in $ver:expr, $changed_method_name:ident ($($changed_name:expr), * $(,)?) => $fixer:expr)* + ) => { fn $method_name( &self, $( @@ -142,8 +145,25 @@ ) -> Result<$result> { let api = self.client.runtime_api(); let at = BlockId::hash(at.unwrap_or_else(|| self.client.info().best_hash)); + let _api_version = if let Ok(Some(api_version)) = + api.api_version::>(&at) + { + api_version + } else { + // unreachable for our runtime + return Err(RpcError { + code: ErrorCode::InvalidParams, + message: "Api is not available".into(), + data: None, + }) + }; - let result = api.$method_name(&at, $($name),*).map_err(|e| RpcError { + let result = $(if _api_version < $ver { + api.$changed_method_name(&at, $($changed_name),*).map(|r| r.map($fixer)) + } else)* + { api.$method_name(&at, $($name),*) }; + + let result = result.map_err(|e| RpcError { code: ErrorCode::ServerError(Error::RuntimeError.into()), message: "Unable to query".into(), data: Some(format!("{:?}", e).into()), @@ -168,7 +188,10 @@ { pass_method!(account_tokens(collection: CollectionId, account: CrossAccountId) -> Vec); pass_method!(token_exists(collection: CollectionId, token: TokenId) -> bool); - pass_method!(token_owner(collection: CollectionId, token: TokenId) -> Option); + pass_method!( + token_owner(collection: CollectionId, token: TokenId) -> Option; + changed_in 2, token_owner_before_version_2(collection, token) => |u| Some(u) + ); pass_method!(const_metadata(collection: CollectionId, token: TokenId) -> Vec); pass_method!(variable_metadata(collection: CollectionId, token: TokenId) -> Vec); pass_method!(collection_tokens(collection: CollectionId) -> u32); --- /dev/null +++ b/doc/hackusama_update.md @@ -0,0 +1,15 @@ +## Hackusama Update + +During the Kusama Hackaphon the following changes were made: + +- Enabled Smart Contracts Pallet +- Enabled integration between Smart Contracts and Unique Pallet (required special edition of RC4 Substrate version) +- Fixed misc. bugs in Unique Pallet +- Deployed Unique TestNet. Public node available at wss://unique.usetech.com, custom UI types - see below in this README. +- New Features: + - Re-Fungible Token Mode + - Off-Chain Schema to store token image URLs + - Alternative economic model + - Allow Lists and Public Mint Permission +- Use example: [SubstraPunks Game](https://github.com/usetech-llc/substrapunks), fully hosted on IPFS and Unique Testnet + Blockchain. \ No newline at end of file --- /dev/null +++ b/doc/re-enable_ink_contracts.md @@ -0,0 +1,81 @@ +## Re-Enabling Ink! Contracts + +Uncomment following lies: +1. In node/rpc/Cargo.toml +``` +# pallet-contracts-rpc = { version = "3.0", git = 'https://github.com/paritytech/substrate.git', branch = 'polkadot-v0.9.9' } +``` + +2. In node/rpc/src/lib.rs +``` +// C::Api: pallet_contracts_rpc::ContractsRuntimeApi, +... +// use pallet_contracts_rpc::{Contracts, ContractsApi}; +... +// io.extend_with(ContractsApi::to_delegate(Contracts::new(client.clone()))); + +``` + +3. In runtime/Cargo.toml +``` + # 'pallet-contracts/std', + # 'pallet-contracts-primitives/std', + # 'pallet-contracts-rpc-runtime-api/std', + # 'pallet-contract-helpers/std', +... + # [dependencies.pallet-contracts] + # git = 'https://github.com/paritytech/substrate.git' + # default-features = false + # branch = 'polkadot-v0.9.9' + # version = '3.0.0' + + # [dependencies.pallet-contracts-primitives] + # git = 'https://github.com/paritytech/substrate.git' + # default-features = false + # branch = 'polkadot-v0.9.9' + # version = '3.0.0' + + # [dependencies.pallet-contracts-rpc-runtime-api] + # git = 'https://github.com/paritytech/substrate.git' + # default-features = false + # branch = 'polkadot-v0.9.9' + # version = '3.0.0' +... + # pallet-contract-helpers = { path = '../pallets/contract-helpers', default-features = false, version = '0.1.0' } +``` + +4. runtime/src/lib.rs +``` +// use pallet_contracts::weights::WeightInfo; +... +// pub use pallet_timestamp::Call as TimestampCall; +... +// mod chain_extension; +// use crate::chain_extension::{NFTExtension, Imbalance}; +... +/* +parameter_types! { + pub TombstoneDeposit: Balance = deposit( + ... +} +*/ +... +//pallet_contract_helpers::ContractSponsorshipHandler, +... +// impl pallet_contract_helpers::Config for Runtime {} +... +// Contracts: pallet_contracts::{Pallet, Call, Storage, Event}, +... +// ContractHelpers: pallet_contract_helpers::{Pallet, Call, Storage}, +... +//pallet_contract_helpers::ContractHelpersExtension, +... +/* + impl pallet_contracts_rpc_runtime_api::ContractsApi + for Runtime + { + ... + } +*/ + +``` \ No newline at end of file --- /dev/null +++ b/launch-config-full.json @@ -0,0 +1,135 @@ +{ + "relaychain": { + "bin": "../polkadot/target/release/polkadot", + "chain": "rococo-local", + "nodes": [ + { + "name": "alice", + "wsPort": 9844, + "rpcPort": 9843, + "port": 30444, + "flags": [ + "-lparachain::candidate_validation=debug", + "-lxcm=trace" + ] + }, + { + "name": "bob", + "wsPort": 9855, + "rpcPort": 9854, + "port": 30555, + "flags": [ + "-lparachain::candidate_validation=debug", + "-lxcm=trace" + ] + }, + { + "name": "charlie", + "wsPort": 9866, + "rpcPort": 9865, + "port": 30666, + "flags": [ + "-lparachain::candidate_validation=debug" + ] + }, + { + "name": "dave", + "wsPort": 9877, + "rpcPort": 9876, + "port": 30777, + "flags": [ + "-lparachain::candidate_validation=debug" + ] + }, + { + "name": "eve", + "wsPort": 9888, + "rpcPort": 9887, + "port": 30888, + "flags": [ + "-lparachain::candidate_validation=debug" + ] + } + ], + "genesis": { + "runtime": { + "runtime_genesis_config": { + "parachainsConfiguration": { + "config": { + "validation_upgrade_frequency": 1, + "validation_upgrade_delay": 1 + } + } + } + } + } + }, + "parachains": [ + { + "bin": "../unique-chain/target/release/unique-collator", + "id": "1000", + "balance": "1000000000000000000000000", + "nodes": [ + { + "port": 31200, + "wsPort": 9944, + "rpcPort": 9933, + "name": "alice", + "flags": [ + "--rpc-cors=all", + "--unsafe-rpc-external", + "--unsafe-ws-external", + "-lxcm=trace" + ] + }, + { + "port": 31201, + "wsPort": 9945, + "rpcPort": 9934, + "name": "bob", + "flags": [ + "--rpc-cors=all", + "--unsafe-rpc-external", + "--unsafe-ws-external", + "-lxcm=trace" + ] + } + ] + }, + { + "bin": "../Acala/target/release/acala", + "id": "2000", + "chain": "karura-dev", + "balance": "1000000000000000000000", + "nodes": [ + { + "wsPort": 9946, + "port": 31202, + "name": "alice", + "flags": [ + "--rpc-cors=all", + "--unsafe-rpc-external", + "--unsafe-ws-external", + "-lxcm=trace" + ] + } + ] + } + ], + "simpleParachains": [], + "hrmpChannels": [ + { + "sender": 2000, + "recipient": 1000, + "maxCapacity": 8, + "maxMessageSize": 512 + }, + { + "sender": 1000, + "recipient": 2000, + "maxCapacity": 8, + "maxMessageSize": 512 + } + ], + "finalization": false +} --- a/launch-config-westend.json +++ /dev/null @@ -1,141 +0,0 @@ -{ - "relaychain": { - "bin": "../polkadot/target/release/polkadot", - "chain": "westend-local", - "nodes": [ - { - "name": "alice", - "wsPort": 9844, - "rpcPort": 9843, - "port": 30444, - "flags": [ - "-lparachain::candidate_validation=debug" - ] - }, - { - "name": "bob", - "wsPort": 9855, - "rpcPort": 9854, - "port": 30555, - "flags": [ - "-lparachain::candidate_validation=debug" - ] - }, - { - "name": "charlie", - "wsPort": 9866, - "rpcPort": 9865, - "port": 30666, - "flags": [ - "-lparachain::candidate_validation=debug" - ] - }, - { - "name": "dave", - "wsPort": 9877, - "rpcPort": 9876, - "port": 30777, - "flags": [ - "-lparachain::candidate_validation=debug" - ] - }, - { - "name": "eve", - "wsPort": 9888, - "rpcPort": 9886, - "port": 30888, - "flags": [ - "-lparachain::candidate_validation=debug" - ] - }, - { - "name": "ferdie", - "wsPort": 9897, - "rpcPort": 9896, - "port": 30999, - "flags": [ - "-lparachain::candidate_validation=debug" - ] - } - ], - "genesis": { - "runtime": { - "runtime_genesis_config": { - "parachainsConfiguration": { - "config": { - "validation_upgrade_frequency": 1, - "validation_upgrade_delay": 1 - } - } - } - } - } - }, - "parachains": [ - { - "bin": "../unique-chain/target/release/unique-collator", - "chain": "westend-local", - "balance": "1000000000000000000000000", - "nodes": [ - { - "port": 31200, - "wsPort": 9944, - "rpcPort": 9933, - "name": "alice", - "flags": [ - "--rpc-cors=all", - "--unsafe-rpc-external", - "--unsafe-ws-external" - ] - }, - { - "port": 31201, - "wsPort": 9945, - "rpcPort": 9934, - "name": "bob", - "flags": [ - "--rpc-cors=all", - "--unsafe-rpc-external", - "--unsafe-ws-external" - ] - }, - { - "port": 31202, - "wsPort": 9946, - "rpcPort": 9935, - "name": "charlie", - "flags": [ - "--rpc-cors=all", - "--unsafe-rpc-external", - "--unsafe-ws-external" - ] - }, - { - "port": 31203, - "wsPort": 9947, - "rpcPort": 9936, - "name": "dave", - "flags": [ - "--rpc-cors=all", - "--unsafe-rpc-external", - "--unsafe-ws-external" - ] - }, - { - "port": 31204, - "wsPort": 9948, - "rpcPort": 9937, - "name": "eve", - "flags": [ - "--rpc-cors=all", - "--unsafe-rpc-external", - "--unsafe-ws-external" - ] - } - ] - } - ], - "simpleParachains": [], - "hrmpChannels": [], - "finalization": false -} --- a/launch-config.json +++ b/launch-config.json @@ -9,7 +9,8 @@ "rpcPort": 9843, "port": 30444, "flags": [ - "-lparachain::candidate_validation=debug" + "-lparachain::candidate_validation=debug", + "-lxcm=trace" ] }, { @@ -18,7 +19,8 @@ "rpcPort": 9854, "port": 30555, "flags": [ - "-lparachain::candidate_validation=debug" + "-lparachain::candidate_validation=debug", + "-lxcm=trace" ] }, { @@ -38,6 +40,15 @@ "flags": [ "-lparachain::candidate_validation=debug" ] + }, + { + "name": "eve", + "wsPort": 9888, + "rpcPort": 9887, + "port": 30888, + "flags": [ + "-lparachain::candidate_validation=debug" + ] } ], "genesis": { @@ -67,7 +78,8 @@ "flags": [ "--rpc-cors=all", "--unsafe-rpc-external", - "--unsafe-ws-external" + "--unsafe-ws-external", + "-lxcm=trace" ] }, { @@ -78,7 +90,8 @@ "flags": [ "--rpc-cors=all", "--unsafe-rpc-external", - "--unsafe-ws-external" + "--unsafe-ws-external", + "-lxcm=trace" ] } ] --- a/launch-test-env.sh +++ /dev/null @@ -1,6 +0,0 @@ -cp launch-config.json ../polkadot-launch/launch-config.json - -cd ../polkadot-launch -yarn install -yarn build -yarn start launch-config.json --- /dev/null +++ b/launch-testnet-full.sh @@ -0,0 +1,6 @@ +cp launch-config-full.json ../polkadot-launch/launch-config-full.json + +cd ../polkadot-launch +yarn install +yarn build +yarn start launch-config-full.json --- /dev/null +++ b/launch-testnet.sh @@ -0,0 +1,6 @@ +cp launch-config.json ../polkadot-launch/launch-config.json + +cd ../polkadot-launch +yarn install +yarn build +yarn start launch-config.json --- a/node/cli/Cargo.toml +++ b/node/cli/Cargo.toml @@ -257,7 +257,7 @@ license = 'All Rights Reserved' name = 'unique-node' repository = 'https://github.com/UniqueNetwork/unique-chain' -version = '0.9.14' +version = '0.9.16' [[bin]] name = 'unique-collator' --- a/pallets/common/src/lib.rs +++ b/pallets/common/src/lib.rs @@ -14,7 +14,10 @@ use up_data_structs::{ COLLECTION_NUMBER_LIMIT, Collection, CollectionId, CreateItemData, ExistenceRequirement, MAX_TOKEN_PREFIX_LENGTH, COLLECTION_ADMINS_LIMIT, MetaUpdatePermission, Pays, PostDispatchInfo, - TokenId, Weight, WithdrawReasons, CollectionStats, CustomDataLimit, + TokenId, Weight, WithdrawReasons, CollectionStats, MAX_TOKEN_OWNERSHIP, CollectionMode, + NFT_SPONSOR_TRANSFER_TIMEOUT, FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, + REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, MAX_SPONSOR_TIMEOUT, CUSTOM_DATA_LIMIT, CollectionLimits, + CustomDataLimit, CreateCollectionData, SponsorshipState, }; pub use pallet::*; use sp_core::H160; @@ -285,6 +288,10 @@ TokenVariableDataLimitExceeded, /// Exceeded max admin count CollectionAdminCountExceeded, + /// Collection limit bounds per collection exceeded + CollectionLimitBoundsExceeded, + /// Tried to enable permissions which are only permitted to be disabled + OwnerPermissionsCantBeReverted, /// Collection settings not allowing items transferring TransferNotAllowed, @@ -300,7 +307,7 @@ /// Item balance not enough. TokenValueTooLow, /// Requested value more than approved. - TokenValueNotEnough, + ApprovedValueTooLow, /// Tried to approve more than owned CantApproveMoreThanOwned, @@ -395,7 +402,10 @@ } impl Pallet { - pub fn init_collection(data: Collection) -> Result { + pub fn init_collection( + owner: T::AccountId, + data: CreateCollectionData, + ) -> Result { { ensure!( data.token_prefix.len() <= MAX_TOKEN_PREFIX_LENGTH as usize, @@ -418,6 +428,29 @@ // ========= + let collection = Collection { + owner: owner.clone(), + name: data.name, + mode: data.mode.clone(), + mint_mode: false, + access: data.access.unwrap_or_default(), + description: data.description, + token_prefix: data.token_prefix, + offchain_schema: data.offchain_schema, + schema_version: data.schema_version.unwrap_or_default(), + sponsorship: data + .pending_sponsor + .map(SponsorshipState::Unconfirmed) + .unwrap_or_default(), + variable_on_chain_schema: data.variable_on_chain_schema, + const_on_chain_schema: data.const_on_chain_schema, + limits: data + .limits + .map(|limits| Self::clamp_limits(data.mode.clone(), &Default::default(), limits)) + .unwrap_or_else(|| Ok(CollectionLimits::default()))?, + meta_update_permission: data.meta_update_permission.unwrap_or_default(), + }; + // Take a (non-refundable) deposit of collection creation { let mut imbalance = @@ -429,7 +462,7 @@ ), ); ::Currency::settle( - &data.owner, + &owner, imbalance, WithdrawReasons::TRANSFER, ExistenceRequirement::KeepAlive, @@ -438,12 +471,8 @@ } >::put(created_count); - >::deposit_event(Event::CollectionCreated( - id, - data.mode.id(), - data.owner.clone(), - )); - >::insert(id, data); + >::deposit_event(Event::CollectionCreated(id, data.mode.id(), owner.clone())); + >::insert(id, collection); Ok(id) } @@ -527,6 +556,61 @@ Ok(()) } + + pub fn clamp_limits( + mode: CollectionMode, + old_limit: &CollectionLimits, + mut new_limit: CollectionLimits, + ) -> Result { + macro_rules! limit_default { + ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{ + $( + if let Some($new) = $new.$field { + let $old = $old.$field($($arg)?); + let _ = $new; + let _ = $old; + $check + } else { + $new.$field = $old.$field + } + )* + }}; + } + + limit_default!(old_limit, new_limit, + account_token_ownership_limit => ensure!( + new_limit <= MAX_TOKEN_OWNERSHIP, + >::CollectionLimitBoundsExceeded, + ), + sponsor_transfer_timeout(match mode { + CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT, + CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, + CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, + }) => ensure!( + new_limit <= MAX_SPONSOR_TIMEOUT, + >::CollectionLimitBoundsExceeded, + ), + sponsored_data_size => ensure!( + new_limit <= CUSTOM_DATA_LIMIT, + >::CollectionLimitBoundsExceeded, + ), + token_limit => ensure!( + old_limit >= new_limit && new_limit > 0, + >::CollectionTokenLimitExceeded + ), + owner_can_transfer => ensure!( + old_limit || !new_limit, + >::OwnerPermissionsCantBeReverted, + ), + owner_can_destroy => ensure!( + old_limit || !new_limit, + >::OwnerPermissionsCantBeReverted, + ), + sponsored_data_rate_limit => {}, + transfers_enabled => {}, + ); + Ok(new_limit) + } } #[macro_export] --- a/pallets/fungible/src/lib.rs +++ b/pallets/fungible/src/lib.rs @@ -2,7 +2,7 @@ use core::ops::Deref; use frame_support::{ensure}; -use up_data_structs::{AccessMode, Collection, CollectionId, TokenId}; +use up_data_structs::{AccessMode, CollectionId, TokenId, CreateCollectionData}; use pallet_common::{ Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, account::CrossAccountId, }; @@ -100,8 +100,11 @@ } impl Pallet { - pub fn init_collection(data: Collection) -> Result { - >::init_collection(data) + pub fn init_collection( + owner: T::AccountId, + data: CreateCollectionData, + ) -> Result { + >::init_collection(owner, data) } pub fn destroy_collection( collection: FungibleHandle, @@ -343,7 +346,7 @@ if allowance.is_none() { ensure!( collection.ignores_allowance(spender), - >::TokenValueNotEnough + >::ApprovedValueTooLow ); } @@ -374,7 +377,7 @@ if allowance.is_none() { ensure!( collection.ignores_allowance(spender), - >::TokenValueNotEnough + >::ApprovedValueTooLow ); } --- a/pallets/nonfungible/src/lib.rs +++ b/pallets/nonfungible/src/lib.rs @@ -2,7 +2,7 @@ use erc::ERC721Events; use frame_support::{BoundedVec, ensure}; -use up_data_structs::{AccessMode, Collection, CollectionId, CustomDataLimit, TokenId}; +use up_data_structs::{AccessMode, CollectionId, CustomDataLimit, TokenId, CreateCollectionData}; use pallet_common::{ Error as CommonError, Pallet as PalletCommon, Event as CommonEvent, account::CrossAccountId, }; @@ -140,8 +140,11 @@ // unchecked calls skips any permission checks impl Pallet { - pub fn init_collection(data: Collection) -> Result { - >::init_collection(data) + pub fn init_collection( + owner: T::AccountId, + data: CreateCollectionData, + ) -> Result { + >::init_collection(owner, data) } pub fn destroy_collection( collection: NonfungibleHandle, @@ -492,7 +495,7 @@ if >::get((collection.id, token)).as_ref() != Some(spender) { ensure!( collection.ignores_allowance(spender), - >::TokenValueNotEnough + >::ApprovedValueTooLow ); } @@ -520,7 +523,7 @@ if >::get((collection.id, token)).as_ref() != Some(spender) { ensure!( collection.ignores_allowance(spender), - >::TokenValueNotEnough + >::ApprovedValueTooLow ); } --- a/pallets/refungible/src/lib.rs +++ b/pallets/refungible/src/lib.rs @@ -2,7 +2,7 @@ use frame_support::{ensure, BoundedVec}; use up_data_structs::{ - AccessMode, Collection, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId, + AccessMode, CollectionId, CustomDataLimit, MAX_REFUNGIBLE_PIECES, TokenId, CreateCollectionData, }; use pallet_common::{ Error as CommonError, Event as CommonEvent, Pallet as PalletCommon, account::CrossAccountId, @@ -155,8 +155,11 @@ // unchecked calls skips any permission checks impl Pallet { - pub fn init_collection(data: Collection) -> Result { - >::init_collection(data) + pub fn init_collection( + owner: T::AccountId, + data: CreateCollectionData, + ) -> Result { + >::init_collection(owner, data) } pub fn destroy_collection( collection: RefungibleHandle, @@ -528,7 +531,7 @@ if allowance.is_none() { ensure!( collection.ignores_allowance(spender), - >::TokenValueNotEnough + >::ApprovedValueTooLow ); } @@ -561,7 +564,7 @@ if allowance.is_none() { ensure!( collection.ignores_allowance(spender), - >::TokenValueNotEnough + >::ApprovedValueTooLow ); } --- a/pallets/unique/src/lib.rs +++ b/pallets/unique/src/lib.rs @@ -36,13 +36,11 @@ use frame_system::{self as system, ensure_signed}; use sp_runtime::{sp_std::prelude::Vec}; use up_data_structs::{ - MAX_DECIMAL_POINTS, MAX_SPONSOR_TIMEOUT, MAX_TOKEN_OWNERSHIP, CUSTOM_DATA_LIMIT, - VARIABLE_ON_CHAIN_SCHEMA_LIMIT, CONST_ON_CHAIN_SCHEMA_LIMIT, OFFCHAIN_SCHEMA_LIMIT, - FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, - NFT_SPONSOR_TRANSFER_TIMEOUT, MAX_COLLECTION_NAME_LENGTH, MAX_COLLECTION_DESCRIPTION_LENGTH, - MAX_TOKEN_PREFIX_LENGTH, AccessMode, Collection, CreateItemData, CollectionLimits, - CollectionId, CollectionMode, TokenId, SchemaVersion, SponsorshipState, MetaUpdatePermission, - CustomDataLimit, + MAX_DECIMAL_POINTS, VARIABLE_ON_CHAIN_SCHEMA_LIMIT, CONST_ON_CHAIN_SCHEMA_LIMIT, + OFFCHAIN_SCHEMA_LIMIT, MAX_COLLECTION_NAME_LENGTH, MAX_COLLECTION_DESCRIPTION_LENGTH, + MAX_TOKEN_PREFIX_LENGTH, AccessMode, CreateItemData, CollectionLimits, CollectionId, + CollectionMode, TokenId, SchemaVersion, SponsorshipState, MetaUpdatePermission, + CreateCollectionData, CustomDataLimit, }; use pallet_common::{ account::CrossAccountId, CollectionHandle, Pallet as PalletCommon, Error as CommonError, @@ -84,10 +82,6 @@ ConfirmUnsetSponsorFail, /// Length of items properties must be greater than 0. EmptyArgument, - /// Collection limit bounds per collection exceeded - CollectionLimitBoundsExceeded, - /// Tried to enable permissions which are only permitted to be disabled - OwnerPermissionsCantBeReverted, } } @@ -321,42 +315,39 @@ // returns collection ID #[weight = >::create_collection()] #[transactional] + #[deprecated] pub fn create_collection(origin, collection_name: BoundedVec>, collection_description: BoundedVec>, token_prefix: BoundedVec>, - mode: CollectionMode) -> DispatchResult { - - // Anyone can create a collection - let who = ensure_signed(origin)?; - - // Create new collection - let new_collection = Collection { - owner: who, + mode: CollectionMode) -> DispatchResult { + let data: CreateCollectionData = CreateCollectionData { name: collection_name, - mode: mode.clone(), - mint_mode: false, - access: AccessMode::Normal, description: collection_description, token_prefix, - offchain_schema: BoundedVec::default(), - schema_version: SchemaVersion::ImageURL, - sponsorship: SponsorshipState::Disabled, - variable_on_chain_schema: BoundedVec::default(), - const_on_chain_schema: BoundedVec::default(), - limits: Default::default(), - meta_update_permission: Default::default(), + mode, + ..Default::default() }; + Self::create_collection_ex(origin, data) + } - let _id = match mode { - CollectionMode::NFT => {>::init_collection(new_collection)?}, + /// This method creates a collection + /// + /// Prefer it to deprecated [`created_collection`] method + #[weight = >::create_collection()] + #[transactional] + pub fn create_collection_ex(origin, data: CreateCollectionData) -> DispatchResult { + let owner = ensure_signed(origin)?; + + let _id = match data.mode { + CollectionMode::NFT => {>::init_collection(owner, data)?}, CollectionMode::Fungible(decimal_points) => { // check params ensure!(decimal_points <= MAX_DECIMAL_POINTS, Error::::CollectionDecimalPointLimitExceeded); - >::init_collection(new_collection)? + >::init_collection(owner, data)? } CollectionMode::ReFungible => { - >::init_collection(new_collection)? + >::init_collection(owner, data)? } }; @@ -1093,61 +1084,12 @@ collection_id: CollectionId, new_limit: CollectionLimits, ) -> DispatchResult { - let mut new_limit = new_limit; let sender = T::CrossAccountId::from_sub(ensure_signed(origin)?); let mut target_collection = >::try_get(collection_id)?; target_collection.check_is_owner(&sender)?; let old_limit = &target_collection.limits; - macro_rules! limit_default { - ($old:ident, $new:ident, $($field:ident $(($arg:expr))? => $check:expr),* $(,)?) => {{ - $( - if let Some($new) = $new.$field { - let $old = $old.$field($($arg)?); - let _ = $new; - let _ = $old; - $check - } else { - $new.$field = $old.$field - } - )* - }}; - } - - limit_default!(old_limit, new_limit, - account_token_ownership_limit => ensure!( - new_limit <= MAX_TOKEN_OWNERSHIP, - >::CollectionLimitBoundsExceeded, - ), - sponsor_transfer_timeout(match target_collection.mode { - CollectionMode::NFT => NFT_SPONSOR_TRANSFER_TIMEOUT, - CollectionMode::Fungible(_) => FUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, - CollectionMode::ReFungible => REFUNGIBLE_SPONSOR_TRANSFER_TIMEOUT, - }) => ensure!( - new_limit <= MAX_SPONSOR_TIMEOUT, - >::CollectionLimitBoundsExceeded, - ), - sponsored_data_size => ensure!( - new_limit <= CUSTOM_DATA_LIMIT, - >::CollectionLimitBoundsExceeded, - ), - token_limit => ensure!( - old_limit >= new_limit && new_limit > 0, - >::CollectionTokenLimitExceeded - ), - owner_can_transfer => ensure!( - old_limit || !new_limit, - >::OwnerPermissionsCantBeReverted, - ), - owner_can_destroy => ensure!( - old_limit || !new_limit, - >::OwnerPermissionsCantBeReverted, - ), - sponsored_data_rate_limit => {}, - transfers_enabled => {}, - ); - - target_collection.limits = new_limit; + target_collection.limits = >::clamp_limits(target_collection.mode.clone(), &old_limit, new_limit)?; >::deposit_event(Event::::CollectionLimitSet( collection_id --- a/pallets/unique/src/tests.rs +++ b/pallets/unique/src/tests.rs @@ -5,7 +5,7 @@ use up_data_structs::{ COLLECTION_NUMBER_LIMIT, CollectionId, CreateItemData, CreateFungibleData, CreateNftData, CreateReFungibleData, MAX_DECIMAL_POINTS, COLLECTION_ADMINS_LIMIT, MetaUpdatePermission, - TokenId, + TokenId, MAX_TOKEN_OWNERSHIP, }; use frame_support::{assert_noop, assert_ok}; use sp_std::convert::TryInto; @@ -601,7 +601,7 @@ 1 ) .map_err(|e| e.error), - CommonError::::TokenValueNotEnough + CommonError::::ApprovedValueTooLow ); // do approve @@ -916,7 +916,7 @@ 4 ) .map_err(|e| e.error), - CommonError::::TokenValueNotEnough + CommonError::::ApprovedValueTooLow ); }); } --- a/primitives/data-structs/src/lib.rs +++ b/primitives/data-structs/src/lib.rs @@ -263,6 +263,31 @@ pub meta_update_permission: MetaUpdatePermission, } +#[derive(Encode, Decode, Clone, PartialEq, TypeInfo, Debug, Derivative, MaxEncodedLen)] +#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] +#[derivative(Default(bound = ""))] +pub struct CreateCollectionData { + #[derivative(Default(value = "CollectionMode::NFT"))] + pub mode: CollectionMode, + pub access: Option, + #[cfg_attr(feature = "serde1", serde(with = "bounded_serde"))] + pub name: BoundedVec>, + #[cfg_attr(feature = "serde1", serde(with = "bounded_serde"))] + pub description: BoundedVec>, + #[cfg_attr(feature = "serde1", serde(with = "bounded_serde"))] + pub token_prefix: BoundedVec>, + #[cfg_attr(feature = "serde1", serde(with = "bounded_serde"))] + pub offchain_schema: BoundedVec>, + pub schema_version: Option, + pub pending_sponsor: Option, + pub limits: Option, + #[cfg_attr(feature = "serde1", serde(with = "bounded_serde"))] + pub variable_on_chain_schema: BoundedVec>, + #[cfg_attr(feature = "serde1", serde(with = "bounded_serde"))] + pub const_on_chain_schema: BoundedVec>, + pub meta_update_permission: Option, +} + #[derive(Encode, Decode, Debug, Clone, PartialEq, TypeInfo)] #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))] pub struct NftItemType { --- a/primitives/rpc/src/lib.rs +++ b/primitives/rpc/src/lib.rs @@ -9,10 +9,14 @@ type Result = core::result::Result; sp_api::decl_runtime_apis! { + #[api_version(2)] pub trait UniqueApi where AccountId: Decode, CrossAccountId: pallet_common::account::CrossAccountId, { + #[changed_in(2)] + fn token_owner(collection: CollectionId, token: TokenId) -> Result; + fn account_tokens(collection: CollectionId, account: CrossAccountId) -> Result>; fn token_exists(collection: CollectionId, token: TokenId) -> Result; --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -23,7 +23,8 @@ use sp_runtime::{ Permill, Perbill, Percent, create_runtime_str, generic, impl_opaque_keys, traits::{ - AccountIdLookup, BlakeTwo256, Block as BlockT, IdentifyAccount, Verify, AccountIdConversion, + AccountIdLookup, BlakeTwo256, Block as BlockT, IdentifyAccount, Verify, + AccountIdConversion, Zero, }, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, MultiSignature, RuntimeAppPublic, @@ -45,8 +46,9 @@ dispatch::DispatchResult, PalletId, parameter_types, StorageValue, ConsensusEngineId, traits::{ - Everything, Currency, ExistenceRequirement, Get, IsInVec, KeyOwnerProofSystem, - LockIdentifier, OnUnbalanced, Randomness, FindAuthor, + tokens::currency::Currency as CurrencyT, OnUnbalanced as OnUnbalancedT, Everything, + Currency, ExistenceRequirement, Get, IsInVec, KeyOwnerProofSystem, LockIdentifier, + OnUnbalanced, Randomness, FindAuthor, }, weights::{ constants::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight, WEIGHT_PER_SECOND}, @@ -69,8 +71,9 @@ use pallet_evm::{Account as EVMAccount, FeeCalculator, GasWeightMapping, OnMethodCall}; use fp_rpc::TransactionStatus; use sp_runtime::{ - traits::{BlockNumberProvider, Dispatchable, PostDispatchInfoOf}, + traits::{BlockNumberProvider, Dispatchable, PostDispatchInfoOf, Saturating}, transaction_validity::TransactionValidityError, + SaturatedConversion, }; // pub use pallet_timestamp::Call as TimestampCall; @@ -82,13 +85,24 @@ use xcm::v1::{BodyId, Junction::*, MultiLocation, NetworkId, Junctions::*}; use xcm_builder::{ AccountId32Aliases, AllowTopLevelPaidExecutionFrom, AllowUnpaidExecutionFrom, CurrencyAdapter, - EnsureXcmOrigin, FixedWeightBounds, IsConcrete, LocationInverter, NativeAsset, - ParentAsSuperuser, ParentIsDefault, RelayChainAsNative, SiblingParachainAsNative, - SiblingParachainConvertsVia, SignedAccountId32AsNative, SignedToAccountId32, - SovereignSignedViaLocation, TakeWeightCredit, UsingComponents, + EnsureXcmOrigin, FixedWeightBounds, LocationInverter, NativeAsset, ParentAsSuperuser, + ParentIsDefault, RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, + SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, }; -use xcm_executor::{Config, XcmExecutor}; +use xcm_executor::{Config, XcmExecutor, Assets}; +use sp_std::{marker::PhantomData}; +use xcm::latest::{ + // Xcm, + AssetId::{Concrete}, + Fungibility::Fungible as XcmFungible, + MultiAsset, + Error as XcmError, +}; +use xcm_executor::traits::{MatchesFungible, WeightTrader}; +//use xcm_executor::traits::MatchesFungible; +use sp_runtime::traits::CheckedConversion; + // mod chain_extension; // use crate::chain_extension::{NFTExtension, Imbalance}; @@ -146,10 +160,10 @@ spec_name: create_runtime_str!("opal"), impl_name: create_runtime_str!("opal"), authoring_version: 1, - spec_version: 914000, - impl_version: 1, + spec_version: 916001, + impl_version: 0, apis: RUNTIME_API_VERSIONS, - transaction_version: 0, + transaction_version: 1, state_version: 0, }; @@ -616,12 +630,22 @@ AccountId32Aliases, ); +pub struct OnlySelfCurrency; +impl> MatchesFungible for OnlySelfCurrency { + fn matches_fungible(a: &MultiAsset) -> Option { + match (&a.id, &a.fun) { + (Concrete(_), XcmFungible(ref amount)) => CheckedConversion::checked_from(*amount), + _ => None, + } + } +} + /// Means for transacting assets on this chain. pub type LocalAssetTransactor = CurrencyAdapter< // Use this currency: Balances, // Use this currency when it is a fungible asset matching the given location or name: - IsConcrete, + OnlySelfCurrency, // Do a simple punn to convert an AccountId32 MultiLocation into a native chain account ID: LocationToAccountId, // Our chain's account ID type (we can't get away without mentioning it explicitly): @@ -677,6 +701,88 @@ // ^^^ Parent & its unit plurality gets free execution ); +pub struct UsingOnlySelfCurrencyComponents< + WeightToFee: WeightToFeePolynomial, + AssetId: Get, + AccountId, + Currency: CurrencyT, + OnUnbalanced: OnUnbalancedT, +>( + Weight, + Currency::Balance, + PhantomData<(WeightToFee, AssetId, AccountId, Currency, OnUnbalanced)>, +); +impl< + WeightToFee: WeightToFeePolynomial, + AssetId: Get, + AccountId, + Currency: CurrencyT, + OnUnbalanced: OnUnbalancedT, + > WeightTrader + for UsingOnlySelfCurrencyComponents +{ + fn new() -> Self { + Self(0, Zero::zero(), PhantomData) + } + + fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result { + let amount = WeightToFee::calc(&weight); + let u128_amount: u128 = amount.try_into().map_err(|_| XcmError::Overflow)?; + + // location to this parachain through relay chain + let option1: xcm::v1::AssetId = Concrete(MultiLocation { + parents: 1, + interior: X1(Parachain(ParachainInfo::parachain_id().into())), + }); + // direct location + let option2: xcm::v1::AssetId = Concrete(MultiLocation { + parents: 0, + interior: Here, + }); + + let required = if payment.fungible.contains_key(&option1) { + (option1, u128_amount).into() + } else if payment.fungible.contains_key(&option2) { + (option2, u128_amount).into() + } else { + (Concrete(MultiLocation::default()), u128_amount).into() + }; + + let unused = payment + .checked_sub(required) + .map_err(|_| XcmError::TooExpensive)?; + self.0 = self.0.saturating_add(weight); + self.1 = self.1.saturating_add(amount); + Ok(unused) + } + + fn refund_weight(&mut self, weight: Weight) -> Option { + let weight = weight.min(self.0); + let amount = WeightToFee::calc(&weight); + self.0 -= weight; + self.1 = self.1.saturating_sub(amount); + let amount: u128 = amount.saturated_into(); + if amount > 0 { + Some((AssetId::get(), amount).into()) + } else { + None + } + } +} +impl< + WeightToFee: WeightToFeePolynomial, + AssetId: Get, + AccountId, + Currency: CurrencyT, + OnUnbalanced: OnUnbalancedT, + > Drop + for UsingOnlySelfCurrencyComponents +{ + fn drop(&mut self) { + OnUnbalanced::on_unbalanced(Currency::issue(self.1)); + } +} + pub struct XcmConfig; impl Config for XcmConfig { type Call = Call; @@ -689,7 +795,13 @@ type LocationInverter = LocationInverter; type Barrier = Barrier; type Weigher = FixedWeightBounds; - type Trader = UsingComponents, RelayLocation, AccountId, Balances, ()>; + type Trader = UsingOnlySelfCurrencyComponents< + IdentityFee, + RelayLocation, + AccountId, + Balances, + (), + >; type ResponseHandler = (); // Don't handle responses for now. type SubscriptionService = PolkadotXcm; --- a/tests/package.json +++ b/tests/package.json @@ -64,12 +64,14 @@ "testOverflow": "mocha --timeout 9999999 -r ts-node/register ./**/overflow.test.ts", "testSetVariableMetadataSponsoringRateLimit": "mocha --timeout 9999999 -r ts-node/register ./**/setVariableMetadataSponsoringRateLimit.test.ts", "testInflation": "mocha --timeout 9999999 -r ts-node/register ./**/inflation.test.ts", + "testXcmTransfer": "mocha --timeout 9999999 -r ts-node/register ./**/xcmTransfer.test.ts", "testPalletPresence": "mocha --timeout 9999999 -r ts-node/register ./**/pallet-presence.test.ts", "testBlockProduction": "mocha --timeout 9999999 -r ts-node/register ./**/block-production.test.ts", "testEnableDisableTransfers": "mocha --timeout 9999999 -r ts-node/register ./**/enableDisableTransfer.test.ts", + "polkadot-types-fetch-metadata": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' http://localhost:9933 > src/interfaces/metadata.json", "polkadot-types-from-defs": "ts-node ./node_modules/.bin/polkadot-types-from-defs --input src/interfaces/ --package .", - "polkadot-types-from-chain": "ts-node ./node_modules/.bin/polkadot-types-from-chain --endpoint ws://localhost:9944 --output src/interfaces/ --package .", - "polkadot-types": "yarn polkadot-types-from-defs && yarn polkadot-types-from-chain" + "polkadot-types-from-chain": "ts-node ./node_modules/.bin/polkadot-types-from-chain --endpoint src/interfaces/metadata.json --output src/interfaces/ --package .", + "polkadot-types": "yarn polkadot-types-fetch-metadata && yarn polkadot-types-from-defs && yarn polkadot-types-from-chain" }, "author": "", "license": "SEE LICENSE IN ../LICENSE", --- a/tests/src/check-event/createCollectionEvent.test.ts +++ b/tests/src/check-event/createCollectionEvent.test.ts @@ -27,7 +27,7 @@ }); it('Check event from createCollection(): ', async () => { await usingApi(async (api: ApiPromise) => { - const tx = api.tx.unique.createCollection([0x31], [0x32], '0x33', 'NFT'); + const tx = api.tx.unique.createCollectionEx({name: [0x31], description: [0x32], tokenPrefix: '0x33', mode: 'NFT'}); const events = await submitTransactionAsync(alice, tx); const msg = JSON.stringify(uniqueEventMessage(events)); expect(msg).to.be.contain(checkSection); --- a/tests/src/createCollection.test.ts +++ b/tests/src/createCollection.test.ts @@ -3,12 +3,11 @@ // file 'LICENSE', which is part of this source code package. // -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import {createCollectionExpectFailure, createCollectionExpectSuccess} from './util/helpers'; +import {expect} from 'chai'; +import privateKey from './substrate/privateKey'; +import usingApi, {executeTransaction, submitTransactionAsync} from './substrate/substrate-api'; +import {createCollectionExpectFailure, createCollectionExpectSuccess, getCreateCollectionResult, getDetailedCollectionInfo} from './util/helpers'; -chai.use(chaiAsPromised); - describe('integration test: ext. createCollection():', () => { it('Create new NFT collection', async () => { await createCollectionExpectSuccess({name: 'A', description: 'B', tokenPrefix: 'C', mode: {type: 'NFT'}}); @@ -28,6 +27,45 @@ it('Create new ReFungible collection', async () => { await createCollectionExpectSuccess({mode: {type: 'ReFungible'}}); }); + it('Create new collection with extra fields', async () => { + await usingApi(async api => { + const alice = privateKey('//Alice'); + const bob = privateKey('//Bob'); + const tx = api.tx.unique.createCollectionEx({ + mode: {Fungible: 8}, + access: 'AllowList', + name: [1], + description: [2], + tokenPrefix: '0x000000', + offchainSchema: '0x111111', + schemaVersion: 'Unique', + pendingSponsor: bob.address, + limits: { + accountTokenOwnershipLimit: 3, + }, + variableOnChainSchema: '0x222222', + constOnChainSchema: '0x333333', + metaUpdatePermission: 'Admin', + }); + const events = await submitTransactionAsync(alice, tx); + const result = getCreateCollectionResult(events); + + const collection = (await getDetailedCollectionInfo(api, result.collectionId))!; + expect(collection.owner.toString()).to.equal(alice.address); + expect(collection.mode.asFungible.toNumber()).to.equal(8); + expect(collection.access.isAllowList).to.be.true; + expect(collection.name.map(v => v.toNumber())).to.deep.equal([1]); + expect(collection.description.map(v => v.toNumber())).to.deep.equal([2]); + expect(collection.tokenPrefix.toString()).to.equal('0x000000'); + expect(collection.offchainSchema.toString()).to.equal('0x111111'); + expect(collection.schemaVersion.isUnique).to.be.true; + expect(collection.sponsorship.asUnconfirmed.toString()).to.equal(bob.address); + expect(collection.limits.accountTokenOwnershipLimit.unwrap().toNumber()).to.equal(3); + expect(collection.variableOnChainSchema.toString()).to.equal('0x222222'); + expect(collection.constOnChainSchema.toString()).to.equal('0x333333'); + expect(collection.metaUpdatePermission.isAdmin).to.be.true; + }); + }); }); describe('(!negative test!) integration test: ext. createCollection():', () => { @@ -40,4 +78,11 @@ it('(!negative test!) create new NFT collection whith incorrect data (token_prefix)', async () => { await createCollectionExpectFailure({tokenPrefix: 'A'.repeat(17), mode: {type: 'NFT'}}); }); + it('fails when bad limits are set', async () => { + await usingApi(async api => { + const alice = privateKey('//Alice'); + const tx = api.tx.unique.createCollectionEx({mode: 'NFT', limits: {tokenLimit: 0}}); + await expect(executeTransaction(api, alice, tx)).to.be.rejectedWith(/^common.CollectionTokenLimitExceeded$/); + }); + }); }); --- /dev/null +++ b/tests/src/interfaces/.gitignore @@ -0,0 +1 @@ +metadata.json --- a/tests/src/interfaces/augment-api-errors.ts +++ b/tests/src/interfaces/augment-api-errors.ts @@ -69,6 +69,10 @@ **/ CollectionDescriptionLimitExceeded: AugmentedError; /** + * Collection limit bounds per collection exceeded + **/ + CollectionLimitBoundsExceeded: AugmentedError; + /** * Collection name can not be longer than 63 char. **/ CollectionNameLimitExceeded: AugmentedError; @@ -97,6 +101,10 @@ **/ NoPermission: AugmentedError; /** + * Tried to enable permissions which are only permitted to be disabled + **/ + OwnerPermissionsCantBeReverted: AugmentedError; + /** * Collection is not in mint mode. **/ PublicMintingNotAllowed: AugmentedError; @@ -107,7 +115,7 @@ /** * Requested value more than approved. **/ - TokenValueNotEnough: AugmentedError; + ApprovedValueTooLow: AugmentedError; /** * Item balance not enough. **/ @@ -436,10 +444,6 @@ * Decimal_points parameter must be lower than MAX_DECIMAL_POINTS constant, currently it is 30. **/ CollectionDecimalPointLimitExceeded: AugmentedError; - /** - * Collection limit bounds per collection exceeded - **/ - CollectionLimitBoundsExceeded: AugmentedError; /** * This address is not set as sponsor, use setCollectionSponsor first. **/ @@ -448,10 +452,6 @@ * Length of items properties must be greater than 0. **/ EmptyArgument: AugmentedError; - /** - * Tried to enable permissions which are only permitted to be disabled - **/ - OwnerPermissionsCantBeReverted: AugmentedError; /** * Generic error **/ --- a/tests/src/interfaces/augment-api-tx.ts +++ b/tests/src/interfaces/augment-api-tx.ts @@ -2,7 +2,7 @@ /* eslint-disable */ import type { CumulusPrimitivesParachainInherentParachainInherentData } from './polkadot'; -import type { PalletCommonAccountBasicCrossAccountIdRepr, UpDataStructsAccessMode, UpDataStructsCollectionLimits, UpDataStructsCollectionMode, UpDataStructsCreateItemData, UpDataStructsMetaUpdatePermission, UpDataStructsSchemaVersion } from './unique'; +import type { PalletCommonAccountBasicCrossAccountIdRepr, UpDataStructsAccessMode, UpDataStructsCollectionLimits, UpDataStructsCollectionMode, UpDataStructsCreateCollectionData, UpDataStructsCreateItemData, UpDataStructsMetaUpdatePermission, UpDataStructsSchemaVersion } from './unique'; import type { ApiTypes, SubmittableExtrinsic } from '@polkadot/api/types'; import type { Bytes, Compact, Option, U256, Vec, bool, u128, u16, u32, u64 } from '@polkadot/types'; import type { Extrinsic } from '@polkadot/types/interfaces/extrinsics'; @@ -671,6 +671,12 @@ **/ createCollection: AugmentedSubmittable<(collectionName: Vec | (u16 | AnyNumber | Uint8Array)[], collectionDescription: Vec | (u16 | AnyNumber | Uint8Array)[], tokenPrefix: Bytes | string | Uint8Array, mode: UpDataStructsCollectionMode | { NFT: any } | { Fungible: any } | { ReFungible: any } | string | Uint8Array) => SubmittableExtrinsic, [Vec, Vec, Bytes, UpDataStructsCollectionMode]>; /** + * This method creates a collection + * + * Prefer it to deprecated [`created_collection`] method + **/ + createCollectionEx: AugmentedSubmittable<(data: UpDataStructsCreateCollectionData | { mode?: any; access?: any; name?: any; description?: any; tokenPrefix?: any; offchainSchema?: any; schemaVersion?: any; pendingSponsor?: any; limits?: any; variableOnChainSchema?: any; constOnChainSchema?: any; metaUpdatePermission?: any } | string | Uint8Array) => SubmittableExtrinsic, [UpDataStructsCreateCollectionData]>; + /** * This method creates a concrete instance of NFT Collection created with CreateCollection method. * * # Permissions --- a/tests/src/interfaces/augment-types.ts +++ b/tests/src/interfaces/augment-types.ts @@ -3,7 +3,7 @@ import type { EthereumBlock, EthereumLog, EthereumReceipt, EthereumTransactionLegacyTransaction, EvmCoreErrorExitReason, FpRpcTransactionStatus } from './ethereum'; import type { CumulusPalletDmpQueueConfigData, CumulusPalletDmpQueuePageIndexData, CumulusPalletParachainSystemRelayStateSnapshotMessagingStateSnapshot, CumulusPalletXcmpQueueInboundStatus, CumulusPalletXcmpQueueOutboundStatus, CumulusPalletXcmpQueueQueueConfigData, CumulusPrimitivesParachainInherentParachainInherentData, PolkadotParachainPrimitivesXcmpMessageFormat, PolkadotPrimitivesV1AbridgedHostConfiguration, PolkadotPrimitivesV1PersistedValidationData } from './polkadot'; -import type { PalletCommonAccountBasicCrossAccountIdRepr, PalletNonfungibleItemData, PalletRefungibleItemData, PalletUnqSchedulerCallSpec, PalletUnqSchedulerReleases, PalletUnqSchedulerScheduledV2, UpDataStructsAccessMode, UpDataStructsCollection, UpDataStructsCollectionId, UpDataStructsCollectionLimits, UpDataStructsCollectionMode, UpDataStructsCollectionStats, UpDataStructsCreateItemData, UpDataStructsMetaUpdatePermission, UpDataStructsSchemaVersion, UpDataStructsSponsorshipState, UpDataStructsTokenId } from './unique'; +import type { PalletCommonAccountBasicCrossAccountIdRepr, PalletNonfungibleItemData, PalletRefungibleItemData, PalletUnqSchedulerCallSpec, PalletUnqSchedulerReleases, PalletUnqSchedulerScheduledV2, UpDataStructsAccessMode, UpDataStructsCollection, UpDataStructsCollectionId, UpDataStructsCollectionLimits, UpDataStructsCollectionMode, UpDataStructsCollectionStats, UpDataStructsCreateCollectionData, UpDataStructsCreateItemData, UpDataStructsMetaUpdatePermission, UpDataStructsSchemaVersion, UpDataStructsSponsorshipState, UpDataStructsTokenId } from './unique'; import type { BitVec, Bool, Bytes, Data, I128, I16, I256, I32, I64, I8, Json, Null, Raw, StorageKey, Text, Type, U128, U16, U256, U32, U64, U8, USize, bool, i128, i16, i256, i32, i64, i8, u128, u16, u256, u32, u64, u8, usize } from '@polkadot/types'; import type { AssetApproval, AssetApprovalKey, AssetBalance, AssetDestroyWitness, AssetDetails, AssetMetadata, TAssetBalance, TAssetDepositBalance } from '@polkadot/types/interfaces/assets'; import type { BlockAttestations, IncludedBlocks, MoreAttestations } from '@polkadot/types/interfaces/attestations'; @@ -1021,6 +1021,7 @@ UpDataStructsCollectionLimits: UpDataStructsCollectionLimits; UpDataStructsCollectionMode: UpDataStructsCollectionMode; UpDataStructsCollectionStats: UpDataStructsCollectionStats; + UpDataStructsCreateCollectionData: UpDataStructsCreateCollectionData; UpDataStructsCreateItemData: UpDataStructsCreateItemData; UpDataStructsMetaUpdatePermission: UpDataStructsMetaUpdatePermission; UpDataStructsSchemaVersion: UpDataStructsSchemaVersion; --- a/tests/src/interfaces/unique/definitions.ts +++ b/tests/src/interfaces/unique/definitions.ts @@ -67,6 +67,20 @@ constOnChainSchema: 'Vec', metaUpdatePermission: 'UpDataStructsMetaUpdatePermission', }, + UpDataStructsCreateCollectionData: { + mode: 'UpDataStructsCollectionMode', + access: 'Option', + name: 'Vec', + description: 'Vec', + tokenPrefix: 'Vec', + offchainSchema: 'Vec', + schemaVersion: 'Option', + pendingSponsor: 'Option', + limits: 'Option', + variableOnChainSchema: 'Vec', + constOnChainSchema: 'Vec', + metaUpdatePermission: 'Option', + }, UpDataStructsCollectionStats: { created: 'u32', destroyed: 'u32', @@ -76,7 +90,13 @@ UpDataStructsTokenId: 'u32', PalletNonfungibleItemData: mkDummy('NftItemData'), PalletRefungibleItemData: mkDummy('RftItemData'), - UpDataStructsCollectionMode: mkDummy('CollectionMode'), + UpDataStructsCollectionMode: { + _enum: { + NFT: null, + Fungible: 'u32', + ReFungible: null, + }, + }, UpDataStructsCreateItemData: mkDummy('CreateItemData'), UpDataStructsCollectionLimits: { accountTokenOwnershipLimit: 'Option', @@ -101,7 +121,9 @@ UpDataStructsAccessMode: { _enum: ['Normal', 'AllowList'], }, - UpDataStructsSchemaVersion: mkDummy('SchemaVersion'), + UpDataStructsSchemaVersion: { + _enum: ['ImageURL', 'Unique'], + }, PalletUnqSchedulerScheduledV2: mkDummy('ScheduledV2'), PalletUnqSchedulerCallSpec: mkDummy('CallSpec'), --- a/tests/src/interfaces/unique/types.ts +++ b/tests/src/interfaces/unique/types.ts @@ -77,8 +77,11 @@ } /** @name UpDataStructsCollectionMode */ -export interface UpDataStructsCollectionMode extends Struct { - readonly dummyCollectionMode: u32; +export interface UpDataStructsCollectionMode extends Enum { + readonly isNft: boolean; + readonly isFungible: boolean; + readonly asFungible: u32; + readonly isReFungible: boolean; } /** @name UpDataStructsCollectionStats */ @@ -88,6 +91,22 @@ readonly alive: u32; } +/** @name UpDataStructsCreateCollectionData */ +export interface UpDataStructsCreateCollectionData extends Struct { + readonly mode: UpDataStructsCollectionMode; + readonly access: Option; + readonly name: Vec; + readonly description: Vec; + readonly tokenPrefix: Bytes; + readonly offchainSchema: Bytes; + readonly schemaVersion: Option; + readonly pendingSponsor: Option; + readonly limits: Option; + readonly variableOnChainSchema: Bytes; + readonly constOnChainSchema: Bytes; + readonly metaUpdatePermission: Option; +} + /** @name UpDataStructsCreateItemData */ export interface UpDataStructsCreateItemData extends Struct { readonly dummyCreateItemData: u32; @@ -101,8 +120,9 @@ } /** @name UpDataStructsSchemaVersion */ -export interface UpDataStructsSchemaVersion extends Struct { - readonly dummySchemaVersion: u32; +export interface UpDataStructsSchemaVersion extends Enum { + readonly isImageUrl: boolean; + readonly isUnique: boolean; } /** @name UpDataStructsSponsorshipState */ --- a/tests/src/substrate/substrate-api.ts +++ b/tests/src/substrate/substrate-api.ts @@ -95,6 +95,32 @@ return TransactionStatus.Fail; } +export function executeTransaction(api: ApiPromise, sender: IKeyringPair, transaction: SubmittableExtrinsic<'promise'>): Promise { + return new Promise(async (res, rej) => { + try { + await transaction.signAndSend(sender, ({events, status}) => { + if (!status.isInBlock && !status.isFinalized) return; + for (const {event} of events) { + if (api.events.system.ExtrinsicSuccess.is(event)) { + res(events); + } else if (api.events.system.ExtrinsicFailed.is(event)) { + const {data: [error]} = event; + if (error.isModule) { + const decoded = api.registry.findMetaError(error.asModule); + const {method, section} = decoded; + rej(new Error(`${section}.${method}`)); + } else { + rej(new Error(error.toString())); + } + } + } + }); + } catch (e) { + rej(e); + } + }); +} + export function submitTransactionAsync(sender: IKeyringPair, transaction: SubmittableExtrinsic): Promise { /* eslint no-async-promise-executor: "off" */ --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -49,7 +49,7 @@ }; } else if ('Substrate' in input) { return input; - }else if ('substrate' in input) { + } else if ('substrate' in input) { return { Substrate: (input as any).substrate, }; @@ -116,15 +116,15 @@ export interface IChainLimits { collectionNumbersLimit: number; - accountTokenOwnershipLimit: number; - collectionsAdminsLimit: number; - customDataLimit: number; - nftSponsorTransferTimeout: number; - fungibleSponsorTransferTimeout: number; - refungibleSponsorTransferTimeout: number; - offchainSchemaLimit: number; - variableOnChainSchemaLimit: number; - constOnChainSchemaLimit: number; + accountTokenOwnershipLimit: number; + collectionsAdminsLimit: number; + customDataLimit: number; + nftSponsorTransferTimeout: number; + fungibleSponsorTransferTimeout: number; + refungibleSponsorTransferTimeout: number; + offchainSchemaLimit: number; + variableOnChainSchemaLimit: number; + constOnChainSchemaLimit: number; } export interface IReFungibleTokenDataType { @@ -283,7 +283,7 @@ modeprm = {refungible: null}; } - const tx = api.tx.unique.createCollection(strToUTF16(name), strToUTF16(description), strToUTF16(tokenPrefix), modeprm as any); + const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any}); const events = await submitTransactionAsync(alicePrivateKey, tx); const result = getCreateCollectionResult(events); @@ -329,7 +329,7 @@ // Run the CreateCollection transaction const alicePrivateKey = privateKey('//Alice'); - const tx = api.tx.unique.createCollection(strToUTF16(name), strToUTF16(description), strToUTF16(tokenPrefix), modeprm as any); + const tx = api.tx.unique.createCollectionEx({name: strToUTF16(name), description: strToUTF16(description), tokenPrefix: strToUTF16(tokenPrefix), mode: modeprm as any}); const events = await expect(submitTransactionExpectFailAsync(alicePrivateKey, tx)).to.be.rejected; const result = getCreateCollectionResult(events); @@ -557,7 +557,7 @@ await usingApi(async (api) => { - const tx = api.tx.unique.setTransfersEnabledFlag (collectionId, enabled); + const tx = api.tx.unique.setTransfersEnabledFlag(collectionId, enabled); const events = await submitTransactionAsync(sender, tx); const result = getGenericResult(events); @@ -569,7 +569,7 @@ await usingApi(async (api) => { - const tx = api.tx.unique.setTransfersEnabledFlag (collectionId, enabled); + const tx = api.tx.unique.setTransfersEnabledFlag(collectionId, enabled); const events = await expect(submitTransactionExpectFailAsync(sender, tx)).to.be.rejected; const result = getGenericResult(events); @@ -811,8 +811,7 @@ } export async function -getFreeBalance(account: IKeyringPair) : Promise -{ +getFreeBalance(account: IKeyringPair): Promise { let balance = 0n; await usingApi(async (api) => { balance = BigInt((await api.query.system.account(account.address)).data.free.toString()); --- /dev/null +++ b/tests/src/xcmTransfer.test.ts @@ -0,0 +1,178 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import {WsProvider} from '@polkadot/api'; +import {ApiOptions} from '@polkadot/api/types'; +import {IKeyringPair} from '@polkadot/types/types'; +import privateKey from './substrate/privateKey'; +import usingApi, {submitTransactionAsync} from './substrate/substrate-api'; +import {getGenericResult} from './util/helpers'; +import waitNewBlocks from './substrate/wait-new-blocks'; +import getBalance from './substrate/get-balance'; +import {alicesPublicKey} from './accounts'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +const UNIQUE_CHAIN = 1000; +const KARURA_CHAIN = 2000; +const KARURA_PORT = '9946'; + +describe('Integration test: Exchanging OPL with Karura', () => { + let alice: IKeyringPair; + + before(async () => { + await usingApi(async () => { + alice = privateKey('//Alice'); + }); + + const karuraApiOptions: ApiOptions = { + provider: new WsProvider('ws://127.0.0.1:' + KARURA_PORT), + }; + + await usingApi(async (api) => { + const destination = { + V0: { + X2: [ + 'Parent', + { + Parachain: UNIQUE_CHAIN, + }, + ], + }, + }; + + const metadata = + { + name: 'OPL', + symbol: 'OPL', + decimals: 18, + minimalBalance: 1, + }; + + const tx = api.tx.assetRegistry.registerForeignAsset(destination, metadata); + const sudoTx = api.tx.sudo.sudo(tx as any); + const events = await submitTransactionAsync(alice, sudoTx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + }, karuraApiOptions); + }); + + it('Should connect and send OPL to Karura', async () => { + let balanceOnKaruraBefore: bigint; + + await usingApi(async (api) => { + const {free} = (await api.query.tokens.accounts(alice.addressRaw, {ForeignAsset: 0})).toJSON() as any; + balanceOnKaruraBefore = free; + }, {provider: new WsProvider('ws://127.0.0.1:' + KARURA_PORT)}); + + await usingApi(async (api) => { + const destination = { + V0: { + X2: [ + 'Parent', + { + Parachain: KARURA_CHAIN, + }, + ], + }, + }; + + const beneficiary = { + V0: { + X1: { + AccountId32: { + network: 'Any', + id: alice.addressRaw, + }, + }, + }, + }; + + const assets = { + V1: [ + { + id: { + Concrete: { + parents: 0, + interior: 'Here', + }, + }, + fun: { + Fungible: 5000000000, + }, + }, + ], + }; + + const feeAssetItem = 0; + + const weightLimit = { + Limited: 5000000000, + }; + + const tx = api.tx.polkadotXcm.limitedReserveTransferAssets(destination, beneficiary, assets, feeAssetItem, weightLimit); + const events = await submitTransactionAsync(alice, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + }); + + await usingApi(async (api) => { + // todo do something about instant sealing, where there might not be any new blocks + await waitNewBlocks(api, 3); + const {free} = (await api.query.tokens.accounts(alice.addressRaw, {ForeignAsset: 0})).toJSON() as any; + expect(free > balanceOnKaruraBefore).to.be.true; + }, {provider: new WsProvider('ws://127.0.0.1:' + KARURA_PORT)}); + }); + + it('Should connect to Karura and send OPL back', async () => { + let balanceBefore: bigint; + + await usingApi(async (api) => { + [balanceBefore] = await getBalance(api, [alicesPublicKey]); + }); + + await usingApi(async (api) => { + const destination = { + V0: { + X3: [ + 'Parent', + { + Parachain: UNIQUE_CHAIN, + }, + { + AccountId32: { + network: 'Any', + id: alice.addressRaw, + }, + }, + ], + }, + }; + + const id = { + ForeignAsset: 0, + }; + + const amount = 5000000000; + const destWeight = 50000000; + + const tx = api.tx.xTokens.transfer(id, amount, destination, destWeight); + const events = await submitTransactionAsync(alice, tx); + const result = getGenericResult(events); + expect(result.success).to.be.true; + }, {provider: new WsProvider('ws://127.0.0.1:' + KARURA_PORT)}); + + await usingApi(async (api) => { + // todo do something about instant sealing, where there might not be any new blocks + await waitNewBlocks(api, 3); + const [balanceAfter] = await getBalance(api, [alicesPublicKey]); + expect(balanceAfter > balanceBefore).to.be.true; + }); + }); +}); \ No newline at end of file