--- a/Cargo.lock +++ b/Cargo.lock @@ -1228,6 +1228,26 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] +name = "enumflags2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "946ee94e3dbf58fdd324f9ce245c7b238d46a66f00e86a020b71996349e46cce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "env_logger" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3502,6 +3522,8 @@ "sc-rpc-api", "sc-service", "sc-transaction-pool", + "serde", + "serde_json", "sp-api", "sp-block-builder", "sp-blockchain", @@ -3541,6 +3563,7 @@ "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-treasury", + "pallet-vesting", "parity-scale-codec", "serde", "sp-api", @@ -4020,6 +4043,20 @@ ] [[package]] +name = "pallet-vesting" +version = "2.0.0" +source = "git+https://github.com/usetech-llc/substrate.git?branch=release_flexi#59646c902484d9c5e8933a80cbed551228b81274" +dependencies = [ + "enumflags2", + "frame-support", + "frame-system", + "parity-scale-codec", + "serde", + "sp-runtime", + "sp-std", +] + +[[package]] name = "parity-db" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" --- a/LICENSE +++ b/LICENSE @@ -1,24 +1,15 @@ -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. +USETECH PROFESSIONAL CONFIDENTIAL +__________________ -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. + [2019] - [2020] UseTech Professional LTD. + All Rights Reserved. -For more information, please refer to +NOTICE: All information contained herein is, and remains +the property of UseTech Professional LTD. and its suppliers, +if any. The intellectual and technical concepts contained +herein are proprietary to UseTech Professional LTD. +and its suppliers and may be covered by U.S. and Foreign Patents, +patents in process, and are protected by trade secret or copyright law. +Dissemination of this information or reproduction of this material +is strictly forbidden unless prior written permission is obtained +from UseTech Professional LTD.. --- a/README.md +++ b/README.md @@ -52,28 +52,27 @@ 2. Remove all installed toolchains with `rustup toolchain list` and `rustup toolchain uninstall `. -3. Install Rust Toolchain 1.44.0: +3. Install Toolchain and make it default: ```bash -rustup install 1.44.0 +rustup toolchain install nightly-2020-10-01 +rustup default nightly-2020-10-01 ``` -4. Make it default (actual toochain version may be different, so do a `rustup toolchain list` first) +4. Add wasm target for default toolchain: + ```bash -rustup toolchain list -rustup default 1.44.0-x86_64-unknown-linux-gnu +rustup target add wasm32-unknown-unknown ``` -5. Install nightly toolchain and add wasm target for it: - +5. Build: ```bash -rustup toolchain install nightly-2020-05-01 -rustup target add wasm32-unknown-unknown --toolchain nightly-2020-05-01-x86_64-unknown-linux-gnu +cargo build ``` -6. Build: +optionally, build in release: ```bash -cargo build +cargo build --release ``` ## Run @@ -134,4 +133,8 @@ ## UI custom types -Moved to [runtime_types.json](./runtime_types.json). \ No newline at end of file +Moved to [runtime_types.json](./runtime_types.json). + +## Running Integration Tests + +See [tests/README.md](./tests/README.md). \ No newline at end of file --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -58,6 +58,9 @@ substrate-frame-rpc-system = {version = '2.0.0', git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi'} pallet-contracts-rpc = {version = '0.8.0', git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi'} +serde = { version = "1.0.102", features = ["derive"] } +serde_json = "1.0.41" + [features] default = [] runtime-benchmarks = ['nft-runtime/runtime-benchmarks'] --- a/node/build.rs +++ b/node/build.rs @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + use substrate_build_script_utils::{generate_cargo_keys, rerun_if_git_head_changed}; fn main() { --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + // use nft_runtime::{ // AccountId, AuraConfig, BalancesConfig, GenesisConfig, GrandpaConfig, Signature, SudoConfig, // SystemConfig, WASM_BINARY, @@ -9,6 +14,7 @@ use sp_core::{sr25519, Pair, Public}; use sp_finality_grandpa::AuthorityId as GrandpaId; use sp_runtime::traits::{IdentifyAccount, Verify}; +use serde_json::map::Map; // Note this is the URL for the telemetry server //const STAGING_TELEMETRY_URL: &str = "wss://telemetry.polkadot.io/submit/"; @@ -41,6 +47,11 @@ pub fn development_config() -> Result { let wasm_binary = WASM_BINARY.ok_or("Development wasm binary not available".to_string())?; + let mut properties = Map::new(); + properties.insert("tokenSymbol".into(), "UniqueTest".into()); + properties.insert("tokenDecimals".into(), 15.into()); + properties.insert("ss58Format".into(), 42.into()); // Generic Substrate wildcard (SS58 checksum preimage) + Ok(ChainSpec::from_genesis( // Name "Development", @@ -71,7 +82,7 @@ // Protocol ID None, // Properties - None, + Some(properties), // Extensions None, )) @@ -132,6 +143,11 @@ endowed_accounts: Vec, enable_println: bool, ) -> GenesisConfig { + + let vested_accounts = vec![ + get_account_id_from_seed::("Bob"), + ]; + GenesisConfig { system: Some(SystemConfig { code: wasm_binary.to_vec(), @@ -154,7 +170,14 @@ .collect(), }), pallet_treasury: Some(Default::default()), - pallet_sudo: Some(SudoConfig { key: root_key }), + pallet_sudo: Some(SudoConfig { key: root_key }), + pallet_vesting: Some(VestingConfig { + vesting: vested_accounts + .iter() + .cloned() + .map(|k| (k, 1000, 100, 1 << 98)) + .collect(), + }), pallet_nft: Some(NftConfig { collection: vec![( 1, --- a/node/src/main.rs +++ b/node/src/main.rs @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + //! Substrate Node Template CLI library. #![warn(missing_docs)] --- a/node/src/service.rs +++ b/node/src/service.rs @@ -1,5 +1,10 @@ //! Service and ServiceFactory implementation. Specialized wrapper over substrate service. +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + use std::sync::Arc; use std::time::Duration; use sc_client_api::{ExecutorProvider, RemoteBackend}; --- a/pallets/nft/src/lib.rs +++ b/pallets/nft/src/lib.rs @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + #![recursion_limit = "1024"] #![cfg_attr(not(feature = "std"), no_std)] --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -45,6 +45,8 @@ pallet-timestamp = { default-features = false, version = '2.0.0' , git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi' } pallet-transaction-payment = { default-features = false, version = '2.0.0' , git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi' } pallet-transaction-payment-rpc-runtime-api = { default-features = false, version = '2.0.0' , git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi' } +pallet-treasury = { version = "2.0.0", default-features = false, git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi' } +pallet-vesting = { version = "2.0.0", default-features = false, git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi' } sp-api = { default-features = false, version = '2.0.0' , git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi' } sp-block-builder = { default-features = false, version = '2.0.0' , git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi' } sp-consensus-aura = { default-features = false, version = '0.8.0' , git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi' } @@ -56,8 +58,6 @@ sp-std = { default-features = false, version = '2.0.0' , git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi' } sp-transaction-pool = { default-features = false, version = '2.0.0' , git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi' } sp-version = { default-features = false, version = '2.0.0' , git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi' } - -pallet-treasury = { version = "2.0.0", default-features = false, git = 'https://github.com/usetech-llc/substrate.git', branch = 'release_flexi' } [features] default = ['std'] @@ -90,6 +90,9 @@ 'pallet-timestamp/std', 'pallet-transaction-payment/std', 'pallet-transaction-payment-rpc-runtime-api/std', + 'pallet-treasury/std', + 'pallet-vesting/std', + 'pallet-nft/std', 'sp-api/std', 'sp-block-builder/std', @@ -103,5 +106,4 @@ 'sp-transaction-pool/std', 'sp-version/std', - 'pallet-treasury/std', ] --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + //! The Substrate Node Template runtime. This can be compiled with `#[no_std]`, ready for Wasm. #![cfg_attr(not(feature = "std"), no_std)] @@ -17,7 +22,7 @@ use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, traits::{ - Convert, BlakeTwo256, Block as BlockT, IdentifyAccount, + Convert, ConvertInto, BlakeTwo256, Block as BlockT, IdentifyAccount, IdentityLookup, NumberFor, Saturating, Verify, }, transaction_validity::{TransactionSource, TransactionValidity}, @@ -412,6 +417,18 @@ type Call = Call; } +parameter_types! { + pub const MinVestedTransfer: Balance = 100 * DOLLARS; +} + +impl pallet_vesting::Trait for Runtime { + type Event = Event; + type Currency = Balances; + type BlockNumberToBalance = ConvertInto; + type MinVestedTransfer = MinVestedTransfer; + type WeightInfo = (); +} + /// Used for the module nft in `./nft.rs` impl pallet_nft::Trait for Runtime { type Event = Event; @@ -435,6 +452,7 @@ Sudo: pallet_sudo::{Module, Call, Config, Storage, Event}, Nft: pallet_nft::{Module, Call, Config, Storage, Event}, Treasury: pallet_treasury::{Module, Call, Storage, Config, Event}, + Vesting: pallet_vesting::{Module, Call, Config, Storage, Event}, } ); --- a/runtime/src/nft_weights.rs +++ b/runtime/src/nft_weights.rs @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + use frame_support::weights::{Weight, constants::RocksDbWeight as DbWeight}; pub struct WeightInfo; --- a/tests/README +++ /dev/null @@ -1,12 +0,0 @@ -# Tests - -## How to run - -1. Run `npm install`. -2. Setup a test node. You can do it using `docker-compose up -d` in parent directory. -3. Configure tests with env variables or by editing [configuration file](src/config.ts). -4. Run `npm run test`. - -## Don't run on the same node twice - -Some tests fail when ran on the same blockchain node twice. Either always use a new node or purge the existing one with `nft purge-chain --dev`. There is also [a script](../purge-running-node.sh) to purge and restart a node, started with docker-compose. --- /dev/null +++ b/tests/README.md @@ -0,0 +1,9 @@ +# Tests + +## How to run + +1. Run `npm install`. +2. Setup a test node. You can do it using `docker-compose up -d` in parent directory. +3. Optional step - configure tests with env variables or by editing [configuration file](src/config.ts). +4. Run `npm test`. + --- a/tests/src/accounts.ts +++ b/tests/src/accounts.ts @@ -1,3 +1,9 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + export const bobsPublicKey = '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty'; export const alicesPublicKey = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'; export const ferdiesPublicKey = '5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL'; +export const nullPublicKey = '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM'; \ No newline at end of file --- a/tests/src/blocks-production.test.ts +++ b/tests/src/blocks-production.test.ts @@ -1,8 +1,13 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + import usingApi from "./substrate/substrate-api"; import promisifySubstrate from "./substrate/promisify-substrate"; import { expect } from "chai"; -describe('Blocks Production', () => { +describe('Blocks Production smoke test', () => { it('Node produces new blocks', async () => { await usingApi(async api => { const blocksPromise = promisifySubstrate(api, () => { --- a/tests/src/config.ts +++ b/tests/src/config.ts @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + import process from 'process'; const config = { --- a/tests/src/connection.test.ts +++ b/tests/src/connection.test.ts @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + import usingApi from "./substrate/substrate-api"; import { WsProvider } from '@polkadot/api'; import * as chai from 'chai'; @@ -7,7 +12,7 @@ const expect = chai.expect; -describe('Connection', () => { +describe('Connection smoke test', () => { it('Connection can be established', async () => { await usingApi(async api => { const health = await api.rpc.system.health(); @@ -16,11 +21,17 @@ }); it('Cannot connect to 255.255.255.255', async () => { + console.log = function () {}; + console.error = function () {}; + const neverConnectProvider = new WsProvider('ws://255.255.255.255:9944'); await expect((async () => { await usingApi(async api => { const health = await api.rpc.system.health(); }, { provider: neverConnectProvider }); })()).to.be.eventually.rejected; + + delete console.log; + delete console.error; }); }); \ No newline at end of file --- a/tests/src/contracts.test.ts +++ b/tests/src/contracts.test.ts @@ -1,13 +1,22 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + +import { ApiPromise } from "@polkadot/api"; import { expect } from "chai"; -import usingApi from "./substrate/substrate-api"; +import { default as usingApi, submitTransactionAsync } from "./substrate/substrate-api"; import fs from "fs"; import { Abi, BlueprintPromise, CodePromise } from "@polkadot/api-contract"; import { IKeyringPair } from "@polkadot/types/types"; import { Keyring } from "@polkadot/api"; import { ApiTypes, SubmittableExtrinsic } from "@polkadot/api/types"; +import { BigNumber } from 'bignumber.js'; +import { findUnusedAddress } from './util/helpers' const value = 0; const gasLimit = 3000n * 1000000n; +const endowment = `1000000000000000`; function deployBlueprint(alice: IKeyringPair, code: CodePromise): Promise { return new Promise(async (resolve, reject) => { @@ -25,7 +34,6 @@ function deployContract(alice: IKeyringPair, blueprint: BlueprintPromise) : Promise { return new Promise(async (resolve, reject) => { - const endowment = 1000000000000000n; const initValue = true; const unsub = await blueprint.tx @@ -39,28 +47,25 @@ }); } -function runTransaction(privateKey: IKeyringPair, extrinsic: SubmittableExtrinsic) { - return new Promise(async (resolve, reject) => { - extrinsic.signAndSend(privateKey, async result => { - if(!result.isInBlock) { - return; - } +async function prepareDeployer(api: ApiPromise) { + // Find unused address + const deployer = await findUnusedAddress(api); - if(result.findRecord('system', 'ExtrinsicSuccess')) { - resolve(); - } - else { - reject('Failed to flip value.'); - } - }) - }); + // Transfer balance to it + const keyring = new Keyring({ type: 'sr25519' }); + const alice = keyring.addFromUri(`//Alice`); + let amount = new BigNumber(endowment); + amount = amount.plus(1e15); + const tx = api.tx.balances.transfer(deployer.address, amount.toFixed()); + await submitTransactionAsync(alice, tx); + + return deployer; } -describe('Contracts', () => { +describe('Contracts smoke test', () => { it(`Can deploy smart contract Flipper, instantiate it and call it's get and flip messages.`, async () => { await usingApi(async api => { - const keyring = new Keyring({ type: 'sr25519' }); - const alice = keyring.addFromUri("//Alice"); + const deployer = await prepareDeployer(api); const wasm = fs.readFileSync('./src/flipper/flipper.wasm'); @@ -69,11 +74,11 @@ const code = new CodePromise(api, abi, wasm); - const blueprint = await deployBlueprint(alice, code); - const contract = (await deployContract(alice, blueprint))['contract']; + const blueprint = await deployBlueprint(deployer, code); + const contract = (await deployContract(deployer, blueprint))['contract']; const getFlipValue = async () => { - const result = await contract.query.get(alice.address, value, gasLimit); + const result = await contract.query.get(deployer.address, value, gasLimit); if(!result.result.isSuccess) { throw `Failed to get flipper value`; @@ -85,7 +90,7 @@ expect(initialGetResponse).to.be.true; const flip = contract.exec('flip', value, gasLimit); - await runTransaction(alice, flip); + await submitTransactionAsync(deployer, flip); const afterFlipGetResponse = await getFlipValue(); @@ -112,7 +117,7 @@ // const bob = new GenericAccountId(api.registry, bobsPublicKey); // const transfer = contractInstance.exec('balance_transfer', 0, 1000000000000n, [bob, new u128(api.registry, 1000000)]); - // await runTransaction(alicesPrivateKey, transfer); + // await submitTransactionAsync(alicesPrivateKey, transfer); // const [alicesBalanceAfter, bobsBalanceAfter] = await getBalance(api, [alicesPublicKey, bobsPublicKey]); --- a/tests/src/createCollection.test.ts +++ b/tests/src/createCollection.test.ts @@ -1,3 +1,8 @@ +// +// 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 { default as usingApi } from "./substrate/substrate-api"; --- a/tests/src/createMultipleItems.test.ts +++ b/tests/src/createMultipleItems.test.ts @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + import { assert } from 'chai'; import { alicesPublicKey } from './accounts'; import privateKey from './substrate/privateKey'; --- /dev/null +++ b/tests/src/creditFeesToTreasury.test.ts @@ -0,0 +1,112 @@ +// +// 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 { default as usingApi, submitTransactionAsync } from "./substrate/substrate-api"; +import { alicesPublicKey, bobsPublicKey } from "./accounts"; +import privateKey from "./substrate/privateKey"; +import { BigNumber } from 'bignumber.js'; +import { createCollectionExpectSuccess, getGenericResult } from './util/helpers'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +const Treasury = "5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z"; +const saneMinimumFee = 0.0001; +const saneMaximumFee = 0.01; + +describe('integration test: Fees must be credited to Treasury:', () => { + it('Total issuance does not change', async () => { + await usingApi(async (api) => { + const totalBefore = new BigNumber((await api.query.balances.totalIssuance()).toString()); + + const alicePrivateKey = privateKey('//Alice'); + const amount = new BigNumber(1); + const transfer = api.tx.balances.transfer(bobsPublicKey, amount.toFixed()); + + const result = getGenericResult(await submitTransactionAsync(alicePrivateKey, transfer)); + + const totalAfter = new BigNumber((await api.query.balances.totalIssuance()).toString()); + + expect(result.success).to.be.true; + expect(totalAfter.toFixed()).to.be.equal(totalBefore.toFixed()); + }); + }); + + it('Sender balance decreased by fee+sent amount, Treasury balance increased by fee', async () => { + await usingApi(async (api) => { + const alicePrivateKey = privateKey('//Alice'); + const treasuryBalanceBefore = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); + const aliceBalanceBefore = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); + + const amount = new BigNumber(1); + const transfer = api.tx.balances.transfer(bobsPublicKey, amount.toFixed()); + const result = getGenericResult(await submitTransactionAsync(alicePrivateKey, transfer)); + + const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); + const aliceBalanceAfter = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); + const fee = aliceBalanceBefore.minus(aliceBalanceAfter).minus(amount); + const treasuryIncrease = treasuryBalanceAfter.minus(treasuryBalanceBefore); + + expect(result.success).to.be.true; + expect(treasuryIncrease.toFixed()).to.be.equal(fee.toFixed()); + }); + }); + + it('Treasury balance increased by failed tx fee', async () => { + await usingApi(async (api) => { + const bobPrivateKey = privateKey('//Bob'); + const treasuryBalanceBefore = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); + const bobBalanceBefore = new BigNumber((await api.query.system.account(bobsPublicKey)).data.free.toString()); + + const badTx = api.tx.balances.setBalance(alicesPublicKey, 0, 0); + const result = getGenericResult(await submitTransactionAsync(bobPrivateKey, badTx)); + + const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); + const bobBalanceAfter = new BigNumber((await api.query.system.account(bobsPublicKey)).data.free.toString()); + const fee = bobBalanceBefore.minus(bobBalanceAfter); + const treasuryIncrease = treasuryBalanceAfter.minus(treasuryBalanceBefore); + + expect(result.success).to.be.false; + expect(treasuryIncrease.toFixed()).to.be.equal(fee.toFixed()); + }); + }); + + it('NFT Transactions also send fees to Treasury', async () => { + await usingApi(async (api) => { + const treasuryBalanceBefore = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); + const aliceBalanceBefore = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); + + await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + + const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); + const aliceBalanceAfter = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); + const fee = aliceBalanceBefore.minus(aliceBalanceAfter); + const treasuryIncrease = treasuryBalanceAfter.minus(treasuryBalanceBefore); + + expect(treasuryIncrease.toFixed()).to.be.equal(fee.toFixed()); + }); + }); + + it('Fees are sane', async () => { + await usingApi(async (api) => { + const treasuryBalanceBefore = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); + const aliceBalanceBefore = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); + + await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + + const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); + const aliceBalanceAfter = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); + const fee = aliceBalanceBefore.minus(aliceBalanceAfter); + const treasuryIncrease = treasuryBalanceAfter.minus(treasuryBalanceBefore); + + expect(fee.dividedBy(1e15).toNumber()).to.be.lessThan(0.01); + expect(fee.dividedBy(1e15).toNumber()).to.be.greaterThan(0.0001); + }); + }); + +}); + --- a/tests/src/crefitFeesToTreasury.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import { default as usingApi, submitTransactionAsync } from "./substrate/substrate-api"; -import { alicesPublicKey, bobsPublicKey } from "./accounts"; -import privateKey from "./substrate/privateKey"; -import { BigNumber } from 'bignumber.js'; -import { createCollectionExpectSuccess, getGenericResult } from './util/helpers'; - -chai.use(chaiAsPromised); -const expect = chai.expect; - -const Treasury = "5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z"; -const saneMinimumFee = 0.0001; -const saneMaximumFee = 0.01; - -describe('integration test: Fees must be credited to Treasury:', () => { - it('Total issuance does not change', async () => { - await usingApi(async (api) => { - const totalBefore = new BigNumber((await api.query.balances.totalIssuance()).toString()); - - const alicePrivateKey = privateKey('//Alice'); - const amount = new BigNumber(1); - const transfer = api.tx.balances.transfer(bobsPublicKey, amount.toFixed()); - - const result = getGenericResult(await submitTransactionAsync(alicePrivateKey, transfer)); - - const totalAfter = new BigNumber((await api.query.balances.totalIssuance()).toString()); - - expect(result.success).to.be.true; - expect(totalAfter.toFixed()).to.be.equal(totalBefore.toFixed()); - }); - }); - - it('Sender balance decreased by fee+sent amount, Treasury balance increased by fee', async () => { - await usingApi(async (api) => { - const alicePrivateKey = privateKey('//Alice'); - const treasuryBalanceBefore = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); - const aliceBalanceBefore = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); - - const amount = new BigNumber(1); - const transfer = api.tx.balances.transfer(bobsPublicKey, amount.toFixed()); - const result = getGenericResult(await submitTransactionAsync(alicePrivateKey, transfer)); - - const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); - const aliceBalanceAfter = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); - const fee = aliceBalanceBefore.minus(aliceBalanceAfter).minus(amount); - const treasuryIncrease = treasuryBalanceAfter.minus(treasuryBalanceBefore); - - expect(result.success).to.be.true; - expect(treasuryIncrease.toFixed()).to.be.equal(fee.toFixed()); - }); - }); - - it('Treasury balance increased by failed tx fee', async () => { - await usingApi(async (api) => { - const bobPrivateKey = privateKey('//Bob'); - const treasuryBalanceBefore = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); - const bobBalanceBefore = new BigNumber((await api.query.system.account(bobsPublicKey)).data.free.toString()); - - const badTx = api.tx.balances.setBalance(alicesPublicKey, 0, 0); - const result = getGenericResult(await submitTransactionAsync(bobPrivateKey, badTx)); - - const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); - const bobBalanceAfter = new BigNumber((await api.query.system.account(bobsPublicKey)).data.free.toString()); - const fee = bobBalanceBefore.minus(bobBalanceAfter); - const treasuryIncrease = treasuryBalanceAfter.minus(treasuryBalanceBefore); - - expect(result.success).to.be.false; - expect(treasuryIncrease.toFixed()).to.be.equal(fee.toFixed()); - }); - }); - - it('NFT Transactions also send fees to Treasury', async () => { - await usingApi(async (api) => { - const treasuryBalanceBefore = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); - const aliceBalanceBefore = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); - - await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); - - const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); - const aliceBalanceAfter = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); - const fee = aliceBalanceBefore.minus(aliceBalanceAfter); - const treasuryIncrease = treasuryBalanceAfter.minus(treasuryBalanceBefore); - - expect(treasuryIncrease.toFixed()).to.be.equal(fee.toFixed()); - }); - }); - - it('Fees are sane', async () => { - await usingApi(async (api) => { - const treasuryBalanceBefore = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); - const aliceBalanceBefore = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); - - await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); - - const treasuryBalanceAfter = new BigNumber((await api.query.system.account(Treasury)).data.free.toString()); - const aliceBalanceAfter = new BigNumber((await api.query.system.account(alicesPublicKey)).data.free.toString()); - const fee = aliceBalanceBefore.minus(aliceBalanceAfter); - const treasuryIncrease = treasuryBalanceAfter.minus(treasuryBalanceBefore); - - expect(fee.dividedBy(1e15).toNumber()).to.be.lessThan(0.01); - expect(fee.dividedBy(1e15).toNumber()).to.be.greaterThan(0.0001); - }); - }); - -}); - --- /dev/null +++ b/tests/src/destroyCollection.test.ts @@ -0,0 +1,87 @@ +import chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { default as usingApi, submitTransactionAsync } from "./substrate/substrate-api"; +import { createCollectionExpectSuccess, createCollectionExpectFailure } from "./util/helpers"; +import type { AccountId, EventRecord } from '@polkadot/types/interfaces'; +import privateKey from './substrate/privateKey'; +import { nullPublicKey } from './accounts'; + +chai.use(chaiAsPromised); +const expect = chai.expect; + +function getDestroyResult(events: EventRecord[]): boolean { + let success: boolean = false; + events.forEach(({ phase, event: { data, method, section } }) => { + // console.log(` ${phase}: ${section}.${method}:: ${data}`); + if (method == 'ExtrinsicSuccess') { + success = true; + } + }); + return success; +} + +async function destroyCollectionExpectSuccess(collectionId: number, senderSeed: string = '//Alice') { + await usingApi(async (api) => { + // Run the DestroyCollection transaction + const alicePrivateKey = privateKey(senderSeed); + const tx = api.tx.nft.destroyCollection(collectionId); + const events = await submitTransactionAsync(alicePrivateKey, tx); + const result = getDestroyResult(events); + + // Get the collection + const collection: any = (await api.query.nft.collection(collectionId)).toJSON(); + + // What to expect + expect(result).to.be.true; + expect(collection).to.be.not.null; + expect(collection.Owner).to.be.equal(nullPublicKey); + }); +} + +async function destroyCollectionExpectFailure(collectionId: number, senderSeed: string = '//Alice') { + await usingApi(async (api) => { + // Run the DestroyCollection transaction + const alicePrivateKey = privateKey(senderSeed); + const tx = api.tx.nft.destroyCollection(collectionId); + const events = await submitTransactionAsync(alicePrivateKey, tx); + const result = getDestroyResult(events); + + // What to expect + expect(result).to.be.false; + }); +} + +describe('integration test: ext. destroyCollection():', () => { + it('NFT collection can be destroyed', async () => { + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + await destroyCollectionExpectSuccess(collectionId); + }); + it('Fungible collection can be destroyed', async () => { + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'Fungible'); + await destroyCollectionExpectSuccess(collectionId); + }); + it('ReFungible collection can be destroyed', async () => { + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'ReFungible'); + await destroyCollectionExpectSuccess(collectionId); + }); +}); + +describe('(!negative test!) integration test: ext. destroyCollection():', () => { + it('(!negative test!) Destroy a collection that never existed', async () => { + await usingApi(async (api) => { + // Find the collection that never existed + const collectionId = parseInt((await api.query.nft.createdCollectionCount()).toString()) + 1; + await destroyCollectionExpectFailure(collectionId); + }); + }); + it('(!negative test!) Destroy a collection that has already been destroyed', async () => { + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + await destroyCollectionExpectSuccess(collectionId); + await destroyCollectionExpectFailure(collectionId); + }); + it('(!negative test!) Destroy a collection using non-owner account', async () => { + const collectionId = await createCollectionExpectSuccess('A', 'B', 'C', 'NFT'); + await destroyCollectionExpectFailure(collectionId, '//Bob'); + await destroyCollectionExpectSuccess(collectionId, '//Alice'); + }); +}); --- a/tests/src/pallet-presence.test.ts +++ b/tests/src/pallet-presence.test.ts @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + import { ApiPromise } from "@polkadot/api"; import { expect } from "chai"; import usingApi from "./substrate/substrate-api"; @@ -6,20 +11,34 @@ return api.runtimeMetadata.asLatest.modules.map(m => m.name.toString().toLowerCase()); } -describe('Pallet presence.', () => { - it('NFT pallet is present.', async () => { +// Pallets that must always be present +const requiredPallets = [ + 'nft', 'balances', 'contracts', 'randomnesscollectiveflip', 'system', 'timestamp', 'transactionpayment', 'treasury', 'vesting' +]; + +// Pallets that depend on consensus and governance configuration +const consensusPallets = [ + 'sudo', 'grandpa', 'aura' +]; + +describe('Pallet presence', () => { + it('Required pallets are present', async () => { await usingApi(async api => { - expect(getModuleNames(api)).to.include('nft'); + for (let i=0; i { + it('Governance and consensus pallets are present', async () => { await usingApi(async api => { - expect(getModuleNames(api)).to.include('balances'); + for (let i=0; i { + it('No extra pallets are included', async () => { await usingApi(async api => { - expect(getModuleNames(api)).to.include('contracts'); + expect(getModuleNames(api).length).to.be.equal(requiredPallets.length + consensusPallets.length); }); }); }); --- a/tests/src/substrate/get-balance.ts +++ b/tests/src/substrate/get-balance.ts @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + import { ApiPromise } from "@polkadot/api"; import promisifySubstrate from "./promisify-substrate"; import {AccountInfo} from "@polkadot/types/interfaces/system"; --- a/tests/src/substrate/privateKey.ts +++ b/tests/src/substrate/privateKey.ts @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + import { Keyring } from "@polkadot/api"; import { IKeyringPair } from "@polkadot/types/types"; --- a/tests/src/substrate/promisify-substrate.ts +++ b/tests/src/substrate/promisify-substrate.ts @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + import ApiPromise from "@polkadot/api/promise/Api"; type PromiseType = T extends PromiseLike ? TInner : T; --- a/tests/src/substrate/substrate-api.ts +++ b/tests/src/substrate/substrate-api.ts @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + import { WsProvider, ApiPromise } from "@polkadot/api"; import type { AccountId, Address, ApplyExtrinsicResult, DispatchError, DispatchInfo, EventRecord, Extrinsic, ExtrinsicStatus, Hash, RuntimeDispatchInfo } from '@polkadot/types/interfaces'; import { IKeyringPair } from "@polkadot/types/types"; --- a/tests/src/substrate/wait-new-blocks.ts +++ b/tests/src/substrate/wait-new-blocks.ts @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + import { ApiPromise } from "@polkadot/api"; export default function waitNewBlocks(api: ApiPromise, blocksCount: number = 1): Promise { --- a/tests/src/transfer.test.ts +++ b/tests/src/transfer.test.ts @@ -1,8 +1,15 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + import { expect, assert } from "chai"; import { default as usingApi, submitTransactionAsync } from "./substrate/substrate-api"; import { alicesPublicKey, bobsPublicKey, ferdiesPublicKey } from "./accounts"; import privateKey from "./substrate/privateKey"; import getBalance from "./substrate/get-balance"; +import { BigNumber } from 'bignumber.js'; +import { findUnusedAddress } from './util/helpers' describe('Transfer', () => { it('Balance transfers', async () => { @@ -23,7 +30,8 @@ it('Inability to pay fees error message is correct', async () => { await usingApi(async api => { - const pk = privateKey('//Ferdie'); + // Find unused address + const pk = await findUnusedAddress(api); console.log = function () {}; console.error = function () {}; --- a/tests/src/util/helpers.ts +++ b/tests/src/util/helpers.ts @@ -1,10 +1,18 @@ +// +// 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 type { EventRecord } from '@polkadot/types/interfaces'; +import type { AccountId, EventRecord } from '@polkadot/types/interfaces'; +import { ApiPromise, Keyring } from "@polkadot/api"; import { default as usingApi, submitTransactionAsync } from "../substrate/substrate-api"; import privateKey from '../substrate/privateKey'; import { alicesPublicKey } from "../accounts"; import { strToUTF16, utf16ToStr, hexToStr } from '../util/util'; +import { IKeyringPair } from "@polkadot/types/types"; +import { BigNumber } from 'bignumber.js'; chai.use(chaiAsPromised); const expect = chai.expect; @@ -49,7 +57,8 @@ return result; } -export async function createCollectionExpectSuccess(name: string, description: string, tokenPrefix: string, mode: string) { +export async function createCollectionExpectSuccess(name: string, description: string, tokenPrefix: string, mode: string): Promise { + let collectionId: number = 0; await usingApi(async (api) => { // Get number of collections before the transaction const AcollectionCount = parseInt((await api.query.nft.createdCollectionCount()).toString()); @@ -75,7 +84,11 @@ expect(utf16ToStr(collection.Name)).to.be.equal(name); expect(utf16ToStr(collection.Description)).to.be.equal(description); expect(hexToStr(collection.TokenPrefix)).to.be.equal(tokenPrefix); + + collectionId = result.collectionId; }); + + return collectionId; } export async function createCollectionExpectFailure(name: string, description: string, tokenPrefix: string, mode: string) { @@ -98,3 +111,14 @@ }); } +export async function findUnusedAddress(api: ApiPromise): Promise { + let bal = new BigNumber(0); + let unused; + do { + const randomSeed = 'seed' + Math.floor(Math.random() * Math.floor(10000)); + const keyring = new Keyring({ type: 'sr25519' }); + unused = keyring.addFromUri(`//${randomSeed}`); + bal = new BigNumber((await api.query.system.account(unused.address)).data.free.toString()); + } while (bal.toFixed() != '0'); + return unused; +} \ No newline at end of file --- a/tests/src/util/util.ts +++ b/tests/src/util/util.ts @@ -1,3 +1,8 @@ +// +// This file is subject to the terms and conditions defined in +// file 'LICENSE', which is part of this source code package. +// + export function strToUTF16(str: string): any { let buf: number[] = []; for (let i=0, strLen=str.length; i < strLen; i++) {