difftreelog
fix untstake
in: master
4 files changed
client/rpc/src/lib.rsdiffbeforeafterboth--- a/client/rpc/src/lib.rs
+++ b/client/rpc/src/lib.rs
@@ -418,47 +418,27 @@
}
}
-pub struct Unique<C, P> {
- client: Arc<C>,
- _marker: std::marker::PhantomData<P>,
-}
-
-impl<C, P> Unique<C, P> {
- pub fn new(client: Arc<C>) -> Self {
- Self {
- client,
- _marker: Default::default(),
+macro_rules! define_struct_for_server_api {
+ ($name:ident) => {
+ pub struct $name<C, P> {
+ client: Arc<C>,
+ _marker: std::marker::PhantomData<P>,
}
- }
-}
-
-pub struct AppPromotion<C, P> {
- client: Arc<C>,
- _marker: std::marker::PhantomData<P>,
-}
-
-impl<C, P> AppPromotion<C, P> {
- pub fn new(client: Arc<C>) -> Self {
- Self {
- client,
- _marker: Default::default(),
+
+ impl<C, P> $name<C, P> {
+ pub fn new(client: Arc<C>) -> Self {
+ Self {
+ client,
+ _marker: Default::default(),
+ }
+ }
}
- }
-}
-
-pub struct Rmrk<C, P> {
- client: Arc<C>,
- _marker: std::marker::PhantomData<P>,
+ };
}
-impl<C, P> Rmrk<C, P> {
- pub fn new(client: Arc<C>) -> Self {
- Self {
- client,
- _marker: Default::default(),
- }
- }
-}
+define_struct_for_server_api!(Unique);
+define_struct_for_server_api!(AppPromotion);
+define_struct_for_server_api!(Rmrk);
macro_rules! pass_method {
(
@@ -615,14 +595,14 @@
|v| v
.into_iter()
.map(|(b, a)| (b, a.to_string()))
- .collect::<Vec<_>>(), unique_api);
- pass_method!(total_staking_locked(staker: CrossAccountId) -> String => |v| v.to_string(), unique_api);
- pass_method!(pending_unstake(staker: Option<CrossAccountId>) -> String => |v| v.to_string(), unique_api);
+ .collect::<Vec<_>>(), app_promotion_api);
+ pass_method!(total_staking_locked(staker: CrossAccountId) -> String => |v| v.to_string(), app_promotion_api);
+ pass_method!(pending_unstake(staker: Option<CrossAccountId>) -> String => |v| v.to_string(), app_promotion_api);
pass_method!(pending_unstake_per_block(staker: CrossAccountId) -> Vec<(BlockNumber, String)> =>
|v| v
.into_iter()
.map(|(b, a)| (b, a.to_string()))
- .collect::<Vec<_>>(), unique_api);
+ .collect::<Vec<_>>(), app_promotion_api);
}
#[allow(deprecated)]
node/rpc/src/lib.rsdiffbeforeafterboth--- a/node/rpc/src/lib.rs
+++ b/node/rpc/src/lib.rs
@@ -38,7 +38,6 @@
use sp_block_builder::BlockBuilder;
use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata};
use sc_service::TransactionPool;
-use uc_rpc::AppPromotion;
use std::{collections::BTreeMap, sync::Arc};
use up_common::types::opaque::{Hash, AccountId, RuntimeInstance, Index, Block, BlockNumber, Balance};
@@ -237,7 +236,7 @@
io.merge(Unique::new(client.clone()).into_rpc())?;
- // #[cfg(not(feature = "unique-runtime"))]
+ #[cfg(not(any(feature = "unique-runtime", feature = "quartz-runtime")))]
io.merge(AppPromotion::new(client.clone()).into_rpc())?;
#[cfg(not(feature = "unique-runtime"))]
tests/src/app-promotion.test.tsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import {19 normalizeAccountId,20 getModuleNames,21 Pallets,22} from './util/helpers';2324import chai from 'chai';25import chaiAsPromised from 'chai-as-promised';26import {usingPlaygrounds} from './util/playgrounds';2728import {encodeAddress} from '@polkadot/util-crypto';29import {stringToU8a} from '@polkadot/util';30import {ApiPromise} from '@polkadot/api';31import {SponsoringMode, contractHelpers, createEthAccountWithBalance, deployFlipper, itWeb3, transferBalanceToEth} from './eth/util/helpers';32chai.use(chaiAsPromised);33const expect = chai.expect;3435let alice: IKeyringPair;36let palletAdmin: IKeyringPair;37let nominal: bigint;38let promotionStartBlock: number | null = null;39const palletAddress = calculatePalleteAddress('appstake');40let accounts: IKeyringPair[] = [];4142before(async function () {43 await usingPlaygrounds(async (helper, privateKeyWrapper) => {44 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();45 alice = privateKeyWrapper('//Alice');46 palletAdmin = privateKeyWrapper('//Charlie'); // TODO use custom address47 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})));48 nominal = helper.balance.getOneTokenNominal();49 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 1000n * nominal);50 await helper.balance.transferToSubstrate(alice, palletAddress, 1000n * nominal);51 if (!promotionStartBlock) {52 promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();53 }54 accounts = await helper.arrange.createCrowd(100, 1000n, alice); // create accounts-pool to speed up tests55 });56});5758after(async function () {59 await usingPlaygrounds(async (helper) => {60 });61});6263describe('app-promotions.stake extrinsic', () => {64 it('should "lock" staking balance, add it to "staked" map, and increase "totalStaked" amount', async () => {65 await usingPlaygrounds(async (helper) => {66 const [staker, recepient] = [accounts.pop()!, accounts.pop()!];67 const totalStakedBefore = await helper.staking.getTotalStaked();6869 // Minimum stake amount is 100:70 await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.eventually.rejected;71 await helper.staking.stake(staker, 100n * nominal);7273 // Staker balance is: miscFrozen: 100, feeFrozen: 100, reserved: 0n...74 // ...so he can not transfer 90075 expect (await helper.balance.getSubstrateFull(staker.address)).to.contain({miscFrozen: 100n * nominal, feeFrozen: 100n * nominal, reserved: 0n});76 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 900n * nominal)).to.be.rejected;77 78 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);79 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);80 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);81 // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 82 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased 8384 85 await helper.staking.stake(staker, 200n * nominal);86 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(300n * nominal);87 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(300n * nominal);88 expect((await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1])).to.be.deep.equal([100n * nominal, 200n * nominal]);89 });90 });9192 it('should allow to stake with nonce', async () => {93 await usingPlaygrounds(async (helper) => {94 const staker = accounts.pop()!;95 const transactions = [];96 for (let nonce = 0; nonce < 9; nonce++) {97 transactions.push(helper.signTransaction(staker, helper.api?.tx.promotion.stake(100n * nominal), 'Staker stakes with nonce', {nonce}));98 }99 await Promise.allSettled(transactions);100101 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(900n * nominal);102 });103 });104 105 it('should reject transaction if stake amount is more than total free balance minus frozen', async () => {106 await usingPlaygrounds(async helper => { 107 const staker = accounts.pop()!;108109 // Can't stake full balance because Alice needs to pay some fee110 await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.eventually.rejected;111 await helper.staking.stake(staker, 500n * nominal);112113 // Can't stake 500 tkn because Alice has Less than 500 transferable;114 await expect(helper.staking.stake(staker, 500n * nominal)).to.be.eventually.rejected; 115 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(500n * nominal);116 });117 });118 119 it('for different accounts in one block is possible', async () => {120 await usingPlaygrounds(async helper => {121 const crowd = [accounts.pop()!, accounts.pop()!, accounts.pop()!, accounts.pop()!];122 123 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));124 await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;125126 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));127 expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);128 });129 });130131 it('should allow to create maximum 10 stakes for account', async () => {132133 });134});135136describe('unstake balance extrinsic', () => { 137 it('should change balance state from "frozen" to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {138 await usingPlaygrounds(async helper => {139 const [staker, recepient] = [accounts.pop()!, accounts.pop()!];140 const totalStakedBefore = await helper.staking.getTotalStaked();141 await helper.staking.stake(staker, 900n * nominal);142 await helper.staking.unstake(staker);143144 // Right after unstake balance is reserved145 // Staker can not transfer 146 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 900n * nominal, miscFrozen: 0n, feeFrozen: 0n});147 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejected;148 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(900n * nominal);149 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);150 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);151 });152 });153154 it('should unlock balance after unlocking period ends and subtract it from "pendingUnstake"', async () => {155 await usingPlaygrounds(async (helper) => {156 const staker = accounts.pop()!;157 await helper.staking.stake(staker, 100n * nominal);158 await helper.staking.unstake(staker);159160 // Wait for unstaking period. Balance now free ~1000; reserved, frozen, miscFrozeb: 0n161 await waitForRelayBlock(helper.api!, 20);162 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n * nominal, miscFrozen: 0n, feeFrozen: 0n});163 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);164165 // staker can transfer:166 await helper.balance.transferToSubstrate(staker, alice.address, 998n * nominal);167 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(1n);168 });169 });170171 it('should successfully unstake multiple stakes', async () => {172 await usingPlaygrounds(async helper => {173 const staker = accounts.pop()!;174 await helper.staking.stake(staker, 100n * nominal);175 await helper.staking.stake(staker, 200n * nominal);176 await helper.staking.stake(staker, 300n * nominal);177178 // staked: [100, 200, 300]; unstaked: 0179 let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});180 let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);181 let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);182 expect(pendingUnstake).to.be.deep.equal(0n);183 expect(unstakedPerBlock).to.be.deep.equal([]);184 expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal, 300n * nominal]);185 186 // Can unstake multiple stakes187 await helper.staking.unstake(staker);188 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});189 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);190 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);191 expect(pendingUnstake).to.be.equal(600n * nominal);192 expect(stakedPerBlock).to.be.deep.equal([]);193 expect(unstakedPerBlock).to.be.deep.equal([600n * nominal]);194195 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 600n * nominal, miscFrozen: 600n * nominal});196 await waitForRelayBlock(helper.api!, 20);197 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});198 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);199 });200 });201202 it('should not have any effects if no active stakes', async () => {203 await usingPlaygrounds(async (helper) => {204 const staker = accounts.pop()!;205 206 // unstake has no effect if no stakes at all207 await helper.staking.unstake(staker);208 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);209 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(0n);210 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper211212 // TODO stake() unstake() waitUnstaked() unstake();213214 // can't unstake if there are only pendingUnstakes215 await helper.staking.stake(staker, 100n * nominal);216 await helper.staking.unstake(staker);217 await helper.staking.unstake(staker);218219 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);220 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);221 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);222 });223 });224225 it('should keep different unlocking block for each unlocking stake', async () => {226 await usingPlaygrounds(async (helper) => {227 const staker = accounts.pop()!;228 await helper.staking.stake(staker, 100n * nominal);229 await helper.staking.unstake(staker);230 await helper.staking.stake(staker, 120n * nominal);231 await helper.staking.unstake(staker);232233 const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});234 expect(unstakingPerBlock).has.length(2);235 expect(unstakingPerBlock[0][1]).to.equal(100n * nominal);236 expect(unstakingPerBlock[1][1]).to.equal(120n * nominal);237 });238 });239240 it('should be possible for different accounts in one block', async () => {241 await usingPlaygrounds(async (helper) => {242 const stakers = [accounts.pop()!, accounts.pop()!, accounts.pop()!];243244 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));245 await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));246247 await Promise.all(stakers.map(async (staker) => {248 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);249 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);250 }));251 });252 });253254 // TODO for different accounts in one block is possible255});256257describe('Admin adress', () => {258 it('can be set by sudo only', async () => {259 await usingPlaygrounds(async (helper) => {260 const nonAdmin = accounts.pop()!;261 // nonAdmin can not set admin not from himself nor as a sudo262 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.setAdminAddress({Substrate: nonAdmin.address}))).to.be.eventually.rejected;263 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: nonAdmin.address})))).to.be.eventually.rejected;264265 // Alice can266 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;267 });268 });269 270 it('can be any valid CrossAccountId', async () => {271 // We are not going to set an eth address as a sponsor,272 // but we do want to check, it doesn't break anything;273 await usingPlaygrounds(async (helper) => {274 const account = accounts.pop()!;275 const ethAccount = helper.address.substrateToEth(account.address); 276 // Alice sets Ethereum address as a sudo. Then Substrate address back...277 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethAccount})))).to.be.eventually.fulfilled;278 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;279 280 // ...It doesn't break anything;281 const collection = await helper.nft.mintCollection(account, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});282 await expect(helper.signTransaction(account, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;283 });284 });285286 it('can be reassigned', async () => {287 await usingPlaygrounds(async (helper) => {288 const [oldAdmin, newAdmin, collectionOwner] = [accounts.pop()!, accounts.pop()!, accounts.pop()!];289 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});290 291 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;292 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;293 await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;294 295 await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;296 });297 });298});299300describe('App-promotion collection sponsoring', () => {301 before(async function () {302 await usingPlaygrounds(async (helper) => {303 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));304 await helper.signTransaction(alice, tx);305 });306 });307308 it('should actually sponsor transactions', async () => {309 await usingPlaygrounds(async (helper) => {310 const [collectionOwner, tokenSender, receiver] = [accounts.pop()!, accounts.pop()!, accounts.pop()!];311 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});312 const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});313 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));314 const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);315316 await token.transfer(tokenSender, {Substrate: receiver.address});317 expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});318 const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);319320 // senders balance the same, transaction has sponsored321 expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(1000n * nominal);322 expect (palletBalanceBefore > palletBalanceAfter).to.be.true;323 });324 });325326 it('can not be set by non admin', async () => {327 await usingPlaygrounds(async (helper) => {328 const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!];329330 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});331 332 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;333 expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');334 });335 });336337 it('should set pallet address as confirmed admin', async () => {338 await usingPlaygrounds(async (helper) => {339 const [collectionOwner, oldSponsor] = [accounts.pop()!, accounts.pop()!];340 341 // Can set sponsoring for collection without sponsor342 const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});343 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;344 expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});345346 // Can set sponsoring for collection with unconfirmed sponsor347 const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});348 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});349 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;350 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});351352 // Can set sponsoring for collection with confirmed sponsor353 const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});354 await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);355 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;356 expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});357 });358 });359360 it('can be overwritten by collection owner', async () => { 361 await usingPlaygrounds(async (helper) => {362 const [collectionOwner, newSponsor] = [accounts.pop()!, accounts.pop()!];363 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});364 const collectionId = collection.collectionId;365 366 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;367 368 // Collection limits still can be changed by the owner369 expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;370 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);371 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});372373 // Collection sponsor can be changed too374 expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;375 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});376 });377 });378 379 it('should not overwrite collection limits set by the owner earlier', async () => {380 await usingPlaygrounds(async (helper) => {381 const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};382 const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});383384 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;385 expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);386 });387 });388 389 it('should reject transaction if collection doesn\'t exist', async () => {390 await usingPlaygrounds(async (helper) => {391 const collectionOwner = accounts.pop()!;392 393 // collection has never existed394 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;395 // collection has been burned396 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});397 await collection.burn(collectionOwner);398399 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;400 });401 });402});403404describe('app-promotion stopSponsoringCollection', () => {405 it('can not be called by non-admin', async () => { 406 await usingPlaygrounds(async (helper) => {407 const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!];408 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});409 410 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;411 412 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;413 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});414 });415 });416417 it('should set sponsoring as disabled', async () => {418 await usingPlaygrounds(async (helper) => {419 const [collectionOwner, recepient] = [accounts.pop()!, accounts.pop()!];420 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}});421 const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address});422 423 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));424 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId));425 426 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');427428 // Transactions are not sponsored anymore:429 const ownerBalanceBefore = await helper.balance.getSubstrate(collectionOwner.address);430 await token.transfer(collectionOwner, {Substrate: recepient.address});431 const ownerBalanceAfter = await helper.balance.getSubstrate(collectionOwner.address);432 expect(ownerBalanceAfter < ownerBalanceBefore).to.be.equal(true);433 });434 });435436 it('should not affect collection which is not sponsored by pallete', async () => {437 await usingPlaygrounds(async (helper) => {438 const collectionOwner = accounts.pop()!;439 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});440 await collection.confirmSponsorship(collectionOwner);441 442 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;443 444 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});445 });446 });447448 it('should reject transaction if collection does not exist', async () => { 449 await usingPlaygrounds(async (helper) => {450 const collectionOwner = accounts.pop()!;451 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});452 453 await collection.burn(collectionOwner);454 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;455 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(999999999))).to.be.eventually.rejected;456 });457 });458});459460describe('app-promotion contract sponsoring', () => {461 itWeb3('should set palletes address as a sponsor', async ({api, web3, privateKeyWrapper}) => {462 await usingPlaygrounds(async (helper) => {463 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();464 const flipper = await deployFlipper(web3, contractOwner);465 const contractMethods = contractHelpers(web3, contractOwner);466467 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));468 469 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 470 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 471 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({472 confirmed: {473 substrate: palletAddress,474 },475 });476 });477 });478479 itWeb3('should overwrite sponsoring mode and existed sponsor', async ({api, web3, privateKeyWrapper}) => {480 await usingPlaygrounds(async (helper) => {481 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();482 const flipper = await deployFlipper(web3, contractOwner);483 const contractMethods = contractHelpers(web3, contractOwner);484485 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;486487 // Contract is self sponsored488 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({489 confirmed: {490 ethereum: flipper.options.address.toLowerCase(),491 },492 });493494 // set promotion sponsoring495 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));496497 // new sponsor is pallet address498 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 499 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 500 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({501 confirmed: {502 substrate: palletAddress,503 },504 });505 });506 });507508 itWeb3('can be overwritten by contract owner', async ({api, web3, privateKeyWrapper}) => {509 await usingPlaygrounds(async (helper) => { 510 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();511 const flipper = await deployFlipper(web3, contractOwner);512 const contractMethods = contractHelpers(web3, contractOwner);513514 // contract sponsored by pallet515 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));516517 // owner sets self sponsoring518 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;519520 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 521 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 522 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({523 confirmed: {524 ethereum: flipper.options.address.toLowerCase(),525 },526 });527 });528 });529530 itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {531 await usingPlaygrounds(async (helper) => { 532 const nonAdmin = accounts.pop()!;533 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();534 const flipper = await deployFlipper(web3, contractOwner);535 const contractMethods = contractHelpers(web3, contractOwner);536537 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;538539 // nonAdmin calls sponsorConract540 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.sponsorConract(flipper.options.address))).to.be.rejected;541542 // contract still self-sponsored 543 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({544 confirmed: {545 ethereum: flipper.options.address.toLowerCase(),546 },547 });548 });549550 itWeb3('should be rejected for non-contract address', async ({api, web3, privateKeyWrapper}) => {551 await usingPlaygrounds(async (helper) => {552553 });554 });555 });556557 itWeb3('should actually sponsor transactions', async ({api, web3, privateKeyWrapper}) => {558 await usingPlaygrounds(async (helper) => { 559 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);560 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();561 const flipper = await deployFlipper(web3, contractOwner);562 const contractHelper = contractHelpers(web3, contractOwner);563 await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});564 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});565 await transferBalanceToEth(api, alice, flipper.options.address, 1000n);566567 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));568 await flipper.methods.flip().send({from: caller});569 expect(await flipper.methods.getValue().call()).to.be.true;570571 const callerBalance = await helper.balance.getEthereum(caller);572 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);573574 expect(callerBalance).to.be.equal(1000n * nominal);575 expect(1000n * nominal > contractBalanceAfter).to.be.true;576 });577 });578});579580describe('app-promotion stopSponsoringContract', () => { 581 itWeb3('should remove pallet address from contract sponsors', async ({api, web3, privateKeyWrapper}) => {582 await usingPlaygrounds(async (helper) => { 583 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);584 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();585 const flipper = await deployFlipper(web3, contractOwner);586 await transferBalanceToEth(api, alice, flipper.options.address);587 const contractHelper = contractHelpers(web3, contractOwner);588 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});589 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));590 await helper.signTransaction(palletAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address));591592 expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false; 593 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 594 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({595 disabled: null,596 });597598 await flipper.methods.flip().send({from: caller});599 expect(await flipper.methods.getValue().call()).to.be.true;600601 const callerBalance = await helper.balance.getEthereum(caller);602 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);603604 // caller payed for call605 expect(1000n * nominal > callerBalance).to.be.true;606 expect(contractBalanceAfter).to.be.equal(1000n * nominal);607 });608 });609610 itWeb3('can not be called by non-admin', async ({api, web3, privateKeyWrapper}) => {611 await usingPlaygrounds(async (helper) => { 612 const nonAdmin = accounts.pop()!;613 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();614 const flipper = await deployFlipper(web3, contractOwner);615616 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));617 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;618 });619 });620621 itWeb3('should not affect a contract which is not sponsored by pallete', async ({api, web3, privateKeyWrapper}) => {622 await usingPlaygrounds(async (helper) => { 623 const nonAdmin = accounts.pop()!;624 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();625 const flipper = await deployFlipper(web3, contractOwner);626 const contractHelper = contractHelpers(web3, contractOwner);627 await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;628629 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;630 });631 });632});633634describe('app-promotion rewards', () => {635 it('can not be called by non admin', async () => {636 await usingPlaygrounds(async (helper) => {637 const nonAdmin = accounts.pop()!;638 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.payoutStakers(50))).to.be.rejected;639 });640 });641642 it('should credit 0.05% for staking period', async () => { 643 // TODO flaky test644 await usingPlaygrounds(async helper => {645 const staker = accounts.pop()!;646 647 await helper.staking.stake(staker, 100n * nominal);648 await helper.staking.stake(staker, 200n * nominal);649 await waitForRelayBlock(helper.api!, 30);650 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));651652 const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);653 expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n)]);654 });655 });656657 it('shoud be paid for more than one period if payments was missed', async () => {658 await usingPlaygrounds(async (helper) => {659 const staker = accounts.pop()!;660661 await helper.staking.stake(staker, 100n * nominal);662 await helper.staking.stake(staker, 200n * nominal);663664 await waitForRelayBlock(helper.api!, 55);665 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));666 const stakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});667 expect(stakedPerBlock[0][1]).to.be.equal(calculateIncome(100n * nominal, 10n, 2));668 expect(stakedPerBlock[1][1]).to.be.equal(calculateIncome(200n * nominal, 10n, 2));669670 const stakerFullBalance = await helper.balance.getSubstrateFull(staker.address);671 const frozenBalanceShouldBe = calculateIncome(300n * nominal, 10n, 2);672673 expect(stakerFullBalance).to.contain({reserved: 0n, feeFrozen: frozenBalanceShouldBe, miscFrozen: frozenBalanceShouldBe});674 });675 });676 677 it('should not be credited for unstaked (reserved) balance', async () => {678 expect.fail('Test not implemented');679 await usingPlaygrounds(async helper => {680 const staker = accounts.pop()!;681 });682 });683 684 it('should bring compound interest', async () => {685 // TODO flaky test686 await usingPlaygrounds(async helper => {687 const staker = accounts.pop()!;688 689 await helper.staking.stake(staker, 100n * nominal);690 await helper.staking.stake(staker, 200n * nominal);691 await helper.staking.stake(staker, 300n * nominal);692 693 await waitForRelayBlock(helper.api!, 34);694 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));695 let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);696 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n), calculateIncome(300n * nominal, 10n)]);697 698 await waitForRelayBlock(helper.api!, 20);699 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(50));700 totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);701 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n, 2), calculateIncome(200n * nominal, 10n, 2), calculateIncome(300n * nominal, 10n, 2)]); 702 });703 });704705 it.skip('can handle 40.000 rewards', async () => {706 await usingPlaygrounds(async (helper) => {707 const [donor] = await helper.arrange.createAccounts([7_000_000n], alice);708 const crowdStakes = async () => {709 // each account in the crowd stakes 2 times710 const crowd = await helper.arrange.createCrowd(500, 300n, donor);711 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));712 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));713 // 714 };715716 for (let i = 0; i < 40; i++) {717 await crowdStakes();718 }719720 // TODO pay rewards for some period721 });722 });723});724725async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {726 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();727 return new Promise<void>(async (resolve, reject) => {728 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {729 // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);730 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {731 unsubscribe();732 resolve();733 }734 });735 });736}737738function calculatePalleteAddress(palletId: any) {739 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));740 return encodeAddress(address);741}742743function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {744 const DAY = 7200n;745 const ACCURACY = 1_000_000_000n;746 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;747 748 if (iter > 1) {749 return calculateIncome(income, calcPeriod, iter - 1);750 } else return income;751}1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617import {IKeyringPair} from '@polkadot/types/types';18import {19 normalizeAccountId,20 getModuleNames,21 Pallets,22} from './util/helpers';2324import chai from 'chai';25import chaiAsPromised from 'chai-as-promised';26import {usingPlaygrounds} from './util/playgrounds';2728import {encodeAddress} from '@polkadot/util-crypto';29import {stringToU8a} from '@polkadot/util';30import {ApiPromise} from '@polkadot/api';31import {SponsoringMode, contractHelpers, createEthAccountWithBalance, deployFlipper, itWeb3, transferBalanceToEth} from './eth/util/helpers';32chai.use(chaiAsPromised);33const expect = chai.expect;3435let alice: IKeyringPair;36let palletAdmin: IKeyringPair;37let nominal: bigint;38const palletAddress = calculatePalleteAddress('appstake');39let accounts: IKeyringPair[] = [];4041before(async function () {42 await usingPlaygrounds(async (helper, privateKeyWrapper) => {43 if (!getModuleNames(helper.api!).includes(Pallets.AppPromotion)) this.skip();44 alice = privateKeyWrapper('//Alice');45 palletAdmin = privateKeyWrapper('//Charlie'); // TODO use custom address46 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})));47 nominal = helper.balance.getOneTokenNominal();48 await helper.balance.transferToSubstrate(alice, palletAdmin.address, 1000n * nominal);49 await helper.balance.transferToSubstrate(alice, palletAddress, 1000n * nominal);50 if (!promotionStartBlock) {51 promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();52 }53 await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!)));54 accounts = await helper.arrange.createCrowd(100, 1000n, alice); // create accounts-pool to speed up tests55 });56});5758after(async function () {59 await usingPlaygrounds(async (helper) => {60 });61});6263describe('app-promotions.stake extrinsic', () => {64 it('should "lock" staking balance, add it to "staked" map, and increase "totalStaked" amount', async () => {65 await usingPlaygrounds(async (helper) => {66 const [staker, recepient] = [accounts.pop()!, accounts.pop()!];67 const totalStakedBefore = await helper.staking.getTotalStaked();6869 // Minimum stake amount is 100:70 await expect(helper.staking.stake(staker, 100n * nominal - 1n)).to.be.eventually.rejected;71 await helper.staking.stake(staker, 100n * nominal);7273 // Staker balance is: miscFrozen: 100, feeFrozen: 100, reserved: 0n...74 // ...so he can not transfer 90075 expect (await helper.balance.getSubstrateFull(staker.address)).to.contain({miscFrozen: 100n * nominal, feeFrozen: 100n * nominal, reserved: 0n});76 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 900n * nominal)).to.be.rejected;77 78 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);79 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(100n * nominal);80 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);81 // it is potentially flaky test. Promotion can credited some tokens. Maybe we need to use closeTo? 82 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore + 100n * nominal); // total tokens amount staked in app-promotion increased 8384 85 await helper.staking.stake(staker, 200n * nominal);86 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(300n * nominal);87 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(300n * nominal);88 expect((await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map((x) => x[1])).to.be.deep.equal([100n * nominal, 200n * nominal]);89 });90 });9192 it('should allow to create maximum 10 stakes for account', async () => {93 await usingPlaygrounds(async (helper) => {94 const [staker] = await helper.arrange.createAccounts([2000n], alice);95 console.log(staker.address);96 for (let i = 0; i < 10; i++) {97 await helper.staking.stake(staker, 100n * nominal);98 }99100 // can have 10 stakes101 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(1000n * nominal);102 expect(await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).to.have.length(10);103104 await expect(helper.staking.stake(staker, 100n * nominal)).to.be.rejected;105106 // After unstake can stake again107 await helper.staking.unstake(staker);108 await helper.staking.stake(staker, 100n * nominal);109 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.equal(100n * nominal);110 });111 });112 113 it('should reject transaction if stake amount is more than total free balance minus frozen', async () => {114 await usingPlaygrounds(async helper => { 115 const staker = accounts.pop()!;116117 // Can't stake full balance because Alice needs to pay some fee118 await expect(helper.staking.stake(staker, 1000n * nominal)).to.be.eventually.rejected;119 await helper.staking.stake(staker, 500n * nominal);120121 // Can't stake 500 tkn because Alice has Less than 500 transferable;122 await expect(helper.staking.stake(staker, 500n * nominal)).to.be.eventually.rejected; 123 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(500n * nominal);124 });125 });126 127 it('for different accounts in one block is possible', async () => {128 await usingPlaygrounds(async helper => {129 const crowd = [accounts.pop()!, accounts.pop()!, accounts.pop()!, accounts.pop()!];130 131 const crowdStartsToStake = crowd.map(user => helper.staking.stake(user, 100n * nominal));132 await expect(Promise.all(crowdStartsToStake)).to.be.eventually.fulfilled;133134 const crowdStakes = await Promise.all(crowd.map(address => helper.staking.getTotalStaked({Substrate: address.address})));135 expect(crowdStakes).to.deep.equal([100n * nominal, 100n * nominal, 100n * nominal, 100n * nominal]);136 });137 });138});139140describe('unstake balance extrinsic', () => { 141 it('should change balance state from "frozen" to "reserved", add it to "pendingUnstake" map, and subtract it from totalStaked', async () => {142 await usingPlaygrounds(async helper => {143 const [staker, recepient] = [accounts.pop()!, accounts.pop()!];144 const totalStakedBefore = await helper.staking.getTotalStaked();145 await helper.staking.stake(staker, 900n * nominal);146 await helper.staking.unstake(staker);147148 // Right after unstake balance is reserved149 // Staker can not transfer 150 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 900n * nominal, miscFrozen: 0n, feeFrozen: 0n});151 await expect(helper.balance.transferToSubstrate(staker, recepient.address, 100n * nominal)).to.be.rejected;152 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(900n * nominal);153 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);154 expect(await helper.staking.getTotalStaked()).to.be.equal(totalStakedBefore);155 });156 });157158 it('should unlock balance after unlocking period ends and remove it from "pendingUnstake"', async () => {159 // TODO Flaky test160 await usingPlaygrounds(async (helper) => {161 const staker = accounts.pop()!;162 await helper.staking.stake(staker, 100n * nominal);163 await helper.staking.unstake(staker);164165 // Wait for unstaking period. Balance now free ~1000; reserved, frozen, miscFrozeb: 0n166 await waitForRelayBlock(helper.api!, 20);167 expect(await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, miscFrozen: 0n, feeFrozen: 0n});168 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);169170 // staker can transfer:171 await helper.balance.transferToSubstrate(staker, alice.address, 998n * nominal);172 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(1n);173 });174 });175176 it('should successfully unstake multiple stakes', async () => {177 await usingPlaygrounds(async helper => {178 const staker = accounts.pop()!;179 await helper.staking.stake(staker, 100n * nominal);180 await helper.staking.stake(staker, 200n * nominal);181 await helper.staking.stake(staker, 300n * nominal);182183 // staked: [100, 200, 300]; unstaked: 0184 let pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});185 let unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);186 let stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);187 expect(pendingUnstake).to.be.deep.equal(0n);188 expect(unstakedPerBlock).to.be.deep.equal([]);189 expect(stakedPerBlock).to.be.deep.equal([100n * nominal, 200n * nominal, 300n * nominal]);190 191 // Can unstake multiple stakes192 await helper.staking.unstake(staker);193 pendingUnstake = await helper.staking.getPendingUnstake({Substrate: staker.address});194 unstakedPerBlock = (await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address})).map(stake => stake[1]);195 stakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(stake => stake[1]);196 expect(pendingUnstake).to.be.equal(600n * nominal);197 expect(stakedPerBlock).to.be.deep.equal([]);198 expect(unstakedPerBlock).to.be.deep.equal([600n * nominal]);199200 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 600n * nominal, feeFrozen: 0n, miscFrozen: 0n});201 await waitForRelayBlock(helper.api!, 20);202 expect (await helper.balance.getSubstrateFull(staker.address)).to.deep.contain({reserved: 0n, feeFrozen: 0n, miscFrozen: 0n});203 expect (await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n);204 });205 });206207 it('should not have any effects if no active stakes', async () => {208 await usingPlaygrounds(async (helper) => {209 const staker = accounts.pop()!;210 211 // unstake has no effect if no stakes at all212 await helper.staking.unstake(staker);213 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(0n);214 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(0n);215 expect(await helper.balance.getSubstrate(staker.address) / nominal).to.be.equal(999n); // TODO bigint closeTo helper216217 // TODO stake() unstake() waitUnstaked() unstake();218219 // can't unstake if there are only pendingUnstakes220 await helper.staking.stake(staker, 100n * nominal);221 await helper.staking.unstake(staker);222 await helper.staking.unstake(staker);223224 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);225 expect(await helper.staking.getTotalStakingLocked({Substrate: staker.address})).to.be.equal(100n * nominal);226 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);227 });228 });229230 it('should keep different unlocking block for each unlocking stake', async () => {231 await usingPlaygrounds(async (helper) => {232 const staker = accounts.pop()!;233 await helper.staking.stake(staker, 100n * nominal);234 await helper.staking.unstake(staker);235 await helper.staking.stake(staker, 120n * nominal);236 await helper.staking.unstake(staker);237238 const unstakingPerBlock = await helper.staking.getPendingUnstakePerBlock({Substrate: staker.address});239 expect(unstakingPerBlock).has.length(2);240 expect(unstakingPerBlock[0][1]).to.equal(100n * nominal);241 expect(unstakingPerBlock[1][1]).to.equal(120n * nominal);242 });243 });244245 it('should be possible for different accounts in one block', async () => {246 await usingPlaygrounds(async (helper) => {247 const stakers = [accounts.pop()!, accounts.pop()!, accounts.pop()!];248249 await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));250 await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));251252 await Promise.all(stakers.map(async (staker) => {253 expect(await helper.staking.getPendingUnstake({Substrate: staker.address})).to.be.equal(100n * nominal);254 expect(await helper.staking.getTotalStaked({Substrate: staker.address})).to.be.equal(0n);255 }));256 });257 });258259 // TODO for different accounts in one block is possible260});261262describe('Admin adress', () => {263 it('can be set by sudo only', async () => {264 await usingPlaygrounds(async (helper) => {265 const nonAdmin = accounts.pop()!;266 // nonAdmin can not set admin not from himself nor as a sudo267 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.setAdminAddress({Substrate: nonAdmin.address}))).to.be.eventually.rejected;268 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: nonAdmin.address})))).to.be.eventually.rejected;269270 // Alice can271 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;272 });273 });274 275 it('can be any valid CrossAccountId', async () => {276 // We are not going to set an eth address as a sponsor,277 // but we do want to check, it doesn't break anything;278 await usingPlaygrounds(async (helper) => {279 const account = accounts.pop()!;280 const ethAccount = helper.address.substrateToEth(account.address); 281 // Alice sets Ethereum address as a sudo. Then Substrate address back...282 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Ethereum: ethAccount})))).to.be.eventually.fulfilled;283 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address})))).to.be.eventually.fulfilled;284 285 // ...It doesn't break anything;286 const collection = await helper.nft.mintCollection(account, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});287 await expect(helper.signTransaction(account, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;288 });289 });290291 it('can be reassigned', async () => {292 await usingPlaygrounds(async (helper) => {293 const [oldAdmin, newAdmin, collectionOwner] = [accounts.pop()!, accounts.pop()!, accounts.pop()!];294 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});295 296 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(oldAdmin))))).to.be.eventually.fulfilled;297 await expect(helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress(normalizeAccountId(newAdmin))))).to.be.eventually.fulfilled;298 await expect(helper.signTransaction(oldAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;299 300 await expect(helper.signTransaction(newAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;301 });302 });303});304305describe('App-promotion collection sponsoring', () => {306 before(async function () {307 await usingPlaygrounds(async (helper) => {308 const tx = helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.setAdminAddress({Substrate: palletAdmin.address}));309 await helper.signTransaction(alice, tx);310 });311 });312313 it('should actually sponsor transactions', async () => {314 await usingPlaygrounds(async (helper) => {315 const [collectionOwner, tokenSender, receiver] = [accounts.pop()!, accounts.pop()!, accounts.pop()!];316 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'Name', description: 'Description', tokenPrefix: 'Prefix', limits: {sponsorTransferTimeout: 0}});317 const token = await collection.mintToken(collectionOwner, {Substrate: tokenSender.address});318 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));319 const palletBalanceBefore = await helper.balance.getSubstrate(palletAddress);320321 await token.transfer(tokenSender, {Substrate: receiver.address});322 expect (await token.getOwner()).to.be.deep.equal({Substrate: receiver.address});323 const palletBalanceAfter = await helper.balance.getSubstrate(palletAddress);324325 // senders balance the same, transaction has sponsored326 expect (await helper.balance.getSubstrate(tokenSender.address)).to.be.equal(1000n * nominal);327 expect (palletBalanceBefore > palletBalanceAfter).to.be.true;328 });329 });330331 it('can not be set by non admin', async () => {332 await usingPlaygrounds(async (helper) => {333 const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!];334335 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});336 337 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;338 expect((await collection.getData())?.raw.sponsorship).to.equal('Disabled');339 });340 });341342 it('should set pallet address as confirmed admin', async () => {343 await usingPlaygrounds(async (helper) => {344 const [collectionOwner, oldSponsor] = [accounts.pop()!, accounts.pop()!];345 346 // Can set sponsoring for collection without sponsor347 const collectionWithoutSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'No-sponsor', description: 'New Collection', tokenPrefix: 'Promotion'});348 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithoutSponsor.collectionId))).to.be.eventually.fulfilled;349 expect((await collectionWithoutSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});350351 // Can set sponsoring for collection with unconfirmed sponsor352 const collectionWithUnconfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Unconfirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});353 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: oldSponsor.address});354 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithUnconfirmedSponsor.collectionId))).to.be.eventually.fulfilled;355 expect((await collectionWithUnconfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});356357 // Can set sponsoring for collection with confirmed sponsor358 const collectionWithConfirmedSponsor = await helper.nft.mintCollection(collectionOwner, {name: 'Confirmed', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: oldSponsor.address});359 await collectionWithConfirmedSponsor.confirmSponsorship(oldSponsor);360 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithConfirmedSponsor.collectionId))).to.be.eventually.fulfilled;361 expect((await collectionWithConfirmedSponsor.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});362 });363 });364365 it('can be overwritten by collection owner', async () => { 366 await usingPlaygrounds(async (helper) => {367 const [collectionOwner, newSponsor] = [accounts.pop()!, accounts.pop()!];368 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});369 const collectionId = collection.collectionId;370 371 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionId))).to.be.eventually.fulfilled;372 373 // Collection limits still can be changed by the owner374 expect(await collection.setLimits(collectionOwner, {sponsorTransferTimeout: 0})).to.be.true;375 expect((await collection.getData())?.raw.limits.sponsorTransferTimeout).to.be.equal(0);376 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});377378 // Collection sponsor can be changed too379 expect((await collection.setSponsor(collectionOwner, newSponsor.address))).to.be.true;380 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Unconfirmed: newSponsor.address});381 });382 });383 384 it('should not overwrite collection limits set by the owner earlier', async () => {385 await usingPlaygrounds(async (helper) => {386 const limits = {ownerCanDestroy: true, ownerCanTransfer: true, sponsorTransferTimeout: 0};387 const collectionWithLimits = await helper.nft.mintCollection(alice, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits});388389 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collectionWithLimits.collectionId))).to.be.eventually.fulfilled;390 expect((await collectionWithLimits.getData())?.raw.limits).to.be.deep.contain(limits);391 });392 });393 394 it('should reject transaction if collection doesn\'t exist', async () => {395 await usingPlaygrounds(async (helper) => {396 const collectionOwner = accounts.pop()!;397 398 // collection has never existed399 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(999999999))).to.be.eventually.rejected;400 // collection has been burned401 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});402 await collection.burn(collectionOwner);403404 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.rejected;405 });406 });407});408409describe('app-promotion stopSponsoringCollection', () => {410 it('can not be called by non-admin', async () => { 411 await usingPlaygrounds(async (helper) => {412 const [collectionOwner, nonAdmin] = [accounts.pop()!, accounts.pop()!];413 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});414 415 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId))).to.be.eventually.fulfilled;416 417 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;418 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: palletAddress});419 });420 });421422 it('should set sponsoring as disabled', async () => {423 await usingPlaygrounds(async (helper) => {424 const [collectionOwner, recepient] = [accounts.pop()!, accounts.pop()!];425 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', limits: {sponsorTransferTimeout: 0}});426 const token = await collection.mintToken(collectionOwner, {Substrate: collectionOwner.address});427 428 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.sponsorCollection(collection.collectionId));429 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId));430 431 expect((await collection.getData())?.raw.sponsorship).to.be.equal('Disabled');432433 // Transactions are not sponsored anymore:434 const ownerBalanceBefore = await helper.balance.getSubstrate(collectionOwner.address);435 await token.transfer(collectionOwner, {Substrate: recepient.address});436 const ownerBalanceAfter = await helper.balance.getSubstrate(collectionOwner.address);437 expect(ownerBalanceAfter < ownerBalanceBefore).to.be.equal(true);438 });439 });440441 it('should not affect collection which is not sponsored by pallete', async () => {442 await usingPlaygrounds(async (helper) => {443 const collectionOwner = accounts.pop()!;444 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion', pendingSponsor: collectionOwner.address});445 await collection.confirmSponsorship(collectionOwner);446 447 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;448 449 expect((await collection.getData())?.raw.sponsorship).to.be.deep.equal({Confirmed: collectionOwner.address});450 });451 });452453 it('should reject transaction if collection does not exist', async () => { 454 await usingPlaygrounds(async (helper) => {455 const collectionOwner = accounts.pop()!;456 const collection = await helper.nft.mintCollection(collectionOwner, {name: 'New', description: 'New Collection', tokenPrefix: 'Promotion'});457 458 await collection.burn(collectionOwner);459 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(collection.collectionId))).to.be.eventually.rejected;460 await expect(helper.signTransaction(palletAdmin, helper.api!.tx.promotion.stopSponsoringCollection(999999999))).to.be.eventually.rejected;461 });462 });463});464465describe('app-promotion contract sponsoring', () => {466 itWeb3('should set palletes address as a sponsor', async ({api, web3, privateKeyWrapper}) => {467 await usingPlaygrounds(async (helper) => {468 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();469 const flipper = await deployFlipper(web3, contractOwner);470 const contractMethods = contractHelpers(web3, contractOwner);471472 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));473 474 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 475 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 476 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({477 confirmed: {478 substrate: palletAddress,479 },480 });481 });482 });483484 itWeb3('should overwrite sponsoring mode and existed sponsor', async ({api, web3, privateKeyWrapper}) => {485 await usingPlaygrounds(async (helper) => {486 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();487 const flipper = await deployFlipper(web3, contractOwner);488 const contractMethods = contractHelpers(web3, contractOwner);489490 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;491492 // Contract is self sponsored493 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.be.deep.equal({494 confirmed: {495 ethereum: flipper.options.address.toLowerCase(),496 },497 });498499 // set promotion sponsoring500 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));501502 // new sponsor is pallet address503 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 504 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 505 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({506 confirmed: {507 substrate: palletAddress,508 },509 });510 });511 });512513 itWeb3('can be overwritten by contract owner', async ({api, web3, privateKeyWrapper}) => {514 await usingPlaygrounds(async (helper) => { 515 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();516 const flipper = await deployFlipper(web3, contractOwner);517 const contractMethods = contractHelpers(web3, contractOwner);518519 // contract sponsored by pallet520 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));521522 // owner sets self sponsoring523 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;524525 expect(await contractMethods.methods.hasSponsor(flipper.options.address).call()).to.be.true; 526 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 527 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({528 confirmed: {529 ethereum: flipper.options.address.toLowerCase(),530 },531 });532 });533 });534535 itWeb3('can not be set by non admin', async ({api, web3, privateKeyWrapper}) => {536 await usingPlaygrounds(async (helper) => { 537 const nonAdmin = accounts.pop()!;538 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();539 const flipper = await deployFlipper(web3, contractOwner);540 const contractMethods = contractHelpers(web3, contractOwner);541542 await expect(contractMethods.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;543544 // nonAdmin calls sponsorConract545 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.sponsorConract(flipper.options.address))).to.be.rejected;546547 // contract still self-sponsored 548 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({549 confirmed: {550 ethereum: flipper.options.address.toLowerCase(),551 },552 });553 });554555 itWeb3('should be rejected for non-contract address', async ({api, web3, privateKeyWrapper}) => {556 await usingPlaygrounds(async (helper) => {557558 });559 });560 });561562 itWeb3('should actually sponsor transactions', async ({api, web3, privateKeyWrapper}) => {563 await usingPlaygrounds(async (helper) => { 564 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);565 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();566 const flipper = await deployFlipper(web3, contractOwner);567 const contractHelper = contractHelpers(web3, contractOwner);568 await contractHelper.methods.setSponsoringRateLimit(flipper.options.address, 0).send({from: contractOwner});569 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});570 await transferBalanceToEth(api, alice, flipper.options.address, 1000n);571572 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));573 await flipper.methods.flip().send({from: caller});574 expect(await flipper.methods.getValue().call()).to.be.true;575576 const callerBalance = await helper.balance.getEthereum(caller);577 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);578579 expect(callerBalance).to.be.equal(1000n * nominal);580 expect(1000n * nominal > contractBalanceAfter).to.be.true;581 });582 });583});584585describe('app-promotion stopSponsoringContract', () => { 586 itWeb3('should remove pallet address from contract sponsors', async ({api, web3, privateKeyWrapper}) => {587 await usingPlaygrounds(async (helper) => { 588 const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);589 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();590 const flipper = await deployFlipper(web3, contractOwner);591 await transferBalanceToEth(api, alice, flipper.options.address);592 const contractHelper = contractHelpers(web3, contractOwner);593 await contractHelper.methods.setSponsoringMode(flipper.options.address, SponsoringMode.Generous).send({from: contractOwner});594 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));595 await helper.signTransaction(palletAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address));596597 expect(await contractHelper.methods.hasSponsor(flipper.options.address).call()).to.be.false; 598 expect((await api.query.evmContractHelpers.owner(flipper.options.address)).toJSON()).to.be.equal(contractOwner); 599 expect((await api.query.evmContractHelpers.sponsoring(flipper.options.address)).toJSON()).to.deep.equal({600 disabled: null,601 });602603 await flipper.methods.flip().send({from: caller});604 expect(await flipper.methods.getValue().call()).to.be.true;605606 const callerBalance = await helper.balance.getEthereum(caller);607 const contractBalanceAfter = await helper.balance.getEthereum(flipper.options.address);608609 // caller payed for call610 expect(1000n * nominal > callerBalance).to.be.true;611 expect(contractBalanceAfter).to.be.equal(1000n * nominal);612 });613 });614615 itWeb3('can not be called by non-admin', async ({api, web3, privateKeyWrapper}) => {616 await usingPlaygrounds(async (helper) => { 617 const nonAdmin = accounts.pop()!;618 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();619 const flipper = await deployFlipper(web3, contractOwner);620621 await helper.signTransaction(palletAdmin, api.tx.promotion.sponsorConract(flipper.options.address));622 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;623 });624 });625626 itWeb3('should not affect a contract which is not sponsored by pallete', async ({api, web3, privateKeyWrapper}) => {627 await usingPlaygrounds(async (helper) => { 628 const nonAdmin = accounts.pop()!;629 const contractOwner = (await createEthAccountWithBalance(api, web3, privateKeyWrapper)).toLowerCase();630 const flipper = await deployFlipper(web3, contractOwner);631 const contractHelper = contractHelpers(web3, contractOwner);632 await expect(contractHelper.methods.selfSponsoredEnable(flipper.options.address).send()).to.be.not.rejected;633634 await expect(helper.signTransaction(nonAdmin, api.tx.promotion.stopSponsoringContract(flipper.options.address))).to.be.rejected;635 });636 });637});638639describe('app-promotion rewards', () => {640 it('can not be called by non admin', async () => {641 await usingPlaygrounds(async (helper) => {642 const nonAdmin = accounts.pop()!;643 await expect(helper.signTransaction(nonAdmin, helper.api!.tx.promotion.payoutStakers(100))).to.be.rejected;644 });645 });646647 it('should credit 0.05% for staking period', async () => { 648 // TODO flaky test649 await usingPlaygrounds(async helper => {650 const staker = accounts.pop()!;651 652 await helper.staking.stake(staker, 100n * nominal);653 await helper.staking.stake(staker, 200n * nominal);654 await waitForRelayBlock(helper.api!, 30);655 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(100));656657 const totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);658 expect(totalStakedPerBlock).to.be.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n)]);659 });660 });661662 it('shoud be paid for more than one period if payments was missed', async () => {663 // TODO flaky test664 await usingPlaygrounds(async (helper) => {665 const staker = accounts.pop()!;666667 await helper.staking.stake(staker, 100n * nominal);668 await helper.staking.stake(staker, 200n * nominal);669670 await waitForRelayBlock(helper.api!, 55);671 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(100));672 const stakedPerBlock = await helper.staking.getTotalStakedPerBlock({Substrate: staker.address});673 expect(stakedPerBlock[0][1]).to.be.equal(calculateIncome(100n * nominal, 10n, 2));674 expect(stakedPerBlock[1][1]).to.be.equal(calculateIncome(200n * nominal, 10n, 2));675676 const stakerFullBalance = await helper.balance.getSubstrateFull(staker.address);677 const frozenBalanceShouldBe = calculateIncome(300n * nominal, 10n, 2);678679 expect(stakerFullBalance).to.contain({reserved: 0n, feeFrozen: frozenBalanceShouldBe, miscFrozen: frozenBalanceShouldBe});680 });681 });682 683 it('should not be credited for unstaked (reserved) balance', async () => {684 await usingPlaygrounds(async helper => {685 // staker unstakes before rewards has been initialized686 const staker = accounts.pop()!;687 await helper.staking.stake(staker, 100n * nominal);688 await waitForRelayBlock(helper.api!, 40);689 await helper.staking.unstake(staker);690 691 // so he did not receive any rewards692 const totalBalanceBefore = await helper.balance.getSubstrate(staker.address);693 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(100));694 const totalBalanceAfter = await helper.balance.getSubstrate(staker.address);695696 expect(totalBalanceBefore).to.be.equal(totalBalanceAfter);697 });698 });699 700 it('should bring compound interest', async () => {701 // TODO flaky test702 await usingPlaygrounds(async helper => {703 const staker = accounts.pop()!;704 705 await helper.staking.stake(staker, 100n * nominal);706 await helper.staking.stake(staker, 200n * nominal);707 await helper.staking.stake(staker, 300n * nominal);708 709 await waitForRelayBlock(helper.api!, 34);710 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(100));711 let totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);712 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n), calculateIncome(200n * nominal, 10n), calculateIncome(300n * nominal, 10n)]);713 714 await waitForRelayBlock(helper.api!, 20);715 await helper.signTransaction(palletAdmin, helper.api!.tx.promotion.payoutStakers(100));716 totalStakedPerBlock = (await helper.staking.getTotalStakedPerBlock({Substrate: staker.address})).map(s => s[1]);717 expect(totalStakedPerBlock).to.deep.equal([calculateIncome(100n * nominal, 10n, 2), calculateIncome(200n * nominal, 10n, 2), calculateIncome(300n * nominal, 10n, 2)]); 718 });719 });720721 it.skip('can handle 40.000 rewards', async () => {722 await usingPlaygrounds(async (helper) => {723 const [donor] = await helper.arrange.createAccounts([7_000_000n], alice);724 const crowdStakes = async () => {725 // each account in the crowd stakes 2 times726 const crowd = await helper.arrange.createCrowd(500, 300n, donor);727 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));728 await Promise.all(crowd.map(account => helper.staking.stake(account, 100n * nominal)));729 // 730 };731732 for (let i = 0; i < 40; i++) {733 await crowdStakes();734 }735736 // TODO pay rewards for some period737 });738 });739});740741async function waitForRelayBlock(api: ApiPromise, blocks = 1): Promise<void> {742 const current_block = (await api.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();743 return new Promise<void>(async (resolve, reject) => {744 const unsubscribe = await api.query.parachainSystem.validationData(async (data) => {745 // console.log(`${current_block} || ${data.value.relayParentNumber.toNumber()}`);746 if (data.value.relayParentNumber.toNumber() - current_block >= blocks) {747 unsubscribe();748 resolve();749 }750 });751 });752}753754function calculatePalleteAddress(palletId: any) {755 const address = stringToU8a(('modl' + palletId).padEnd(32, '\0'));756 return encodeAddress(address);757}758759function calculateIncome(base: bigint, calcPeriod: bigint, iter = 0): bigint {760 const DAY = 7200n;761 const ACCURACY = 1_000_000_000n;762 const income = base + base * (ACCURACY * (calcPeriod * 5n) / (10_000n * DAY)) / ACCURACY ;763 764 if (iter > 1) {765 return calculateIncome(income, calcPeriod, iter - 1);766 } else return income;767}tests/src/interfaces/augment-api-query.tsdiffbeforeafterboth--- a/tests/src/interfaces/augment-api-query.ts
+++ b/tests/src/interfaces/augment-api-query.ts
@@ -6,7 +6,7 @@
import '@polkadot/api-base/types/storage';
import type { ApiTypes, AugmentedQuery, QueryableStorageEntry } from '@polkadot/api-base/types';
-import type { BTreeMap, Bytes, Option, U256, U8aFixed, Vec, bool, u128, u16, u32, u64 } from '@polkadot/types-codec';
+import type { BTreeMap, Bytes, Option, U256, U8aFixed, Vec, bool, u128, u16, u32, u64, u8 } from '@polkadot/types-codec';
import type { AnyNumber, ITuple } from '@polkadot/types-codec/types';
import type { AccountId32, H160, H256 } from '@polkadot/types/interfaces/runtime';
import type { CumulusPalletDmpQueueConfigData, CumulusPalletDmpQueuePageIndexData, CumulusPalletParachainSystemRelayStateSnapshotMessagingStateSnapshot, CumulusPalletXcmpQueueInboundChannelDetails, CumulusPalletXcmpQueueOutboundChannelDetails, CumulusPalletXcmpQueueQueueConfigData, EthereumBlock, EthereumLog, EthereumReceiptReceiptV3, EthereumTransactionTransactionV2, FpRpcTransactionStatus, FrameSupportWeightsPerDispatchClassU64, FrameSystemAccountInfo, FrameSystemEventRecord, FrameSystemLastRuntimeUpgradeInfo, FrameSystemPhase, OrmlVestingVestingSchedule, PalletBalancesAccountData, PalletBalancesBalanceLock, PalletBalancesReleases, PalletBalancesReserveData, PalletEvmAccountBasicCrossAccountIdRepr, PalletEvmContractHelpersSponsoringModeT, PalletNonfungibleItemData, PalletRefungibleItemData, PalletTransactionPaymentReleases, PalletTreasuryProposal, PalletUniqueSchedulerScheduledV3, PhantomTypeUpDataStructs, PolkadotCorePrimitivesOutboundHrmpMessage, PolkadotPrimitivesV2AbridgedHostConfiguration, PolkadotPrimitivesV2PersistedValidationData, PolkadotPrimitivesV2UpgradeRestriction, SpRuntimeDigest, SpTrieStorageProof, UpDataStructsCollection, UpDataStructsCollectionStats, UpDataStructsProperties, UpDataStructsPropertiesMapPropertyPermission, UpDataStructsPropertyPermission, UpDataStructsPropertyScope, UpDataStructsSponsorshipStateBasicCrossAccountIdRepr, UpDataStructsTokenChild } from '@polkadot/types/lookup';
@@ -513,10 +513,10 @@
promotion: {
admin: AugmentedQuery<ApiType, () => Observable<Option<AccountId32>>, []> & QueryableStorageEntry<ApiType, []>;
/**
- * Stores the address of the staker for which the last revenue recalculation was performed.
+ * Stores hash a record for which the last revenue recalculation was performed.
* If `None`, then recalculation has not yet been performed or calculations have been completed for all stakers.
**/
- lastCalcucaltedStaker: AugmentedQuery<ApiType, () => Observable<Option<AccountId32>>, []> & QueryableStorageEntry<ApiType, []>;
+ nextCalculatedRecord: AugmentedQuery<ApiType, () => Observable<Option<ITuple<[AccountId32, u32]>>>, []> & QueryableStorageEntry<ApiType, []>;
/**
* Next target block when interest is recalculated
**/
@@ -530,6 +530,10 @@
**/
staked: AugmentedQuery<ApiType, (arg1: AccountId32 | string | Uint8Array, arg2: u32 | AnyNumber | Uint8Array) => Observable<ITuple<[u128, u32]>>, [AccountId32, u32]> & QueryableStorageEntry<ApiType, [AccountId32, u32]>;
/**
+ * Amount of stakes for an Account
+ **/
+ stakesPerAccount: AugmentedQuery<ApiType, (arg: AccountId32 | string | Uint8Array) => Observable<u8>, [AccountId32]> & QueryableStorageEntry<ApiType, [AccountId32]>;
+ /**
* A block when app-promotion has started .I think this is redundant, because we only need `NextInterestBlock`.
**/
startBlock: AugmentedQuery<ApiType, () => Observable<u32>, []> & QueryableStorageEntry<ApiType, []>;