diff --git a/.gitignore b/.gitignore index 22fa52c180..e590d097e2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ yarn* # Visual Studio Code .vscode + +# Compiled WASM code +*.wasm \ No newline at end of file diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index 7d663a7176..250fa271d0 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -23,8 +23,9 @@ use node_runtime::{ AuthorityDiscoveryConfig, BabeConfig, Balance, BalancesConfig, ContentWorkingGroupConfig, CouncilConfig, CouncilElectionConfig, DataObjectStorageRegistryConfig, DataObjectTypeRegistryConfig, ElectionParameters, GrandpaConfig, ImOnlineConfig, IndicesConfig, - MembersConfig, Perbill, ProposalsCodexConfig, SessionConfig, SessionKeys, Signature, - StakerStatus, StakingConfig, SudoConfig, SystemConfig, VersionedStoreConfig, DAYS, WASM_BINARY, + MembersConfig, MigrationConfig, Perbill, ProposalsCodexConfig, SessionConfig, SessionKeys, + Signature, StakerStatus, StakingConfig, SudoConfig, SystemConfig, VersionedStoreConfig, DAYS, + WASM_BINARY, }; pub use node_runtime::{AccountId, GenesisConfig}; use primitives::{sr25519, Pair, Public}; @@ -301,6 +302,7 @@ pub fn testnet_genesis( channel_banner_constraint: crate::forum_config::new_validation(5, 1024), channel_title_constraint: crate::forum_config::new_validation(5, 1024), }), + migration: Some(MigrationConfig {}), proposals_codex: Some(ProposalsCodexConfig { set_validator_count_proposal_voting_period: cpcp .set_validator_count_proposal_voting_period, diff --git a/package.json b/package.json index cbe0e84994..ead0da298b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "name": "joystream", "license": "GPL-3.0-only", "scripts": { - "test": "yarn && yarn workspaces run test" + "test": "yarn && yarn workspaces run test", + "test-migration": "yarn && yarn workspaces run test-migration" }, "workspaces": [ "tests/network-tests" diff --git a/runtime-modules/content-working-group/src/lib.rs b/runtime-modules/content-working-group/src/lib.rs index 7f3df8821a..90f2c5e8e9 100755 --- a/runtime-modules/content-working-group/src/lib.rs +++ b/runtime-modules/content-working-group/src/lib.rs @@ -1894,7 +1894,7 @@ decl_module! { origin, curator_id: CuratorId, rationale_text: Vec - ) { + ) { // Ensure lead is set and is origin signer Self::ensure_origin_is_set_lead(origin)?; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5a3bceb939..cea8303ef5 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -889,12 +889,12 @@ construct_runtime!( RandomnessCollectiveFlip: randomness_collective_flip::{Module, Call, Storage}, Sudo: sudo, // Joystream + Migration: migration::{Module, Call, Storage, Event, Config}, CouncilElection: election::{Module, Call, Storage, Event, Config}, Council: council::{Module, Call, Storage, Event, Config}, Memo: memo::{Module, Call, Storage, Event}, Members: members::{Module, Call, Storage, Event, Config}, Forum: forum::{Module, Call, Storage, Event, Config}, - Migration: migration::{Module, Call, Storage, Event}, Actors: actors::{Module, Call, Storage, Event, Config}, DataObjectTypeRegistry: data_object_type_registry::{Module, Call, Storage, Event, Config}, DataDirectory: data_directory::{Module, Call, Storage, Event}, diff --git a/runtime/src/migration.rs b/runtime/src/migration.rs index c05acfd655..b7ac479177 100644 --- a/runtime/src/migration.rs +++ b/runtime/src/migration.rs @@ -2,23 +2,65 @@ #![allow(clippy::redundant_closure_call)] // disable it because of the substrate lib design use crate::VERSION; +use common::currency::BalanceOf; +use rstd::prelude::*; use sr_primitives::{print, traits::Zero}; -use srml_support::{decl_event, decl_module, decl_storage}; +use srml_support::{debug, decl_event, decl_module, decl_storage}; impl Module { + /// This method is called from on_initialize() when a runtime upgrade is detected. This + /// happens when the runtime spec version is found to be higher than the stored value. + /// Important to note this method should be carefully maintained, because it runs on every runtime + /// upgrade. fn runtime_upgraded() { - print("running runtime initializers..."); + print("Running runtime upgraded handler"); - // ... - // add initialization of modules introduced in new runtime release. This + // Add initialization of modules introduced in new runtime release. Typically this // would be any new storage values that need an initial value which would not - // have been initialized with config() or build() mechanism. - // ... + // have been initialized with config() or build() chainspec construction mechanism. + // Other tasks like resetting values, migrating values etc. + + // Runtime Upgrade Code for going from Rome to Constantinople // Create the Council mint. If it fails, we can't do anything about it here. - let _ = governance::council::Module::::create_new_council_mint( + if let Err(err) = governance::council::Module::::create_new_council_mint( minting::BalanceOf::::zero(), - ); + ) { + debug::warn!( + "Failed to create a mint for council during migration: {:?}", + err + ); + } + + // Reset working group mint capacity + if let Err(err) = content_working_group::Module::::set_mint_capacity( + system::RawOrigin::Root.into(), + minting::BalanceOf::::zero(), + ) { + debug::warn!( + "Failed to reset mint for working group during migration: {:?}", + err + ); + } + + // Set Storage Role reward to zero + if let Some(parameters) = + roles::actors::Parameters::::get(roles::actors::Role::StorageProvider) + { + if let Err(err) = roles::actors::Module::::set_role_parameters( + system::RawOrigin::Root.into(), + roles::actors::Role::StorageProvider, + roles::actors::RoleParameters { + reward: BalanceOf::::zero(), + ..parameters + }, + ) { + debug::warn!( + "Failed to set zero reward for storage role during migration: {:?}", + err + ); + } + } proposals_codex::Module::::set_default_config_values(); @@ -31,11 +73,9 @@ impl Module { pub trait Trait: system::Trait - + storage::data_directory::Trait - + storage::data_object_storage_registry::Trait - + forum::Trait - + sudo::Trait - + governance::council::Trait + + governance::election::Trait + + content_working_group::Trait + + roles::actors::Trait + proposals_codex::Trait { type Event: From> + Into<::Event>; @@ -43,9 +83,13 @@ pub trait Trait: decl_storage! { trait Store for Module as Migration { - /// Records at what runtime spec version the store was initialized. This allows the runtime - /// to know when to run initialize code if it was installed as an update. - pub SpecVersion get(spec_version) build(|_| VERSION.spec_version) : Option; + /// Records at what runtime spec version the store was initialized. At genesis this will be + /// initialized to Some(VERSION.spec_version). It is an Option because the first time the module + /// was introduced was as a runtime upgrade and type was never changed. + /// When the runtime is upgraded the spec version be updated. + pub SpecVersion get(spec_version) build(|_config: &GenesisConfig| { + VERSION.spec_version + }) : Option; } } @@ -61,11 +105,16 @@ decl_module! { fn on_initialize(_now: T::BlockNumber) { if Self::spec_version().map_or(true, |spec_version| VERSION.spec_version > spec_version) { - // mark store version with current version of the runtime + // Mark store version with current version of the runtime SpecVersion::put(VERSION.spec_version); - // run migrations and store initializers + // Run migrations and store initializers Self::runtime_upgraded(); + + Self::deposit_event(RawEvent::Migrated( + >::block_number(), + VERSION.spec_version, + )); } } } diff --git a/tests/network-tests/.env b/tests/network-tests/.env index 937f1e6923..bbcf7b95da 100644 --- a/tests/network-tests/.env +++ b/tests/network-tests/.env @@ -12,5 +12,11 @@ COUNCIL_STAKE_GREATER_AMOUNT = 1500 COUNCIL_STAKE_LESSER_AMOUNT = 1000 # Number of members with greater stake in council election test. COUNCIL_ELECTION_K = 2 -# Stake for runtime upgrade proposal test -RUNTIME_UPGRADE_PROPOSAL_STAKE = 200 \ No newline at end of file +# Balance to spend using spending proposal +SPENDING_BALANCE = 1000 +# Minting capacity increment for content working group minting capacity test. +MINTING_CAPACITY_INCREMENT = 20 +# Minting capacity for council mint for spending proposal. +COUNCIL_MINTING_CAPACITY = 100000 +# Stake amount for Rome runtime upgrade proposal +RUNTIME_UPGRADE_PROPOSAL_STAKE = 100000 \ No newline at end of file diff --git a/tests/network-tests/package.json b/tests/network-tests/package.json index 3c59a6f28f..e0e8df66dd 100644 --- a/tests/network-tests/package.json +++ b/tests/network-tests/package.json @@ -4,17 +4,20 @@ "license": "GPL-3.0-only", "scripts": { "build": "tsc --build tsconfig.json", - "test": "mocha -r ts-node/register src/tests/*", + "test": "mocha -r ts-node/register src/tests/constantinople/proposals/*", + "test-migration": "mocha -r ts-node/register src/tests/rome/* && mocha -r ts-node/register src/tests/constantinople/*", "lint": "tslint --project tsconfig.json", "prettier": "prettier --write ./src" }, "dependencies": { - "@joystream/types": "^0.7.0", + "@joystream/types": "../joystream-apps/packages/joy-types", + "@rome/types@npm:@joystream/types": "^0.7.0", "@polkadot/api": "^0.96.1", "@polkadot/keyring": "^1.7.0-beta.5", "@types/bn.js": "^4.11.5", "bn.js": "^4.11.8", "dotenv": "^8.2.0", + "fs": "^0.0.1-security", "uuid": "^7.0.3" }, "devDependencies": { diff --git a/tests/network-tests/src/tests/electingCouncilTest.ts b/tests/network-tests/src/tests/constantinople/electingCouncilTest.ts similarity index 97% rename from tests/network-tests/src/tests/electingCouncilTest.ts rename to tests/network-tests/src/tests/constantinople/electingCouncilTest.ts index 25338d8fad..fd0b7dccd7 100644 --- a/tests/network-tests/src/tests/electingCouncilTest.ts +++ b/tests/network-tests/src/tests/constantinople/electingCouncilTest.ts @@ -1,13 +1,13 @@ import { membershipTest } from './membershipCreationTest'; import { KeyringPair } from '@polkadot/keyring/types'; -import { ApiWrapper } from '../utils/apiWrapper'; +import { ApiWrapper } from '../../utils/apiWrapper'; import { WsProvider, Keyring } from '@polkadot/api'; -import { initConfig } from '../utils/config'; +import { initConfig } from '../../utils/config'; import BN = require('bn.js'); import { registerJoystreamTypes, Seat } from '@joystream/types'; import { assert } from 'chai'; import { v4 as uuid } from 'uuid'; -import { Utils } from '../utils/utils'; +import { Utils } from '../../utils/utils'; export function councilTest(m1KeyPairs: KeyringPair[], m2KeyPairs: KeyringPair[]) { initConfig(); diff --git a/tests/network-tests/src/tests/membershipCreationTest.ts b/tests/network-tests/src/tests/constantinople/membershipCreationTest.ts similarity index 86% rename from tests/network-tests/src/tests/membershipCreationTest.ts rename to tests/network-tests/src/tests/constantinople/membershipCreationTest.ts index 38037386f0..ca5ba1cda2 100644 --- a/tests/network-tests/src/tests/membershipCreationTest.ts +++ b/tests/network-tests/src/tests/constantinople/membershipCreationTest.ts @@ -4,8 +4,8 @@ import { Keyring } from '@polkadot/keyring'; import { assert } from 'chai'; import { KeyringPair } from '@polkadot/keyring/types'; import BN = require('bn.js'); -import { ApiWrapper } from '../utils/apiWrapper'; -import { initConfig } from '../utils/config'; +import { ApiWrapper } from '../../utils/apiWrapper'; +import { initConfig } from '../../utils/config'; import { v4 as uuid } from 'uuid'; export function membershipTest(nKeyPairs: KeyringPair[]) { @@ -50,8 +50,8 @@ export function membershipTest(nKeyPairs: KeyringPair[]) { ); nKeyPairs.forEach((keyPair, index) => apiWrapper - .getMembership(keyPair.address) - .then(membership => assert(!membership.isEmpty, `Account ${keyPair.address} is not a member`)) + .getMemberIds(keyPair.address) + .then(membership => assert(membership.length > 0, `Account ${keyPair.address} is not a member`)) ); }).timeout(defaultTimeout); @@ -65,7 +65,9 @@ export function membershipTest(nKeyPairs: KeyringPair[]) { ) ); await apiWrapper.buyMembership(aKeyPair, paidTerms, `late_member_${aKeyPair.address.substring(0, 8)}`, true); - apiWrapper.getMembership(aKeyPair.address).then(membership => assert(membership.isEmpty, 'Account A is a member')); + apiWrapper + .getMemberIds(aKeyPair.address) + .then(membership => assert(membership.length === 0, 'Account A is a member')); }).timeout(defaultTimeout); it('Account A was able to buy the membership with sufficient funds', async () => { @@ -77,8 +79,8 @@ export function membershipTest(nKeyPairs: KeyringPair[]) { ); await apiWrapper.buyMembership(aKeyPair, paidTerms, `late_member_${aKeyPair.address.substring(0, 8)}`); apiWrapper - .getMembership(aKeyPair.address) - .then(membership => assert(!membership.isEmpty, 'Account A is a not member')); + .getMemberIds(aKeyPair.address) + .then(membership => assert(membership.length > 0, 'Account A is a not member')); }).timeout(defaultTimeout); after(() => { diff --git a/tests/network-tests/src/tests/constantinople/proposals/spendingProposalTest.ts b/tests/network-tests/src/tests/constantinople/proposals/spendingProposalTest.ts new file mode 100644 index 0000000000..92d84e3d14 --- /dev/null +++ b/tests/network-tests/src/tests/constantinople/proposals/spendingProposalTest.ts @@ -0,0 +1,86 @@ +import { initConfig } from '../../../utils/config'; +import { Keyring, WsProvider } from '@polkadot/api'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { membershipTest } from '../membershipCreationTest'; +import { councilTest } from '../electingCouncilTest'; +import { registerJoystreamTypes } from '@joystream/types'; +import { ApiWrapper } from '../../../utils/apiWrapper'; +import { v4 as uuid } from 'uuid'; +import BN = require('bn.js'); +import { assert } from 'chai'; + +describe('Spending proposal network tests', () => { + initConfig(); + const keyring = new Keyring({ type: 'sr25519' }); + const nodeUrl: string = process.env.NODE_URL!; + const sudoUri: string = process.env.SUDO_ACCOUNT_URI!; + const spendingBalance: BN = new BN(+process.env.SPENDING_BALANCE!); + const mintCapacity: BN = new BN(+process.env.COUNCIL_MINTING_CAPACITY!); + const defaultTimeout: number = 120000; + + const m1KeyPairs: KeyringPair[] = new Array(); + const m2KeyPairs: KeyringPair[] = new Array(); + + let apiWrapper: ApiWrapper; + let sudo: KeyringPair; + + before(async function () { + this.timeout(defaultTimeout); + registerJoystreamTypes(); + const provider = new WsProvider(nodeUrl); + apiWrapper = await ApiWrapper.create(provider); + }); + + membershipTest(m1KeyPairs); + membershipTest(m2KeyPairs); + councilTest(m1KeyPairs, m2KeyPairs); + + it('Spending proposal test', async () => { + // Setup + sudo = keyring.addFromUri(sudoUri); + const description: string = 'spending proposal which is used for API network testing with some mock data'; + const runtimeVoteFee: BN = apiWrapper.estimateVoteForProposalFee(); + + // Topping the balances + const proposalStake: BN = await apiWrapper.getRequiredProposalStake(25, 10000); + const runtimeProposalFee: BN = apiWrapper.estimateProposeSpendingFee( + description, + description, + proposalStake, + spendingBalance, + sudo.address + ); + await apiWrapper.transferBalance(sudo, m1KeyPairs[0].address, runtimeProposalFee.add(proposalStake)); + await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee); + await apiWrapper.sudoSetCouncilMintCapacity(sudo, mintCapacity); + + // Proposal creation + const proposalPromise = apiWrapper.expectProposalCreated(); + await apiWrapper.proposeSpending( + m1KeyPairs[0], + 'testing spending' + uuid().substring(0, 8), + 'spending to test proposal functionality' + uuid().substring(0, 8), + proposalStake, + spendingBalance, + sudo.address + ); + const proposalNumber = await proposalPromise; + + // Approving spending proposal + const balanceBeforeMinting: BN = await apiWrapper.getBalance(sudo.address); + const spendingPromise = apiWrapper.expectProposalFinalized(); + await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber); + await spendingPromise; + const balanceAfterMinting: BN = await apiWrapper.getBalance(sudo.address); + assert( + balanceAfterMinting.sub(balanceBeforeMinting).eq(spendingBalance), + `member ${ + m1KeyPairs[0].address + } has unexpected balance ${balanceAfterMinting}, expected ${balanceBeforeMinting.add(spendingBalance)}` + ); + }).timeout(defaultTimeout); + + after(() => { + apiWrapper.close(); + }); +}); diff --git a/tests/network-tests/src/tests/constantinople/proposals/textProposalTest.ts b/tests/network-tests/src/tests/constantinople/proposals/textProposalTest.ts new file mode 100644 index 0000000000..04a3d246c1 --- /dev/null +++ b/tests/network-tests/src/tests/constantinople/proposals/textProposalTest.ts @@ -0,0 +1,68 @@ +import { initConfig } from '../../../utils/config'; +import { Keyring, WsProvider } from '@polkadot/api'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { membershipTest } from '../membershipCreationTest'; +import { councilTest } from '../electingCouncilTest'; +import { registerJoystreamTypes } from '@joystream/types'; +import { ApiWrapper } from '../../../utils/apiWrapper'; +import { v4 as uuid } from 'uuid'; +import BN = require('bn.js'); + +describe('Text proposal network tests', () => { + initConfig(); + const keyring = new Keyring({ type: 'sr25519' }); + const nodeUrl: string = process.env.NODE_URL!; + const sudoUri: string = process.env.SUDO_ACCOUNT_URI!; + const defaultTimeout: number = 180000; + + const m1KeyPairs: KeyringPair[] = new Array(); + const m2KeyPairs: KeyringPair[] = new Array(); + + let apiWrapper: ApiWrapper; + let sudo: KeyringPair; + + before(async function () { + this.timeout(defaultTimeout); + registerJoystreamTypes(); + const provider = new WsProvider(nodeUrl); + apiWrapper = await ApiWrapper.create(provider); + }); + + membershipTest(m1KeyPairs); + membershipTest(m2KeyPairs); + councilTest(m1KeyPairs, m2KeyPairs); + + it('Text proposal test', async () => { + // Setup + sudo = keyring.addFromUri(sudoUri); + const proposalTitle: string = 'Testing proposal ' + uuid().substring(0, 8); + const description: string = 'Testing text proposal ' + uuid().substring(0, 8); + const proposalText: string = 'Text of the testing proposal ' + uuid().substring(0, 8); + const runtimeVoteFee: BN = apiWrapper.estimateVoteForProposalFee(); + await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee); + + // Proposal stake calculation + const proposalStake: BN = await apiWrapper.getRequiredProposalStake(25, 10000); + const runtimeProposalFee: BN = apiWrapper.estimateProposeTextFee( + proposalStake, + description, + description, + proposalText + ); + await apiWrapper.transferBalance(sudo, m1KeyPairs[0].address, runtimeProposalFee.add(proposalStake)); + + // Proposal creation + const proposalPromise = apiWrapper.expectProposalCreated(); + await apiWrapper.proposeText(m1KeyPairs[0], proposalStake, proposalTitle, description, proposalText); + const proposalNumber = await proposalPromise; + + // Approving text proposal + const textProposalPromise = apiWrapper.expectProposalFinalized(); + await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber); + await textProposalPromise; + }).timeout(defaultTimeout); + + after(() => { + apiWrapper.close(); + }); +}); diff --git a/tests/network-tests/src/tests/updateRuntimeTest.ts b/tests/network-tests/src/tests/constantinople/proposals/updateRuntimeTest.ts similarity index 77% rename from tests/network-tests/src/tests/updateRuntimeTest.ts rename to tests/network-tests/src/tests/constantinople/proposals/updateRuntimeTest.ts index 402b0410f6..2e79576f69 100644 --- a/tests/network-tests/src/tests/updateRuntimeTest.ts +++ b/tests/network-tests/src/tests/constantinople/proposals/updateRuntimeTest.ts @@ -1,19 +1,19 @@ -import { initConfig } from '../utils/config'; +import { initConfig } from '../../../utils/config'; import { Keyring, WsProvider } from '@polkadot/api'; import { Bytes } from '@polkadot/types'; import { KeyringPair } from '@polkadot/keyring/types'; -import { membershipTest } from './membershipCreationTest'; -import { councilTest } from './electingCouncilTest'; +import { membershipTest } from '../membershipCreationTest'; +import { councilTest } from '../electingCouncilTest'; import { registerJoystreamTypes } from '@joystream/types'; -import { ApiWrapper } from '../utils/apiWrapper'; +import { ApiWrapper } from '../../../utils/apiWrapper'; +import { v4 as uuid } from 'uuid'; import BN = require('bn.js'); -describe('Runtime upgrade integration tests', () => { +describe('Runtime upgrade networt tests', () => { initConfig(); const keyring = new Keyring({ type: 'sr25519' }); const nodeUrl: string = process.env.NODE_URL!; const sudoUri: string = process.env.SUDO_ACCOUNT_URI!; - const proposalStake: BN = new BN(+process.env.RUNTIME_UPGRADE_PROPOSAL_STAKE!); const defaultTimeout: number = 120000; const m1KeyPairs: KeyringPair[] = new Array(); @@ -37,16 +37,17 @@ describe('Runtime upgrade integration tests', () => { // Setup sudo = keyring.addFromUri(sudoUri); const runtime: Bytes = await apiWrapper.getRuntime(); - const description: string = 'runtime upgrade proposal which is used for API integration testing'; + const description: string = 'runtime upgrade proposal which is used for API network testing'; + const runtimeVoteFee: BN = apiWrapper.estimateVoteForProposalFee(); + + // Topping the balances + const proposalStake: BN = await apiWrapper.getRequiredProposalStake(1, 100); const runtimeProposalFee: BN = apiWrapper.estimateProposeRuntimeUpgradeFee( proposalStake, description, description, runtime ); - const runtimeVoteFee: BN = apiWrapper.estimateVoteForProposalFee(); - - // Topping the balances await apiWrapper.transferBalance(sudo, m1KeyPairs[0].address, runtimeProposalFee.add(proposalStake)); await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee); @@ -55,14 +56,14 @@ describe('Runtime upgrade integration tests', () => { await apiWrapper.proposeRuntime( m1KeyPairs[0], proposalStake, - 'testing runtime', - 'runtime to test proposal functionality', + 'testing runtime' + uuid().substring(0, 8), + 'runtime to test proposal functionality' + uuid().substring(0, 8), runtime ); const proposalNumber = await proposalPromise; // Approving runtime update proposal - const runtimePromise = apiWrapper.expectRuntimeUpgraded(); + const runtimePromise = apiWrapper.expectProposalFinalized(); await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber); await runtimePromise; }).timeout(defaultTimeout); diff --git a/tests/network-tests/src/tests/constantinople/proposals/workingGroupMintCapacityProposalTest.ts b/tests/network-tests/src/tests/constantinople/proposals/workingGroupMintCapacityProposalTest.ts new file mode 100644 index 0000000000..dca6bfbaa2 --- /dev/null +++ b/tests/network-tests/src/tests/constantinople/proposals/workingGroupMintCapacityProposalTest.ts @@ -0,0 +1,83 @@ +import { initConfig } from '../../../utils/config'; +import { Keyring, WsProvider } from '@polkadot/api'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { membershipTest } from '../membershipCreationTest'; +import { councilTest } from '../electingCouncilTest'; +import { registerJoystreamTypes } from '@joystream/types'; +import { ApiWrapper } from '../../../utils/apiWrapper'; +import { v4 as uuid } from 'uuid'; +import BN = require('bn.js'); +import { assert } from 'chai'; + +describe('Working group mint capacity proposal network tests', () => { + initConfig(); + const keyring = new Keyring({ type: 'sr25519' }); + const nodeUrl: string = process.env.NODE_URL!; + const sudoUri: string = process.env.SUDO_ACCOUNT_URI!; + const mintingCapacityIncrement: BN = new BN(+process.env.MINTING_CAPACITY_INCREMENT!); + const defaultTimeout: number = 120000; + + const m1KeyPairs: KeyringPair[] = new Array(); + const m2KeyPairs: KeyringPair[] = new Array(); + + let apiWrapper: ApiWrapper; + let sudo: KeyringPair; + + before(async function () { + this.timeout(defaultTimeout); + registerJoystreamTypes(); + const provider = new WsProvider(nodeUrl); + apiWrapper = await ApiWrapper.create(provider); + }); + + membershipTest(m1KeyPairs); + membershipTest(m2KeyPairs); + councilTest(m1KeyPairs, m2KeyPairs); + + // TODO implement the test + it('Mint capacity proposal test', async () => { + // Setup + sudo = keyring.addFromUri(sudoUri); + const description: string = 'spending proposal which is used for API network testing'; + const runtimeVoteFee: BN = apiWrapper.estimateVoteForProposalFee(); + const initialMintingCapacity: BN = await apiWrapper.getWorkingGroupMintCapacity(); + + // Topping the balances + const proposalStake: BN = await apiWrapper.getRequiredProposalStake(25, 10000); + const runtimeProposalFee: BN = apiWrapper.estimateProposeWorkingGroupMintCapacityFee( + description, + description, + proposalStake, + initialMintingCapacity.add(mintingCapacityIncrement) + ); + await apiWrapper.transferBalance(sudo, m1KeyPairs[0].address, runtimeProposalFee.add(proposalStake)); + await apiWrapper.transferBalanceToAccounts(sudo, m2KeyPairs, runtimeVoteFee); + + // Proposal creation + const proposalPromise = apiWrapper.expectProposalCreated(); + await apiWrapper.proposeWorkingGroupMintCapacity( + m1KeyPairs[0], + 'testing mint capacity' + uuid().substring(0, 8), + 'mint capacity to test proposal functionality' + uuid().substring(0, 8), + proposalStake, + initialMintingCapacity.add(mintingCapacityIncrement) + ); + const proposalNumber = await proposalPromise; + + // Approving mint capacity proposal + const mintCapacityPromise = apiWrapper.expectProposalFinalized(); + await apiWrapper.batchApproveProposal(m2KeyPairs, proposalNumber); + await mintCapacityPromise; + const updatedMintingCapacity: BN = await apiWrapper.getWorkingGroupMintCapacity(); + assert( + updatedMintingCapacity.sub(initialMintingCapacity).eq(mintingCapacityIncrement), + `Content working group has unexpected minting capacity ${updatedMintingCapacity}, expected ${initialMintingCapacity.add( + mintingCapacityIncrement + )}` + ); + }).timeout(defaultTimeout); + + after(() => { + apiWrapper.close(); + }); +}); diff --git a/tests/network-tests/src/utils/apiWrapper.ts b/tests/network-tests/src/utils/apiWrapper.ts index 386a4420b4..d8e0fb21c2 100644 --- a/tests/network-tests/src/utils/apiWrapper.ts +++ b/tests/network-tests/src/utils/apiWrapper.ts @@ -1,9 +1,10 @@ import { ApiPromise, WsProvider } from '@polkadot/api'; -import { Option, Vec, Bytes } from '@polkadot/types'; +import { Option, Vec, Bytes, u32 } from '@polkadot/types'; import { Codec } from '@polkadot/types/types'; import { KeyringPair } from '@polkadot/keyring/types'; -import { UserInfo, PaidMembershipTerms } from '@joystream/types/lib/members'; -import { Seat, VoteKind } from '@joystream/types'; +import { UserInfo, PaidMembershipTerms, MemberId } from '@joystream/types/lib/members'; +import { Mint, MintId } from '@joystream/types/lib/mint'; +import { Seat } from '@joystream/types'; import { Balance, EventRecord } from '@polkadot/types/interfaces'; import BN = require('bn.js'); import { SubmittableExtrinsic } from '@polkadot/api/types'; @@ -41,8 +42,8 @@ export class ApiWrapper { ); } - public getMembership(address: string): Promise { - return this.api.query.members.memberIdsByControllerAccountId(address); + public getMemberIds(address: string): Promise { + return this.api.query.members.memberIdsByControllerAccountId>(address); } public getBalance(address: string): Promise { @@ -61,12 +62,11 @@ export class ApiWrapper { return this.getPaidMembershipTerms(paidTermsId).then(terms => terms.unwrap().fee.toBn()); } - public async transferBalanceToAccounts(from: KeyringPair, to: KeyringPair[], amount: BN): Promise { - return Promise.all( - to.map(async keyPair => { - await this.transferBalance(from, keyPair.address, amount); - }) - ); + public async transferBalanceToAccounts(from: KeyringPair, to: KeyringPair[], amount: BN): Promise { + for (let i = 0; i < to.length; i++) { + await this.transferBalance(from, to[i].address, amount); + } + return; } private getBaseTxFee(): BN { @@ -99,12 +99,42 @@ export class ApiWrapper { return this.estimateTxFee(this.api.tx.councilElection.reveal(hashedVote, nominee, salt)); } - public estimateProposeRuntimeUpgradeFee(stake: BN, name: string, description: string, runtime: Bytes): BN { - return this.estimateTxFee(this.api.tx.proposals.createProposal(stake, name, description, runtime)); + public estimateProposeRuntimeUpgradeFee(stake: BN, name: string, description: string, runtime: Bytes | string): BN { + return this.estimateTxFee( + this.api.tx.proposalsCodex.createRuntimeUpgradeProposal(stake, name, description, stake, runtime) + ); + } + + public estimateProposeTextFee(stake: BN, name: string, description: string, text: string): BN { + return this.estimateTxFee(this.api.tx.proposalsCodex.createTextProposal(stake, name, description, stake, text)); + } + + public estimateProposeSpendingFee( + title: string, + description: string, + stake: BN, + balance: BN, + destination: string + ): BN { + return this.estimateTxFee( + this.api.tx.proposalsCodex.createSpendingProposal(stake, title, description, stake, balance, destination) + ); + } + + public estimateProposeWorkingGroupMintCapacityFee(title: string, description: string, stake: BN, balance: BN): BN { + return this.estimateTxFee( + this.api.tx.proposalsCodex.createSetContentWorkingGroupMintCapacityProposal( + stake, + title, + description, + stake, + balance + ) + ); } public estimateVoteForProposalFee(): BN { - return this.estimateTxFee(this.api.tx.proposals.voteOnProposal(0, 'Approve')); + return this.estimateTxFee(this.api.tx.proposalsEngine.vote(0, 0, 'Approve')); } private applyForCouncilElection(account: KeyringPair, amount: BN): Promise { @@ -183,13 +213,21 @@ export class ApiWrapper { ); } + public sudoSetCouncilMintCapacity(sudo: KeyringPair, capacity: BN): Promise { + return this.sender.signAndSend( + this.api.tx.sudo.sudo(this.api.tx.council.setCouncilMintCapacity(capacity)), + sudo, + false + ); + } + public getBestBlock(): Promise { return this.api.derive.chain.bestNumber(); } public getCouncil(): Promise { return this.api.query.council.activeCouncil>().then(seats => { - return JSON.parse(seats.toString()); + return (seats as unknown) as Seat[]; }); } @@ -197,32 +235,82 @@ export class ApiWrapper { return this.api.query.substrate.code(); } - public proposeRuntime( + public async proposeRuntime( account: KeyringPair, stake: BN, name: string, description: string, - runtime: Bytes + runtime: Bytes | string + ): Promise { + const memberId: BN = (await this.getMemberIds(account.address))[0].toBn(); + return this.sender.signAndSend( + this.api.tx.proposalsCodex.createRuntimeUpgradeProposal(memberId, name, description, stake, runtime), + account, + false + ); + } + + public async proposeText( + account: KeyringPair, + stake: BN, + name: string, + description: string, + text: string + ): Promise { + const memberId: BN = (await this.getMemberIds(account.address))[0].toBn(); + return this.sender.signAndSend( + this.api.tx.proposalsCodex.createTextProposal(memberId, name, description, stake, text), + account, + false + ); + } + + public async proposeSpending( + account: KeyringPair, + title: string, + description: string, + stake: BN, + balance: BN, + destination: string ): Promise { + const memberId: BN = (await this.getMemberIds(account.address))[0].toBn(); return this.sender.signAndSend( - this.api.tx.proposals.createProposal(stake, name, description, runtime), + this.api.tx.proposalsCodex.createSpendingProposal(memberId, title, description, stake, balance, destination), account, false ); } - public approveProposal(account: KeyringPair, proposal: BN): Promise { + public async proposeWorkingGroupMintCapacity( + account: KeyringPair, + title: string, + description: string, + stake: BN, + balance: BN + ): Promise { + const memberId: BN = (await this.getMemberIds(account.address))[0].toBn(); return this.sender.signAndSend( - this.api.tx.proposals.voteOnProposal(proposal, new VoteKind('Approve')), + this.api.tx.proposalsCodex.createSetContentWorkingGroupMintCapacityProposal( + memberId, + title, + description, + stake, + balance + ), account, false ); } + public approveProposal(account: KeyringPair, memberId: BN, proposal: BN): Promise { + return this.sender.signAndSend(this.api.tx.proposalsEngine.vote(memberId, proposal, 'Approve'), account, false); + } + public batchApproveProposal(council: KeyringPair[], proposal: BN): Promise { return Promise.all( council.map(async keyPair => { - await this.approveProposal(keyPair, proposal); + const memberId: BN = (await this.getMemberIds(keyPair.address))[0].toBn(); + await this.approveProposal(keyPair, memberId, proposal); }) ); } @@ -235,7 +323,7 @@ export class ApiWrapper { return new Promise(async resolve => { await this.api.query.system.events>(events => { events.forEach(record => { - if (record.event.method.toString() === 'ProposalCreated') { + if (record.event.method && record.event.method.toString() === 'ProposalCreated') { resolve(new BN(record.event.data[1].toString())); } }); @@ -254,4 +342,46 @@ export class ApiWrapper { }); }); } + + public expectProposalFinalized(): Promise { + return new Promise(async resolve => { + await this.api.query.system.events>(events => { + events.forEach(record => { + if ( + record.event.method && + record.event.method.toString() === 'ProposalStatusUpdated' && + record.event.data[1].toString().includes('Executed') + ) { + resolve(); + } + }); + }); + }); + } + + public getTotalIssuance(): Promise { + return this.api.query.balances.totalIssuance(); + } + + public async getProposal(id: BN) { + const proposal = await this.api.query.proposalsEngine.proposals(id); + return; + } + + public async getRequiredProposalStake(numerator: number, denominator: number): Promise { + const issuance: number = await (await this.getTotalIssuance()).toNumber(); + const stake = (issuance * numerator) / denominator; + return new BN(stake.toFixed(0)); + } + + public getProposalCount(): Promise { + return this.api.query.proposalsEngine.proposalCount(); + } + + public async getWorkingGroupMintCapacity(): Promise { + const mintId = await this.api.query.contentWorkingGroup.mint(); + const mintCodec = await this.api.query.minting.mints(mintId); + const mint = (mintCodec[0] as unknown) as Mint; + return mint.getField('capacity'); + } } diff --git a/tests/network-tests/src/utils/sender.ts b/tests/network-tests/src/utils/sender.ts index b09e780cdc..b8e1c15499 100644 --- a/tests/network-tests/src/utils/sender.ts +++ b/tests/network-tests/src/utils/sender.ts @@ -37,7 +37,6 @@ export class Sender { ): Promise { return new Promise(async (resolve, reject) => { const nonce: BN = await this.getNonce(account.address); - // console.log('sending transaction from ' + account.address + ' with nonce ' + nonce); const signedTx = tx.sign(account, { nonce }); await signedTx .send(async result => { diff --git a/tests/network-tests/src/utils/utils.ts b/tests/network-tests/src/utils/utils.ts index 8f65093c3d..0f6cd79e65 100644 --- a/tests/network-tests/src/utils/utils.ts +++ b/tests/network-tests/src/utils/utils.ts @@ -1,7 +1,9 @@ import { IExtrinsic } from '@polkadot/types/types'; +import { Bytes } from '@polkadot/types'; import { compactToU8a, stringToU8a } from '@polkadot/util'; import { blake2AsHex } from '@polkadot/util-crypto'; import BN = require('bn.js'); +import fs = require('fs'); import { decodeAddress } from '@polkadot/keyring'; import { Seat } from '@joystream/types'; @@ -42,4 +44,8 @@ export class Utils { public static getTotalStake(seat: Seat): BN { return new BN(+seat.stake.toString() + seat.backers.reduce((a, baker) => a + +baker.stake.toString(), 0)); } + + public static readRuntimeFromFile(path: string): string { + return '0x' + fs.readFileSync(path).toString('hex'); + } }