--- /dev/null +++ b/tests/src/util/playgrounds/types.ts @@ -0,0 +1,130 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +import {IKeyringPair} from '@polkadot/types/types'; + +export interface IChainEvent { + data: any; + method: string; + section: string; +} + +export interface ITransactionResult { + status: 'Fail' | 'Success'; + result: { + events: { + event: IChainEvent + }[]; + }, + moduleError?: string; +} + +export interface ILogger { + log: (msg: any, level?: string) => void; + level: { + ERROR: 'ERROR'; + WARNING: 'WARNING'; + INFO: 'INFO'; + [key: string]: string; + } +} + +export interface IUniqueHelperLog { + executedAt: number; + executionTime: number; + type: 'extrinsic' | 'rpc'; + status: 'Fail' | 'Success'; + call: string; + params: any[]; + moduleError?: string; + events?: any; +} + +export interface IApiListeners { + connected?: (...args: any[]) => any; + disconnected?: (...args: any[]) => any; + error?: (...args: any[]) => any; + ready?: (...args: any[]) => any; + decorated?: (...args: any[]) => any; +} + +export interface ICrossAccountId { + Substrate?: TSubstrateAccount; + Ethereum?: TEthereumAccount; +} + +export interface ICrossAccountIdLower { + substrate?: TSubstrateAccount; + ethereum?: TEthereumAccount; +} + +export interface ICollectionLimits { + accountTokenOwnershipLimit?: number | null; + sponsoredDataSize?: number | null; + sponsoredDataRateLimit?: {blocks: number} | {sponsoringDisabled: null} | null; + tokenLimit?: number | null; + sponsorTransferTimeout?: number | null; + sponsorApproveTimeout?: number | null; + ownerCanTransfer?: boolean | null; + ownerCanDestroy?: boolean | null; + transfersEnabled?: boolean | null; +} + +export interface INestingPermissions { + tokenOwner?: boolean; + collectionAdmin?: boolean; + restricted?: number[] | null; +} + +export interface ICollectionPermissions { + access?: 'Normal' | 'AllowList'; + mintMode?: boolean; + nesting?: INestingPermissions; +} + +export interface IProperty { + key: string; + value: string; +} + +export interface ITokenPropertyPermission { + key: string; + permission: { + mutable: boolean; + tokenOwner: boolean; + collectionAdmin: boolean; + } +} + +export interface IToken { + collectionId: number; + tokenId: number; +} + +export interface ICollectionCreationOptions { + name: string | number[]; + description: string | number[]; + tokenPrefix: string | number[]; + mode?: { + nft?: null; + refungible?: null; + fungible?: number; + } + permissions?: ICollectionPermissions; + properties?: IProperty[]; + tokenPropertyPermissions?: ITokenPropertyPermission[]; + limits?: ICollectionLimits; + pendingSponsor?: TSubstrateAccount; +} + +export interface IChainProperties { + ss58Format: number; + tokenDecimals: number[]; + tokenSymbol: string[] +} + +export type TSubstrateAccount = string; +export type TEthereumAccount = string; +export type TApiAllowedListeners = 'connected' | 'disconnected' | 'error' | 'ready' | 'decorated'; +export type TUniqueNetworks = 'opal' | 'quartz' | 'unique'; +export type TSigner = IKeyringPair; // | 'string' \ No newline at end of file --- a/tests/src/util/playgrounds/unique.dev.ts +++ b/tests/src/util/playgrounds/unique.dev.ts @@ -3,9 +3,10 @@ import {mnemonicGenerate} from '@polkadot/util-crypto'; import {UniqueHelper} from './unique'; -import {IKeyringPair} from '@polkadot/types/types'; import {ApiPromise, WsProvider} from '@polkadot/api'; import * as defs from '../../interfaces/definitions'; +import {TSigner} from './types'; +import {IKeyringPair} from '@polkadot/types/types'; export class DevUniqueHelper extends UniqueHelper { @@ -73,7 +74,7 @@ let nonce = await this.helper.chain.getNonce(donor.address); const tokenNominal = this.helper.balance.getOneTokenNominal(); const transactions = []; - const accounts = []; + const accounts: IKeyringPair[] = []; for (const balance of balances) { const recepient = this.helper.util.fromSeed(mnemonicGenerate()); accounts.push(recepient); @@ -84,7 +85,52 @@ } } - await Promise.all(transactions); + await Promise.all(transactions).catch(e => {}); + + //#region TODO remove this region, when nonce problem will be solved + const checkBalances = async () => { + let isSuccess = true; + for (let i = 0; i < balances.length; i++) { + const balance = await this.helper.balance.getSubstrate(accounts[i].address); + if (balance !== balances[i] * tokenNominal) { + isSuccess = false; + break; + } + } + return isSuccess; + }; + + let accountsCreated = false; + // checkBalances retry up to 5 blocks + for (let index = 0; index < 5; index++) { + console.log(await this.helper.chain.getLatestBlockNumber()); + accountsCreated = await checkBalances(); + if(accountsCreated) break; + await this.waitNewBlocks(1); + } + + if (!accountsCreated) throw Error('Accounts generation failed'); + //#endregion + return accounts; }; + + /** + * Wait for specified bnumber of blocks + * @param blocksCount number of blocks to wait + * @returns + */ + async waitNewBlocks(blocksCount = 1): Promise { + const promise = new Promise(async (resolve) => { + const unsubscribe = await this.helper.api!.rpc.chain.subscribeNewHeads(() => { + if (blocksCount > 0) { + blocksCount--; + } else { + unsubscribe(); + resolve(); + } + }); + }); + return promise; + } } \ No newline at end of file --- a/tests/src/util/playgrounds/unique.ts +++ b/tests/src/util/playgrounds/unique.ts @@ -7,10 +7,10 @@ import {ApiPromise, WsProvider, Keyring} from '@polkadot/api'; import {ApiInterfaceEvents} from '@polkadot/api/types'; +import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto'; import {IKeyringPair} from '@polkadot/types/types'; -import {encodeAddress, decodeAddress, keccakAsHex, evmToAddress, addressToEvm} from '@polkadot/util-crypto'; +import {IApiListeners, IChainEvent, IChainProperties, ICollectionCreationOptions, ICollectionLimits, ICollectionPermissions, ICrossAccountId, ICrossAccountIdLower, ILogger, INestingPermissions, IProperty, IToken, ITokenPropertyPermission, ITransactionResult, IUniqueHelperLog, TApiAllowedListeners, TEthereumAccount, TSigner, TSubstrateAccount, TUniqueNetworks} from './types'; - const crossAccountIdFromLower = (lowerAddress: ICrossAccountIdLower): ICrossAccountId => { const address = {} as ICrossAccountId; if(lowerAddress.substrate) address.Substrate = lowerAddress.substrate; @@ -43,133 +43,6 @@ return this.toChecksumAddress(`0xf8238ccfff8ed887463fd5e0${collectionId.toString(16).padStart(8, '0')}${tokenId.toString(16).padStart(8, '0')}`); }, }; - - -interface IChainEvent { - data: any; - method: string; - section: string; -} - -interface ITransactionResult { - status: 'Fail' | 'Success'; - result: { - events: { - event: IChainEvent - }[]; - }, - moduleError?: string; -} - -interface ILogger { - log: (msg: any, level?: string) => void; - level: { - ERROR: 'ERROR'; - WARNING: 'WARNING'; - INFO: 'INFO'; - [key: string]: string; - } -} - -interface IUniqueHelperLog { - executedAt: number; - executionTime: number; - type: 'extrinsic' | 'rpc'; - status: 'Fail' | 'Success'; - call: string; - params: any[]; - moduleError?: string; - events?: any; -} - -interface IApiListeners { - connected?: (...args: any[]) => any; - disconnected?: (...args: any[]) => any; - error?: (...args: any[]) => any; - ready?: (...args: any[]) => any; - decorated?: (...args: any[]) => any; -} - -interface ICrossAccountId { - Substrate?: TSubstrateAccount; - Ethereum?: TEthereumAccount; -} - -interface ICrossAccountIdLower { - substrate?: TSubstrateAccount; - ethereum?: TEthereumAccount; -} - -interface ICollectionLimits { - accountTokenOwnershipLimit?: number | null; - sponsoredDataSize?: number | null; - sponsoredDataRateLimit?: {blocks: number} | {sponsoringDisabled: null} | null; - tokenLimit?: number | null; - sponsorTransferTimeout?: number | null; - sponsorApproveTimeout?: number | null; - ownerCanTransfer?: boolean | null; - ownerCanDestroy?: boolean | null; - transfersEnabled?: boolean | null; -} - -interface INestingPermissions { - tokenOwner?: boolean; - collectionAdmin?: boolean; - restricted?: number[] | null; -} - -interface ICollectionPermissions { - access?: 'Normal' | 'AllowList'; - mintMode?: boolean; - nesting?: INestingPermissions; -} - -interface IProperty { - key: string; - value: string; -} - -interface ITokenPropertyPermission { - key: string; - permission: { - mutable: boolean; - tokenOwner: boolean; - collectionAdmin: boolean; - } -} - -interface IToken { - collectionId: number; - tokenId: number; -} - -interface ICollectionCreationOptions { - name: string | number[]; - description: string | number[]; - tokenPrefix: string | number[]; - mode?: { - nft?: null; - refungible?: null; - fungible?: number; - } - permissions?: ICollectionPermissions; - properties?: IProperty[]; - tokenPropertyPermissions?: ITokenPropertyPermission[]; - limits?: ICollectionLimits; - pendingSponsor?: TSubstrateAccount; -} - -interface IChainProperties { - ss58Format: number; - tokenDecimals: number[]; - tokenSymbol: string[] -} - -type TSubstrateAccount = string; -type TEthereumAccount = string; -type TApiAllowedListeners = 'connected' | 'disconnected' | 'error' | 'ready' | 'decorated'; -type TUniqueNetworks = 'opal' | 'quartz' | 'unique'; -type TSigner = IKeyringPair; // | 'string' class UniqueUtil { static transactionStatus = { @@ -652,6 +525,22 @@ } /** + * Get the normalized addresses added to the collection allow-list. + * @param collectionId ID of collection + * @example await getAllowList(1) + * @returns array of allow-listed addresses + */ + async getAllowList(collectionId: number): Promise { + const normalized = []; + const allowListed = (await this.helper.callRpc('api.rpc.unique.allowlist', [collectionId])).toHuman(); + for (const address of allowListed) { + if (address.Substrate) normalized.push({Substrate: this.helper.address.normalizeSubstrate(address.Substrate)}); + else normalized.push(address); + } + return normalized; + } + + /** * Get the effective limits of the collection instead of null for default values * * @param collectionId ID of collection @@ -795,6 +684,25 @@ } /** + * Adds an address to allow list + * @param signer keyring of signer + * @param collectionId ID of collection + * @param addressObj address to add to the allow list + * @param label extra label for log + * @returns ```true``` if extrinsic success, otherwise ```false``` + */ + async addToAllowList(signer: TSigner, collectionId: number, addressObj: ICrossAccountId, label?: string): Promise { + if(typeof label === 'undefined') label = `collection #${collectionId}`; + const result = await this.helper.executeExtrinsic( + signer, + 'api.tx.unique.addToAllowList', [collectionId, addressObj], + true, `Unable to add address to allow list for ${label}`, + ); + + return this.helper.util.findCollectionInEvents(result.result.events, collectionId, 'unique', 'AllowListAddressAdded'); + } + + /** * Removes a collection administrator. * * @param signer keyring of signer @@ -2119,6 +2027,10 @@ return await this.helper.collection.getAdmins(this.collectionId); } + async getAllowList() { + return await this.helper.collection.getAllowList(this.collectionId); + } + async getEffectiveLimits() { return await this.helper.collection.getEffectiveLimits(this.collectionId); } @@ -2143,6 +2055,10 @@ return await this.helper.collection.addAdmin(signer, this.collectionId, adminAddressObj, label); } + async addToAllowList(signer: TSigner, addressObj: ICrossAccountId, label?: string) { + return await this.helper.collection.addToAllowList(signer, this.collectionId, addressObj, label); + } + async removeAdmin(signer: TSigner, adminAddressObj: ICrossAccountId, label?: string) { return await this.helper.collection.removeAdmin(signer, this.collectionId, adminAddressObj, label); }