From e1b9a6a5a7469c61fd6d65c1b316a84c72e0650f Mon Sep 17 00:00:00 2001 From: ut-akuznetsov <59873862+ut-akuznetsov@users.noreply.github.com> Date: Wed, 21 Dec 2022 14:30:48 +0000 Subject: [PATCH] Merge pull request #742 from UniqueNetwork/tests/vesting --- --- a/tests/src/app-promotion.test.ts +++ b/tests/src/app-promotion.test.ts @@ -56,7 +56,8 @@ // Staker balance is: miscFrozen: 100, feeFrozen: 100, reserved: 0n... // ...so he can not transfer 900 - expect (await helper.balance.getSubstrateFull(staker.address)).to.contain({miscFrozen: 100n * nominal, feeFrozen: 100n * nominal, reserved: 0n}); + expect(await helper.balance.getSubstrateFull(staker.address)).to.contain({miscFrozen: 100n * nominal, feeFrozen: 100n * nominal, reserved: 0n}); + expect(await helper.balance.getLocked(staker.address)).to.deep.eq([{id: 'appstake', amount: 100n * nominal, reasons: 'All'}]); await expect(helper.balance.transferToSubstrate(staker, recepient.address, 900n * nominal)).to.be.rejectedWith('balances.LiquidityRestrictions'); expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal); @@ -530,7 +531,7 @@ }); }); - describe('rewards', () => { + describe('payoutStakers', () => { itSub('can not be called by non admin', async ({helper}) => { const nonAdmin = accounts.pop()!; await expect(helper.admin.payoutStakers(nonAdmin, 100)).to.be.rejectedWith('appPromotion.NoPermission'); @@ -569,8 +570,14 @@ expect(payoutToStaker + 300n * nominal).to.equal(calculateIncome(300n * nominal)); const totalStakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address}); - expect(totalStakedPerBlock[0].amount).to.equal(calculateIncome(100n * nominal)); - expect(totalStakedPerBlock[1].amount).to.equal(calculateIncome(200n * nominal)); + const income1 = calculateIncome(100n * nominal); + const income2 = calculateIncome(200n * nominal); + expect(totalStakedPerBlock[0].amount).to.equal(income1); + expect(totalStakedPerBlock[1].amount).to.equal(income2); + + const stakerBalance = await helper.balance.getSubstrateFull(staker.address); + expect(stakerBalance).to.contain({miscFrozen: income1 + income2, feeFrozen: income1 + income2, reserved: 0n}); + expect(stakerBalance.free / nominal).to.eq(999n); }); itSub('shoud be paid for more than one period if payments was missed', async ({helper}) => { --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -2200,6 +2200,15 @@ } /** + * Get latest relay block + * @returns {number} relay block + */ + async getRelayBlockNumber(): Promise { + const blockNumber = (await this.helper.callRpc('api.query.parachainSystem.validationData')).toJSON().relayParentNumber; + return BigInt(blockNumber); + } + + /** * Get account nonce * @param address substrate address * @example getNonce("5GrwvaEF5zXb26Fz..."); @@ -2257,6 +2266,11 @@ const accountInfo = (await this.helper.callRpc('api.query.system.account', [address])).data; return {free: accountInfo.free.toBigInt(), miscFrozen: accountInfo.miscFrozen.toBigInt(), feeFrozen: accountInfo.feeFrozen.toBigInt(), reserved: accountInfo.reserved.toBigInt()}; } + + async getLocked(address: TSubstrateAccount): Promise<[{id: string, amount: bigint, reason: string}]> { + const locks = (await this.helper.callRpc('api.query.balances.locks', [address])).toHuman(); + return locks.map((lock: any) => {return {id: lock.id, amount: BigInt(lock.amount.replace(/,/g, '')), reasons: lock.reasons};}); + } } class EthereumBalanceGroup extends HelperGroup { @@ -2341,6 +2355,15 @@ } /** + * Get locked balances + * @param address substrate address + * @returns locked balances with reason via api.query.balances.locks + */ + getLocked(address: TSubstrateAccount) { + return this.subBalanceGroup.getLocked(address); + } + + /** * Get ethereum address balance * @param address ethereum address * @example getEthereum("0x9F0583DbB855d...") @@ -2380,6 +2403,52 @@ isSuccess = isSuccess && BigInt(amount) === transfer.amount; return isSuccess; } + + /** + * Transfer tokens with the unlock period + * @param signer signers Keyring + * @param address Substrate address of recipient + * @param schedule Schedule params + * @example vestedTransfer(signer, recepient.address, 20000, 100, 10, 50 * nominal); // total amount of vested tokens will be 100 * 50 = 5000 + */ + async vestedTransfer(signer: TSigner, address: TSubstrateAccount, schedule: {start: bigint, period: bigint, periodCount: bigint, perPeriod: bigint}): Promise { + const result = await this.helper.executeExtrinsic(signer, 'api.tx.vesting.vestedTransfer', [address, schedule]); + const event = result.result.events + .find(e => e.event.section === 'vesting' && + e.event.method === 'VestingScheduleAdded' && + e.event.data[0].toHuman() === signer.address); + if (!event) throw Error('Cannot find transfer in events'); + } + + /** + * Get schedule for recepient of vested transfer + * @param address Substrate address of recipient + * @returns + */ + async getVestingSchedules(address: TSubstrateAccount): Promise<{start: bigint, period: bigint, periodCount: bigint, perPeriod: bigint}[]> { + const schedule = (await this.helper.callRpc('api.query.vesting.vestingSchedules', [address])).toJSON(); + return schedule.map((schedule: any) => { + return { + start: BigInt(schedule.start), + period: BigInt(schedule.period), + periodCount: BigInt(schedule.periodCount), + perPeriod: BigInt(schedule.perPeriod), + }; + }); + } + + /** + * Claim vested tokens + * @param signer signers Keyring + */ + async claim(signer: TSigner) { + const result = await this.helper.executeExtrinsic(signer, 'api.tx.vesting.claim', []); + const event = result.result.events + .find(e => e.event.section === 'vesting' && + e.event.method === 'Claimed' && + e.event.data[0].toHuman() === signer.address); + if (!event) throw Error('Cannot find claim in events'); + } } class AddressGroup extends HelperGroup { --- /dev/null +++ b/tests/src/vesting.test.ts @@ -0,0 +1,153 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +import {IKeyringPair} from '@polkadot/types/types'; +import {itSub, usingPlaygrounds, expect} from './util'; + +describe('Vesting', () => { + let donor: IKeyringPair; + let nominal: bigint; + + before(async () => { + await usingPlaygrounds(async (helper, privateKey) => { + donor = await privateKey({filename: __filename}); + nominal = helper.balance.getOneTokenNominal(); + }); + }); + + itSub('can perform vestedTransfer and claim tokens', async ({helper}) => { + // arrange + const [sender, recepient] = await helper.arrange.createAccounts([1000n, 1n], donor); + const currentRelayBlock = await helper.chain.getRelayBlockNumber(); + const schedule1 = {start: currentRelayBlock + 4n, period: 4n, periodCount: 2n, perPeriod: 50n * nominal}; + const schedule2 = {start: currentRelayBlock + 8n, period: 8n, periodCount: 2n, perPeriod: 100n * nominal}; + + // act + await helper.balance.vestedTransfer(sender, recepient.address, schedule1); + await helper.balance.vestedTransfer(sender, recepient.address, schedule2); + let schedule = await helper.balance.getVestingSchedules(recepient.address); + + // check senders balance after vesting: + let balanceSender = await helper.balance.getSubstrateFull(sender.address); + expect(balanceSender.free / nominal).to.eq(699n); + expect(balanceSender.feeFrozen).to.eq(0n); + expect(balanceSender.miscFrozen).to.eq(0n); + expect(balanceSender.reserved).to.eq(0n); + + // check recepient balance after vesting: + let balanceRecepient = await helper.balance.getSubstrateFull(recepient.address); + expect(balanceRecepient.free).to.eq(301n * nominal); + expect(balanceRecepient.feeFrozen).to.eq(300n * nominal); + expect(balanceRecepient.miscFrozen).to.eq(300n * nominal); + expect(balanceRecepient.reserved).to.eq(0n); + + // Schedules list correct: + expect(schedule).to.has.length(2); + expect(schedule[0]).to.deep.eq(schedule1); + expect(schedule[1]).to.deep.eq(schedule2); + + await helper.wait.forRelayBlockNumber(currentRelayBlock + 8n); + await helper.balance.claim(recepient); + + // check recepient balance after claim (50 tokens claimed): + balanceRecepient = await helper.balance.getSubstrateFull(recepient.address); + expect(balanceRecepient.free / nominal).to.eq(300n); + expect(balanceRecepient.feeFrozen).to.eq(250n * nominal); + expect(balanceRecepient.miscFrozen).to.eq(250n * nominal); + expect(balanceRecepient.reserved).to.eq(0n); + + await helper.wait.forRelayBlockNumber(currentRelayBlock + 16n); + await helper.balance.claim(recepient); + + // check recepient balance after second claim (150 tokens claimed): + balanceRecepient = await helper.balance.getSubstrateFull(recepient.address); + expect(balanceRecepient.free / nominal).to.eq(300n); + expect(balanceRecepient.feeFrozen).to.eq(100n * nominal); + expect(balanceRecepient.miscFrozen).to.eq(100n * nominal); + expect(balanceRecepient.reserved).to.eq(0n); + + // Schedules list contain 1 vesting: + schedule = await helper.balance.getVestingSchedules(recepient.address); + expect(schedule).to.has.length(1); + expect(schedule[0]).to.deep.eq(schedule2); + + await helper.wait.forRelayBlockNumber(currentRelayBlock + 24n); + await helper.balance.claim(recepient); + + // check recepient balance after second claim (100 tokens claimed): + balanceRecepient = await helper.balance.getSubstrateFull(recepient.address); + expect(balanceRecepient.free / nominal).to.eq(300n); + expect(balanceRecepient.feeFrozen).to.eq(0n); + expect(balanceRecepient.miscFrozen).to.eq(0n); + expect(balanceRecepient.reserved).to.eq(0n); + + // check sender balance does not changed: + balanceSender = await helper.balance.getSubstrateFull(sender.address); + expect(balanceSender.free / nominal).to.eq(699n); + expect(balanceSender.feeFrozen).to.eq(0n); + expect(balanceSender.miscFrozen).to.eq(0n); + expect(balanceSender.reserved).to.eq(0n); + }); + + itSub('cannot send more tokens than have', async ({helper}) => { + const [sender, receiver] = await helper.arrange.createAccounts([1000n, 1n], donor); + const schedule = {start: 0n, period: 1n, periodCount: 1n, perPeriod: 100n * nominal}; + const manyPeriodsSchedule = {start: 0n, period: 1n, periodCount: 100n, perPeriod: 10n * nominal}; + const oneBigSumSchedule = {start: 0n, period: 1n, periodCount: 1n, perPeriod: 5000n * nominal}; + + // Sender cannot send vestedTransfer to self or other + await expect(helper.balance.vestedTransfer(sender, sender.address, manyPeriodsSchedule)).to.be.rejectedWith(/InsufficientBalance/); + await expect(helper.balance.vestedTransfer(sender, receiver.address, manyPeriodsSchedule)).to.be.rejectedWith(/InsufficientBalance/); + await expect(helper.balance.vestedTransfer(sender, sender.address, oneBigSumSchedule)).to.be.rejectedWith(/InsufficientBalance/); + await expect(helper.balance.vestedTransfer(sender, receiver.address, oneBigSumSchedule)).to.be.rejectedWith(/InsufficientBalance/); + + const balanceSender = await helper.balance.getSubstrateFull(sender.address); + const balanceReceiver = await helper.balance.getSubstrateFull(receiver.address); + + // Sender's balance has not changed + expect(balanceSender.free / nominal).to.eq(999n); + expect(balanceSender.feeFrozen).to.eq(0n); + expect(balanceSender.miscFrozen).to.eq(0n); + expect(balanceSender.reserved).to.eq(0n); + + // Receiver's balance has not changed + expect(balanceReceiver.free).to.be.eq(1n * nominal); + expect(balanceReceiver.feeFrozen).to.be.eq(0n); + expect(balanceReceiver.miscFrozen).to.be.eq(0n); + expect(balanceReceiver.reserved).to.be.eq(0n); + + // Receiver cannot send vestedTransfer back because of freeze + await expect(helper.balance.vestedTransfer(receiver, sender.address, schedule)).to.be.rejectedWith(/InsufficientBalance/); + }); + + itSub('cannot send vestedTransfer with incorrect parameters', async ({helper}) => { + const [sender, receiver] = await helper.arrange.createAccounts([1000n, 1n], donor); + const incorrectperiodSchedule = {start: 0n, period: 0n, periodCount: 10n, perPeriod: 10n * nominal}; + const incorrectPeriodCountSchedule = {start: 0n, period: 1n, periodCount: 0n, perPeriod: 10n * nominal}; + const incorrectPerPeriodSchedule = {start: 0n, period: 1n, periodCount: 1n, perPeriod: 0n * nominal}; + + await expect(helper.balance.vestedTransfer(sender, sender.address, incorrectperiodSchedule)).to.be.rejectedWith(/vesting.ZeroVestingPeriod/); + await expect(helper.balance.vestedTransfer(sender, receiver.address, incorrectPeriodCountSchedule)).to.be.rejectedWith(/vesting.ZeroVestingPeriod/); + await expect(helper.balance.vestedTransfer(sender, receiver.address, incorrectPerPeriodSchedule)).to.be.rejectedWith(/vesting.AmountLow/); + + const balanceSender = await helper.balance.getSubstrateFull(sender.address); + // Sender's balance has not changed + expect(balanceSender.free / nominal).to.eq(999n); + expect(balanceSender.feeFrozen).to.eq(0n); + expect(balanceSender.miscFrozen).to.eq(0n); + expect(balanceSender.reserved).to.eq(0n); + }); +}); -- gitstuff