--- a/tests/src/util/playgrounds/index.ts +++ b/tests/src/util/playgrounds/index.ts @@ -7,16 +7,18 @@ import {Context} from 'mocha'; import config from '../../config'; import '../../interfaces/augment-api-events'; -import {DevUniqueHelper, SilentLogger, SilentConsole} from './unique.dev'; +import {ChainHelperBase} from './unique'; +import {ILogger} from './types'; +import {DevUniqueHelper, SilentLogger, SilentConsole, DevMoonbeamHelper, DevMoonriverHelper, DevAcalaHelper, DevKaruraHelper, DevRelayHelper} from './unique.dev'; chai.use(chaiAsPromised); export const expect = chai.expect; -export async function usingPlaygrounds(code: (helper: DevUniqueHelper, privateKey: (seed: string) => IKeyringPair) => Promise, url: string = config.substrateUrl) { +async function usingPlaygroundsGeneral(helperType: new(logger: ILogger) => T, url: string, code: (helper: T, privateKey: (seed: string) => IKeyringPair) => Promise) { const silentConsole = new SilentConsole(); silentConsole.enable(); - const helper = new DevUniqueHelper(new SilentLogger()); + const helper = new helperType(new SilentLogger()); try { await helper.connect(url); @@ -30,8 +32,33 @@ } } -usingPlaygrounds.atUrl = (url: string, code: (helper: DevUniqueHelper, privateKey: (seed: string) => IKeyringPair) => Promise) => { - return usingPlaygrounds(code, url); +export const usingPlaygrounds = (code: (helper: DevUniqueHelper, privateKey: (seed: string) => IKeyringPair) => Promise, url: string = config.substrateUrl) => { + return usingPlaygroundsGeneral(DevUniqueHelper, url, code); +}; + +// TODO specific type +export const usingStatemintPlaygrounds = async (url: string, code: (helper: DevUniqueHelper, privateKey: (seed: string) => IKeyringPair) => Promise) => { + return usingPlaygroundsGeneral(DevUniqueHelper, url, code); +}; + +export const usingRelayPlaygrounds = async (url: string, code: (helper: DevRelayHelper, privateKey: (seed: string) => IKeyringPair) => Promise) => { + return usingPlaygroundsGeneral(DevRelayHelper, url, code); +}; + +export const usingAcalaPlaygrounds = async (url: string, code: (helper: DevAcalaHelper, privateKey: (seed: string) => IKeyringPair) => Promise) => { + return usingPlaygroundsGeneral(DevAcalaHelper, url, code); +}; + +export const usingKaruraPlaygrounds = async (url: string, code: (helper: DevKaruraHelper, privateKey: (seed: string) => IKeyringPair) => Promise) => { + return usingPlaygroundsGeneral(DevAcalaHelper, url, code); +}; + +export const usingMoonbeamPlaygrounds = async (url: string, code: (helper: DevMoonbeamHelper, privateKey: (seed: string) => IKeyringPair) => Promise) => { + return usingPlaygroundsGeneral(DevMoonbeamHelper, url, code); +}; + +export const usingMoonriverPlaygrounds = async (url: string, code: (helper: DevMoonbeamHelper, privateKey: (seed: string) => IKeyringPair) => Promise) => { + return usingPlaygroundsGeneral(DevMoonriverHelper, url, code); }; export enum Pallets { --- a/tests/src/util/playgrounds/types.ts +++ b/tests/src/util/playgrounds/types.ts @@ -179,8 +179,41 @@ minimalBalance?: bigint, } +export interface MoonbeamAssetInfo { + location: any, + metadata: { + name: string, + symbol: string, + decimals: number, + isFrozen: boolean, + minimalBalance: bigint, + }, + existentialDeposit: bigint, + isSufficient: boolean, + unitsPerSecond: bigint, + numAssetsWeightHint: number, +} + +export interface AcalaAssetMetadata { + name: string, + symbol: string, + decimals: number, + minimalBalance: bigint, +} + +export interface DemocracyStandardAccountVote { + balance: bigint, + vote: { + aye: boolean, + conviction: number, + }, +} + export type TSubstrateAccount = string; export type TEthereumAccount = string; export type TApiAllowedListeners = 'connected' | 'disconnected' | 'error' | 'ready' | 'decorated'; export type TUniqueNetworks = 'opal' | 'quartz' | 'unique'; +export type TSiblingNetworkds = 'moonbeam' | 'moonriver' | 'acala' | 'karura' | 'westmint'; +export type TRelayNetworks = 'rococo' | 'westend'; +export type TNetworks = TUniqueNetworks | TSiblingNetworkds | TRelayNetworks; export type TSigner = IKeyringPair; // | 'string' --- a/tests/src/util/playgrounds/unique.dev.ts +++ b/tests/src/util/playgrounds/unique.dev.ts @@ -2,10 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import {mnemonicGenerate} from '@polkadot/util-crypto'; -import {UniqueHelper} from './unique'; -import {ApiPromise, WsProvider} from '@polkadot/api'; +import {UniqueHelper, MoonbeamHelper, ChainHelperBase, AcalaHelper, RelayHelper} from './unique'; +import {ApiPromise, Keyring, WsProvider} from '@polkadot/api'; import * as defs from '../../interfaces/definitions'; import {IKeyringPair} from '@polkadot/types/types'; +import {EventRecord} from '@polkadot/types/interfaces'; import {ICrossAccountId} from './types'; export class SilentLogger { @@ -52,7 +53,6 @@ console.warn = this.consoleWarn; } } - export class DevUniqueHelper extends UniqueHelper { /** @@ -108,6 +108,36 @@ } } +export class DevRelayHelper extends RelayHelper {} + +export class DevMoonbeamHelper extends MoonbeamHelper { + account: MoonbeamAccountGroup; + wait: WaitGroup; + + constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) { + options.helperBase = options.helperBase ?? DevMoonbeamHelper; + + super(logger, options); + this.account = new MoonbeamAccountGroup(this); + this.wait = new WaitGroup(this); + } +} + +export class DevMoonriverHelper extends DevMoonbeamHelper {} + +export class DevAcalaHelper extends AcalaHelper { + wait: WaitGroup; + + constructor(logger: { log: (msg: any, level: any) => void, level: any }, options: {[key: string]: any} = {}) { + options.helperBase = options.helperBase ?? DevAcalaHelper; + + super(logger, options); + this.wait = new WaitGroup(this); + } +} + +export class DevKaruraHelper extends DevAcalaHelper {} + class ArrangeGroup { helper: DevUniqueHelper; @@ -245,10 +275,43 @@ } } +class MoonbeamAccountGroup { + helper: MoonbeamHelper; + + _alithAccount: IKeyringPair; + _baltatharAccount: IKeyringPair; + _dorothyAccount: IKeyringPair; + + constructor(helper: MoonbeamHelper) { + this.helper = helper; + + const keyring = new Keyring({type: 'ethereum'}); + const alithPrivateKey = '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133'; + const baltatharPrivateKey = '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b'; + const dorothyPrivateKey = '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68'; + + this._alithAccount = keyring.addFromUri(alithPrivateKey, undefined, 'ethereum'); + this._baltatharAccount = keyring.addFromUri(baltatharPrivateKey, undefined, 'ethereum'); + this._dorothyAccount = keyring.addFromUri(dorothyPrivateKey, undefined, 'ethereum'); + } + + alithAccount() { + return this._alithAccount; + } + + baltatharAccount() { + return this._baltatharAccount; + } + + dorothyAccount() { + return this._dorothyAccount; + } +} + class WaitGroup { - helper: DevUniqueHelper; + helper: ChainHelperBase; - constructor(helper: DevUniqueHelper) { + constructor(helper: ChainHelperBase) { this.helper = helper; } @@ -296,6 +359,40 @@ }); }); } + + async event(maxBlocksToWait: number, eventSection: string, eventMethod: string) { + // eslint-disable-next-line no-async-promise-executor + const promise = new Promise(async (resolve) => { + const unsubscribe = await this.helper.getApi().rpc.chain.subscribeNewHeads(async header => { + const blockNumber = header.number.toHuman(); + const blockHash = header.hash; + const eventIdStr = `${eventSection}.${eventMethod}`; + const waitLimitStr = `wait blocks remaining: ${maxBlocksToWait}`; + + console.log(`[Block #${blockNumber}] Waiting for event \`${eventIdStr}\` (${waitLimitStr})`); + + const apiAt = await this.helper.getApi().at(blockHash); + const eventRecords = await apiAt.query.system.events(); + + const neededEvent = eventRecords.find(r => { + return r.event.section == eventSection && r.event.method == eventMethod; + }); + + if (neededEvent) { + unsubscribe(); + resolve(neededEvent); + } else if (maxBlocksToWait > 0) { + maxBlocksToWait--; + } else { + console.log(`Event \`${eventIdStr}\` is NOT found`); + + unsubscribe(); + resolve(null); + } + }); + }); + return promise; + } } class AdminGroup { --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -9,7 +9,7 @@ import {ApiInterfaceEvents, SignerOptions} from '@polkadot/api/types'; import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto'; import {IKeyringPair} from '@polkadot/types/types'; -import {IApiListeners, IBlock, IEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, IStakingInfo, ISchedulerOptions, ISubstrateBalance, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, TUniqueNetworks, IForeignAssetMetadata} from './types'; +import {IApiListeners, IBlock, IEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, IStakingInfo, ISchedulerOptions, ISubstrateBalance, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, IForeignAssetMetadata, TNetworks, MoonbeamAssetInfo, DemocracyStandardAccountVote, AcalaAssetMetadata} from './types'; export class CrossAccountId implements ICrossAccountId { Substrate?: TSubstrateAccount; @@ -308,19 +308,25 @@ } } -class ChainHelperBase { +export class ChainHelperBase { + helperBase: any; + transactionStatus = UniqueUtil.transactionStatus; chainLogType = UniqueUtil.chainLogType; util: typeof UniqueUtil; eventHelper: typeof UniqueEventHelper; logger: ILogger; api: ApiPromise | null; - forcedNetwork: TUniqueNetworks | null; - network: TUniqueNetworks | null; + forcedNetwork: TNetworks | null; + network: TNetworks | null; chainLog: IUniqueHelperLog[]; children: ChainHelperBase[]; + address: AddressGroup; + chain: ChainGroup; + + constructor(logger?: ILogger, helperBase?: any) { + this.helperBase = helperBase; - constructor(logger?: ILogger) { this.util = UniqueUtil; this.eventHelper = UniqueEventHelper; if (typeof logger == 'undefined') logger = this.util.getDefaultLogger(); @@ -330,6 +336,21 @@ this.network = null; this.chainLog = []; this.children = []; + this.address = new AddressGroup(this); + this.chain = new ChainGroup(this); + } + + clone(helperCls: ChainHelperBaseConstructor, options: {[key: string]: any} = {}) { + Object.setPrototypeOf(helperCls.prototype, this); + const newHelper = new helperCls(this.logger, options); + + newHelper.api = this.api; + newHelper.network = this.network; + newHelper.forceNetwork = this.forceNetwork; + + this.children.push(newHelper); + + return newHelper; } getApi(): ApiPromise { @@ -341,7 +362,7 @@ this.chainLog = []; } - forceNetwork(value: TUniqueNetworks): void { + forceNetwork(value: TNetworks): void { this.forcedNetwork = value; } @@ -367,13 +388,17 @@ this.network = null; } - static async detectNetwork(api: ApiPromise): Promise { + static async detectNetwork(api: ApiPromise): Promise { const spec = (await api.query.system.lastRuntimeUpgrade()).toJSON() as any; + const xcmChains = ['rococo', 'westend', 'westmint', 'acala', 'karura', 'moonbeam', 'moonriver']; + + if(xcmChains.indexOf(spec.specName) > -1) return spec.specName; + if(['quartz', 'unique'].indexOf(spec.specName) > -1) return spec.specName; return 'opal'; } - static async detectNetworkByWsEndpoint(wsEndpoint: string): Promise { + static async detectNetworkByWsEndpoint(wsEndpoint: string): Promise { const api = new ApiPromise({provider: new WsProvider(wsEndpoint)}); await api.isReady; @@ -384,10 +409,11 @@ return network; } - static async createConnection(wsEndpoint: string, listeners?: IApiListeners, network?: TUniqueNetworks | null): Promise<{ + static async createConnection(wsEndpoint: string, listeners?: IApiListeners, network?: TNetworks | null): Promise<{ api: ApiPromise; - network: TUniqueNetworks; + network: TNetworks; }> { + console.log('createConnection network = ', network); if(typeof network === 'undefined' || network === null) network = 'opal'; const supportedRPC = { opal: { @@ -399,6 +425,13 @@ unique: { unique: require('@unique-nft/unique-mainnet-types/definitions').unique.rpc, }, + rococo: {}, + westend: {}, + moonbeam: {}, + moonriver: {}, + acala: {}, + karura: {}, + westmint: {}, }; if(!supportedRPC.hasOwnProperty(network)) network = await this.detectNetworkByWsEndpoint(wsEndpoint); const rpc = supportedRPC[network]; @@ -590,16 +623,16 @@ } -class HelperGroup { - helper: UniqueHelper; +class HelperGroup { + helper: T; - constructor(uniqueHelper: UniqueHelper) { + constructor(uniqueHelper: T) { this.helper = uniqueHelper; } } -class CollectionGroup extends HelperGroup { +class CollectionGroup extends HelperGroup { /** * Get number of blocks when sponsored transaction is available. * @@ -2000,7 +2033,7 @@ } -class ChainGroup extends HelperGroup { +class ChainGroup extends HelperGroup { /** * Get system properties of a chain * @example getChainProperties(); @@ -2054,10 +2087,106 @@ } } +class SubstrateBalanceGroup extends HelperGroup { + /** + * Get substrate address balance + * @param address substrate address + * @example getSubstrate("5GrwvaEF5zXb26Fz...") + * @returns amount of tokens on address + */ + async getSubstrate(address: TSubstrateAccount): Promise { + return (await this.helper.callRpc('api.query.system.account', [address])).data.free.toBigInt(); + } + + /** + * Transfer tokens to substrate address + * @param signer keyring of signer + * @param address substrate address of a recipient + * @param amount amount of tokens to be transfered + * @example transferToSubstrate(aliceKeyring, "5GrwvaEF5zXb26Fz...", 100_000_000_000n); + * @returns ```true``` if extrinsic success, otherwise ```false``` + */ + async transferToSubstrate(signer: TSigner, address: TSubstrateAccount, amount: bigint | string): Promise { + const result = await this.helper.executeExtrinsic(signer, 'api.tx.balances.transfer', [address, amount], true/*, `Unable to transfer balance from ${this.helper.getSignerAddress(signer)} to ${address}`*/); + + let transfer = {from: null, to: null, amount: 0n} as any; + result.result.events.forEach(({event: {data, method, section}}) => { + if ((section === 'balances') && (method === 'Transfer')) { + transfer = { + from: this.helper.address.normalizeSubstrate(data[0]), + to: this.helper.address.normalizeSubstrate(data[1]), + amount: BigInt(data[2]), + }; + } + }); + const isSuccess = this.helper.address.normalizeSubstrate(typeof signer === 'string' ? signer : signer.address) === transfer.from + && this.helper.address.normalizeSubstrate(address) === transfer.to + && BigInt(amount) === transfer.amount; + return isSuccess; + } + + /** + * Get full substrate balance including free, miscFrozen, feeFrozen, and reserved + * @param address substrate address + * @returns + */ + async getSubstrateFull(address: TSubstrateAccount): Promise { + 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()}; + } +} + +class EthereumBalanceGroup extends HelperGroup { + /** + * Get ethereum address balance + * @param address ethereum address + * @example getEthereum("0x9F0583DbB855d...") + * @returns amount of tokens on address + */ + async getEthereum(address: TEthereumAccount): Promise { + return (await this.helper.callRpc('api.rpc.eth.getBalance', [address])).toBigInt(); + } + + /** + * Transfer tokens to address + * @param signer keyring of signer + * @param address Ethereum address of a recipient + * @param amount amount of tokens to be transfered + * @example transferToEthereum(alithKeyring, "0x9F0583DbB855d...", 100_000_000_000n); + * @returns ```true``` if extrinsic success, otherwise ```false``` + */ + async transferToEthereum(signer: TSigner, address: TEthereumAccount, amount: bigint | string): Promise { + const result = await this.helper.executeExtrinsic(signer, 'api.tx.balances.transfer', [address, amount], true); + + let transfer = {from: null, to: null, amount: 0n} as any; + result.result.events.forEach(({event: {data, method, section}}) => { + if ((section === 'balances') && (method === 'Transfer')) { + transfer = { + from: data[0].toString(), + to: data[1].toString(), + amount: BigInt(data[2]), + }; + } + }); + const isSuccess = (typeof signer === 'string' ? signer : signer.address) === transfer.from + && address === transfer.to + && BigInt(amount) === transfer.amount; + return isSuccess; + } +} + +class BalanceGroup extends HelperGroup { + subBalanceGroup: SubstrateBalanceGroup; + ethBalanceGroup: EthereumBalanceGroup; + + constructor(helper: T) { + super(helper); + this.subBalanceGroup = new SubstrateBalanceGroup(helper); + this.ethBalanceGroup = new EthereumBalanceGroup(helper); + } -class BalanceGroup extends HelperGroup { getCollectionCreationPrice(): bigint { - return 2n * this.helper.balance.getOneTokenNominal(); + return 2n * this.getOneTokenNominal(); } /** * Representation of the native token in the smallest unit - one OPAL (OPL), QUARTZ (QTZ), or UNIQUE (UNQ). @@ -2076,7 +2205,7 @@ * @returns amount of tokens on address */ async getSubstrate(address: TSubstrateAccount): Promise { - return (await this.helper.callRpc('api.query.system.account', [address])).data.free.toBigInt(); + return this.subBalanceGroup.getSubstrate(address); } /** @@ -2085,8 +2214,7 @@ * @returns */ async getSubstrateFull(address: TSubstrateAccount): Promise { - 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()}; + return this.subBalanceGroup.getSubstrateFull(address); } /** @@ -2096,7 +2224,7 @@ * @returns amount of tokens on address */ async getEthereum(address: TEthereumAccount): Promise { - return (await this.helper.callRpc('api.rpc.eth.getBalance', [address])).toBigInt(); + return this.ethBalanceGroup.getEthereum(address); } /** @@ -2108,27 +2236,11 @@ * @returns ```true``` if extrinsic success, otherwise ```false``` */ async transferToSubstrate(signer: TSigner, address: TSubstrateAccount, amount: bigint | string): Promise { - const result = await this.helper.executeExtrinsic(signer, 'api.tx.balances.transfer', [address, amount], true/*, `Unable to transfer balance from ${this.helper.getSignerAddress(signer)} to ${address}`*/); - - let transfer = {from: null, to: null, amount: 0n} as any; - result.result.events.forEach(({event: {data, method, section}}) => { - if ((section === 'balances') && (method === 'Transfer')) { - transfer = { - from: this.helper.address.normalizeSubstrate(data[0]), - to: this.helper.address.normalizeSubstrate(data[1]), - amount: BigInt(data[2]), - }; - } - }); - const isSuccess = this.helper.address.normalizeSubstrate(typeof signer === 'string' ? signer : signer.address) === transfer.from - && this.helper.address.normalizeSubstrate(address) === transfer.to - && BigInt(amount) === transfer.amount; - return isSuccess; + return this.subBalanceGroup.transferToSubstrate(signer, address, amount); } } - -class AddressGroup extends HelperGroup { +class AddressGroup extends HelperGroup { /** * Normalizes the address to the specified ss58 format, by default ```42```. * @param address substrate address @@ -2172,7 +2284,7 @@ } } -class StakingGroup extends HelperGroup { +class StakingGroup extends HelperGroup { /** * Stake tokens for App Promotion * @param signer keyring of signer @@ -2258,7 +2370,7 @@ } } -class SchedulerGroup extends HelperGroup { +class SchedulerGroup extends HelperGroup { constructor(helper: UniqueHelper) { super(helper); } @@ -2314,11 +2426,7 @@ } } -class ForeignAssetsGroup extends HelperGroup { - constructor(helper: UniqueHelper) { - super(helper); - } - +class ForeignAssetsGroup extends HelperGroup { async register(signer: TSigner, ownerAddress: TSubstrateAccount, location: any, metadata: IForeignAssetMetadata) { await this.helper.executeExtrinsic( signer, @@ -2338,14 +2446,130 @@ } } +class XcmGroup extends HelperGroup { + palletName: string; + + constructor(helper: T, palletName: string) { + super(helper); + + this.palletName = palletName; + } + + async limitedReserveTransferAssets(signer: TSigner, destination: any, beneficiary: any, assets: any, feeAssetItem: number, weightLimit: number) { + await this.helper.executeExtrinsic(signer, `api.tx.${this.palletName}.limitedReserveTransferAssets`, [destination, beneficiary, assets, feeAssetItem, {Limited: weightLimit}], true); + } +} + +class XTokensGroup extends HelperGroup { + async transfer(signer: TSigner, currencyId: any, amount: bigint, destination: any, destWeight: number) { + await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transfer', [currencyId, amount, destination, destWeight], true); + } + + async transferMultiasset(signer: TSigner, asset: any, destination: any, destWeight: number) { + await this.helper.executeExtrinsic(signer, 'api.tx.xTokens.transferMultiasset', [asset, destination, destWeight], true); + } +} + +class TokensGroup extends HelperGroup { + async accounts(address: string, currencyId: any) { + const {free} = (await this.helper.callRpc('api.query.tokens.accounts', [address, currencyId])).toJSON() as any; + return BigInt(free); + } +} + +class AcalaAssetRegistryGroup extends HelperGroup { + async registerForeignAsset(signer: TSigner, destination: any, metadata: AcalaAssetMetadata) { + await this.helper.executeExtrinsic(signer, 'api.tx.assetRegistry.registerForeignAsset', [destination, metadata], true); + } +} + +class MoonbeamAssetManagerGroup extends HelperGroup { + makeRegisterForeignAssetProposal(assetInfo: MoonbeamAssetInfo) { + const apiPrefix = 'api.tx.assetManager.'; + + const registerTx = this.helper.constructApiCall( + apiPrefix + 'registerForeignAsset', + [assetInfo.location, assetInfo.metadata, assetInfo.existentialDeposit, assetInfo.isSufficient], + ); + + const setUnitsTx = this.helper.constructApiCall( + apiPrefix + 'setAssetUnitsPerSecond', + [assetInfo.location, assetInfo.unitsPerSecond, assetInfo.numAssetsWeightHint], + ); + + const batchCall = this.helper.getApi().tx.utility.batchAll([registerTx, setUnitsTx]); + const encodedProposal = batchCall?.method.toHex() || ''; + return encodedProposal; + } + + async assetTypeId(location: any) { + return await this.helper.callRpc('api.query.assetManager.assetTypeId', [location]); + } +} + +class MoonbeamAssetsGroup extends HelperGroup { + async account(assetId: string, address: string) { + const accountAsset = ( + await this.helper.callRpc('api.query.assets.account', [assetId, address]) + ).toJSON()! as any; + + if (accountAsset !== null) { + return BigInt(accountAsset['balance']); + } else { + return null; + } + } +} + +class MoonbeamDemocracyGroup extends HelperGroup { + async notePreimage(signer: TSigner, encodedProposal: string) { + await this.helper.executeExtrinsic(signer, 'api.tx.democracy.notePreimage', [encodedProposal], true); + } + + externalProposeMajority(proposalHash: string) { + return this.helper.constructApiCall('api.tx.democracy.externalProposeMajority', [proposalHash]); + } + + fastTrack(proposalHash: string, votingPeriod: number, delayPeriod: number) { + return this.helper.constructApiCall('api.tx.democracy.fastTrack', [proposalHash, votingPeriod, delayPeriod]); + } + + async referendumVote(signer: TSigner, referendumIndex: number, accountVote: DemocracyStandardAccountVote) { + await this.helper.executeExtrinsic(signer, 'api.tx.democracy.vote', [referendumIndex, {Standard: accountVote}], true); + } +} + +class MoonbeamCollectiveGroup extends HelperGroup { + collective: string; + + constructor(helper: MoonbeamHelper, collective: string) { + super(helper); + + this.collective = collective; + } + + async propose(signer: TSigner, threshold: number, proposalHash: string, lengthBound: number) { + await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.propose`, [threshold, proposalHash, lengthBound], true); + } + + async vote(signer: TSigner, proposalHash: string, proposalIndex: number, approve: boolean) { + await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.vote`, [proposalHash, proposalIndex, approve], true); + } + + async close(signer: TSigner, proposalHash: string, proposalIndex: number, weightBound: number, lengthBound: number) { + await this.helper.executeExtrinsic(signer, `api.tx.${this.collective}.close`, [proposalHash, proposalIndex, weightBound, lengthBound], true); + } + + async proposalCount() { + return Number(await this.helper.callRpc(`api.query.${this.collective}.proposalCount`, [])); + } +} + +export type ChainHelperBaseConstructor = new(...args: any[]) => ChainHelperBase; export type UniqueHelperConstructor = new(...args: any[]) => UniqueHelper; export class UniqueHelper extends ChainHelperBase { - helperBase: any; - - chain: ChainGroup; - balance: BalanceGroup; - address: AddressGroup; + balance: BalanceGroup; collection: CollectionGroup; nft: NFTGroup; rft: RFTGroup; @@ -2353,13 +2577,13 @@ staking: StakingGroup; scheduler: SchedulerGroup; foreignAssets: ForeignAssetsGroup; + xcm: XcmGroup; + xTokens: XTokensGroup; + tokens: TokensGroup; constructor(logger?: ILogger, options: {[key: string]: any} = {}) { - super(logger); - - this.helperBase = options.helperBase ?? UniqueHelper; + super(logger, options.helperBase ?? UniqueHelper); - this.chain = new ChainGroup(this); this.balance = new BalanceGroup(this); this.address = new AddressGroup(this); this.collection = new CollectionGroup(this); @@ -2369,24 +2593,83 @@ this.staking = new StakingGroup(this); this.scheduler = new SchedulerGroup(this); this.foreignAssets = new ForeignAssetsGroup(this); + this.xcm = new XcmGroup(this, 'polkadotXcm'); + this.xTokens = new XTokensGroup(this); + this.tokens = new TokensGroup(this); } - clone(helperCls: UniqueHelperConstructor, options: {[key: string]: any} = {}) { - Object.setPrototypeOf(helperCls.prototype, this); - const newHelper = new helperCls(this.logger, options); + getSudo() { + // eslint-disable-next-line @typescript-eslint/naming-convention + const SudoHelperType = SudoHelper(this.helperBase); + return this.clone(SudoHelperType) as T; + } +} - newHelper.api = this.api; - newHelper.network = this.network; - newHelper.forceNetwork = this.forceNetwork; +export class XcmChainHelper extends ChainHelperBase { + async connect(wsEndpoint: string, _listeners?: any): Promise { + const wsProvider = new WsProvider(wsEndpoint); + this.api = new ApiPromise({ + provider: wsProvider, + }); + await this.api.isReadyOrError; + this.network = await UniqueHelper.detectNetwork(this.api); + } +} - this.children.push(newHelper); +export class RelayHelper extends XcmChainHelper { + xcm: XcmGroup; - return newHelper; + constructor(logger?: ILogger, options: {[key: string]: any} = {}) { + super(logger, options.helperBase ?? RelayHelper); + + this.xcm = new XcmGroup(this, 'xcmPallet'); } +} + +export class MoonbeamHelper extends XcmChainHelper { + balance: EthereumBalanceGroup; + assetManager: MoonbeamAssetManagerGroup; + assets: MoonbeamAssetsGroup; + xTokens: XTokensGroup; + democracy: MoonbeamDemocracyGroup; + collective: { + council: MoonbeamCollectiveGroup, + techCommittee: MoonbeamCollectiveGroup, + }; - getSudo() { + constructor(logger?: ILogger, options: {[key: string]: any} = {}) { + super(logger, options.helperBase ?? MoonbeamHelper); + + this.balance = new EthereumBalanceGroup(this); + this.assetManager = new MoonbeamAssetManagerGroup(this); + this.assets = new MoonbeamAssetsGroup(this); + this.xTokens = new XTokensGroup(this); + this.democracy = new MoonbeamDemocracyGroup(this); + this.collective = { + council: new MoonbeamCollectiveGroup(this, 'councilCollective'), + techCommittee: new MoonbeamCollectiveGroup(this, 'techCommitteeCollective'), + }; + } +} + +export class AcalaHelper extends XcmChainHelper { + balance: SubstrateBalanceGroup; + assetRegistry: AcalaAssetRegistryGroup; + xTokens: XTokensGroup; + tokens: TokensGroup; + + constructor(logger?: ILogger, options: {[key: string]: any} = {}) { + super(logger, options.helperBase ?? AcalaHelper); + + this.balance = new SubstrateBalanceGroup(this); + this.assetRegistry = new AcalaAssetRegistryGroup(this); + this.xTokens = new XTokensGroup(this); + this.tokens = new TokensGroup(this); + } + + getSudo() { // eslint-disable-next-line @typescript-eslint/naming-convention - const SudoHelperType = SudoUniqueHelper(this.helperBase); + const SudoHelperType = SudoHelper(this.helperBase); return this.clone(SudoHelperType) as T; } } @@ -2437,7 +2720,7 @@ } // eslint-disable-next-line @typescript-eslint/naming-convention -function SudoUniqueHelper(Base: T) { +function SudoHelper(Base: T) { return class extends Base { constructor(...args: any[]) { super(...args);