difftreelog
test(refungible-pallet) add tests for ERC-721 EVM API
in: master
3 files changed
tests/src/eth/reFungible.test.tsdiffbeforeafterboth--- /dev/null
+++ b/tests/src/eth/reFungible.test.ts
@@ -0,0 +1,222 @@
+// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.
+// This file is part of Unique Network.
+
+// Unique Network is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Unique Network is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.
+
+import {createCollectionExpectSuccess} from '../util/helpers';
+import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, evmCollection, evmCollectionHelpers, GAS_ARGS, getCollectionAddressFromResult, itWeb3, normalizeEvents, tokenIdToAddress} from './util/helpers';
+import reFungibleAbi from './reFungibleAbi.json';
+import reFungibleTokenAbi from './reFungibleTokenAbi.json';
+import {expect} from 'chai';
+
+describe('Refungible: Information getting', () => {
+ itWeb3('totalSupply', async ({api, web3, privateKeyWrapper}) => {
+ const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+ const helper = evmCollectionHelpers(web3, caller);
+ const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+ const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+ const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+ const nextTokenId = await contract.methods.nextTokenId().call();
+ await contract.methods.mint(caller, nextTokenId).send();
+ const totalSupply = await contract.methods.totalSupply().call();
+ expect(totalSupply).to.equal('1');
+ });
+
+ itWeb3('balanceOf', async ({api, web3, privateKeyWrapper}) => {
+ const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+ const helper = evmCollectionHelpers(web3, caller);
+ const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+ const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+ const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+
+ {
+ const nextTokenId = await contract.methods.nextTokenId().call();
+ await contract.methods.mint(caller, nextTokenId).send();
+ }
+ {
+ const nextTokenId = await contract.methods.nextTokenId().call();
+ await contract.methods.mint(caller, nextTokenId).send();
+ }
+ {
+ const nextTokenId = await contract.methods.nextTokenId().call();
+ await contract.methods.mint(caller, nextTokenId).send();
+ }
+
+ const balance = await contract.methods.balanceOf(caller).call();
+
+ expect(balance).to.equal('3');
+ });
+
+ itWeb3('ownerOf', async ({api, web3, privateKeyWrapper}) => {
+ const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+ const helper = evmCollectionHelpers(web3, caller);
+ const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+ const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+ const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+
+ const tokenId = await contract.methods.nextTokenId().call();
+ await contract.methods.mint(caller, tokenId).send();
+
+ const owner = await contract.methods.ownerOf(tokenId).call();
+
+ expect(owner).to.equal(caller);
+ });
+
+ itWeb3('ownerOf after burn', async ({api, web3, privateKeyWrapper}) => {
+ const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+ const receiver = createEthAccount(web3);
+ const helper = evmCollectionHelpers(web3, caller);
+ const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+ const {collectionIdAddress, collectionId} = await getCollectionAddressFromResult(api, result);
+ const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+
+ const tokenId = await contract.methods.nextTokenId().call();
+ await contract.methods.mint(caller, tokenId).send();
+
+ const tokenAddress = tokenIdToAddress(collectionId, tokenId);
+ const tokenContract = new web3.eth.Contract(reFungibleTokenAbi as any, tokenAddress, {from: caller, ...GAS_ARGS});
+
+ await tokenContract.methods.repartition(2).send();
+ await tokenContract.methods.transfer(receiver, 1).send();
+
+ await tokenContract.methods.burnFrom(caller, 1).send();
+
+ const owner = await contract.methods.ownerOf(tokenId).call();
+
+ expect(owner).to.equal(receiver);
+ });
+});
+
+describe('Refungible: Plain calls', () => {
+ itWeb3('Can perform mint()', async ({web3, api, privateKeyWrapper}) => {
+ const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+ const helper = evmCollectionHelpers(web3, owner);
+ let result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+ const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+ const receiver = createEthAccount(web3);
+ const contract = evmCollection(web3, owner, collectionIdAddress);
+ const nextTokenId = await contract.methods.nextTokenId().call();
+
+ expect(nextTokenId).to.be.equal('1');
+ result = await contract.methods.mintWithTokenURI(
+ receiver,
+ nextTokenId,
+ 'Test URI',
+ ).send();
+
+ const events = normalizeEvents(result.events);
+
+ expect(events).to.include.deep.members([
+ {
+ address: collectionIdAddress,
+ event: 'Transfer',
+ args: {
+ from: '0x0000000000000000000000000000000000000000',
+ to: receiver,
+ tokenId: nextTokenId,
+ },
+ },
+ ]);
+
+ expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI');
+ });
+
+ itWeb3('Can perform mintBulk()', async ({web3, api, privateKeyWrapper}) => {
+ const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+ const helper = evmCollectionHelpers(web3, caller);
+ const result = await helper.methods.createRefungibleCollection('Mint collection', '6', '6').send();
+ const {collectionIdAddress} = await getCollectionAddressFromResult(api, result);
+ const contract = new web3.eth.Contract(reFungibleAbi as any, collectionIdAddress, {from: caller, ...GAS_ARGS});
+
+ const receiver = createEthAccount(web3);
+
+ {
+ const nextTokenId = await contract.methods.nextTokenId().call();
+ expect(nextTokenId).to.be.equal('1');
+ const result = await contract.methods.mintBulkWithTokenURI(
+ receiver,
+ [
+ [nextTokenId, 'Test URI 0'],
+ [+nextTokenId + 1, 'Test URI 1'],
+ [+nextTokenId + 2, 'Test URI 2'],
+ ],
+ ).send();
+ const events = normalizeEvents(result.events);
+
+ expect(events).to.include.deep.members([
+ {
+ address: collectionIdAddress,
+ event: 'Transfer',
+ args: {
+ from: '0x0000000000000000000000000000000000000000',
+ to: receiver,
+ tokenId: nextTokenId,
+ },
+ },
+ {
+ address: collectionIdAddress,
+ event: 'Transfer',
+ args: {
+ from: '0x0000000000000000000000000000000000000000',
+ to: receiver,
+ tokenId: String(+nextTokenId + 1),
+ },
+ },
+ {
+ address: collectionIdAddress,
+ event: 'Transfer',
+ args: {
+ from: '0x0000000000000000000000000000000000000000',
+ to: receiver,
+ tokenId: String(+nextTokenId + 2),
+ },
+ },
+ ]);
+
+ expect(await contract.methods.tokenURI(nextTokenId).call()).to.be.equal('Test URI 0');
+ expect(await contract.methods.tokenURI(+nextTokenId + 1).call()).to.be.equal('Test URI 1');
+ expect(await contract.methods.tokenURI(+nextTokenId + 2).call()).to.be.equal('Test URI 2');
+ }
+ });
+});
+
+describe('Common metadata', () => {
+ itWeb3('Returns collection name', async ({api, web3, privateKeyWrapper}) => {
+ const collection = await createCollectionExpectSuccess({
+ name: 'token name',
+ mode: {type: 'ReFungible'},
+ });
+ const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+ const address = collectionIdToAddress(collection);
+ const contract = new web3.eth.Contract(reFungibleAbi as any, address, {from: caller, ...GAS_ARGS});
+ const name = await contract.methods.name().call();
+
+ expect(name).to.equal('token name');
+ });
+
+ itWeb3('Returns symbol name', async ({api, web3, privateKeyWrapper}) => {
+ const collection = await createCollectionExpectSuccess({
+ tokenPrefix: 'TOK',
+ mode: {type: 'ReFungible'},
+ });
+ const caller = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
+
+ const address = collectionIdToAddress(collection);
+ const contract = new web3.eth.Contract(reFungibleAbi as any, address, {from: caller, ...GAS_ARGS});
+ const symbol = await contract.methods.symbol().call();
+
+ expect(symbol).to.equal('TOK');
+ });
+});
\ No newline at end of file
tests/src/eth/reFungibleToken.test.tsdiffbeforeafterboth--- a/tests/src/eth/reFungibleToken.test.ts
+++ b/tests/src/eth/reFungibleToken.test.ts
@@ -74,7 +74,7 @@
});
// FIXME: Need erc721 for ReFubgible.
-describe.skip('Check ERC721 token URI for ReFungible', () => {
+describe('Check ERC721 token URI for ReFungible', () => {
itWeb3('Empty tokenURI', async ({web3, api, privateKeyWrapper}) => {
const owner = await createEthAccountWithBalance(api, web3, privateKeyWrapper);
const helper = evmCollectionHelpers(web3, owner);
tests/src/eth/util/helpers.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/>.1617// eslint-disable-next-line @typescript-eslint/triple-slash-reference18/// <reference path="helpers.d.ts" />1920import {ApiPromise} from '@polkadot/api';21import {IKeyringPair} from '@polkadot/types/types';22import {addressToEvm, evmToAddress} from '@polkadot/util-crypto';23import {expect} from 'chai';24import * as solc from 'solc';25import Web3 from 'web3';26import config from '../../config';27import getBalance from '../../substrate/get-balance';28import usingApi, {submitTransactionAsync} from '../../substrate/substrate-api';29import waitNewBlocks from '../../substrate/wait-new-blocks';30import {CollectionMode, CrossAccountId, getDetailedCollectionInfo, getGenericResult, UNIQUE} from '../../util/helpers';31import collectionHelpersAbi from '../collectionHelpersAbi.json';32import fungibleAbi from '../fungibleAbi.json';33import nonFungibleAbi from '../nonFungibleAbi.json';34import refungibleAbi from '../refungibleAbi.json';35import contractHelpersAbi from './contractHelpersAbi.json';3637export const GAS_ARGS = {gas: 2500000};3839export enum SponsoringMode {40 Disabled = 0,41 Allowlisted = 1,42 Generous = 2,43}4445let web3Connected = false;46export async function usingWeb3<T>(cb: (web3: Web3) => Promise<T> | T): Promise<T> {47 if (web3Connected) throw new Error('do not nest usingWeb3 calls');48 web3Connected = true;4950 const provider = new Web3.providers.WebsocketProvider(config.substrateUrl);51 const web3 = new Web3(provider);5253 try {54 return await cb(web3);55 } finally {56 // provider.disconnect(3000, 'normal disconnect');57 provider.connection.close();58 web3Connected = false;59 }60}6162function encodeIntBE(v: number): number[] {63 if (v >= 0xffffffff || v < 0) throw new Error('id overflow');64 return [65 v >> 24,66 (v >> 16) & 0xff,67 (v >> 8) & 0xff,68 v & 0xff,69 ];70}7172export async function getCollectionAddressFromResult(api: ApiPromise, result: any) {73 const collectionIdAddress = normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);74 const collectionId = collectionIdFromAddress(collectionIdAddress); 75 const collection = (await getDetailedCollectionInfo(api, collectionId))!;76 return {collectionIdAddress, collectionId, collection};77}7879export function collectionIdToAddress(collection: number): string {80 const buf = Buffer.from([0x17, 0xc4, 0xe6, 0x45, 0x3c, 0xc4, 0x9a, 0xaa, 0xae, 0xac, 0xa8, 0x94, 0xe6, 0xd9, 0x68, 0x3e,81 ...encodeIntBE(collection),82 ]);83 return Web3.utils.toChecksumAddress('0x' + buf.toString('hex'));84}85export function collectionIdFromAddress(address: string): number {86 if (!address.startsWith('0x'))87 throw 'address not starts with "0x"';88 if (address.length > 42)89 throw 'address length is more than 20 bytes';90 return Number('0x' + address.substring(address.length - 8));91}92 93export function normalizeAddress(address: string): string {94 return '0x' + address.substring(address.length - 40);95}9697export function tokenIdToAddress(collection: number, token: number): string {98 const buf = Buffer.from([0xf8, 0x23, 0x8c, 0xcf, 0xff, 0x8e, 0xd8, 0x87, 0x46, 0x3f, 0xd5, 0xe0,99 ...encodeIntBE(collection),100 ...encodeIntBE(token),101 ]);102 return Web3.utils.toChecksumAddress('0x' + buf.toString('hex'));103}104export function tokenIdToCross(collection: number, token: number): CrossAccountId {105 return {106 Ethereum: tokenIdToAddress(collection, token),107 };108}109110export function createEthAccount(web3: Web3) {111 const account = web3.eth.accounts.create();112 web3.eth.accounts.wallet.add(account.privateKey);113 return account.address;114}115116export async function createEthAccountWithBalance(api: ApiPromise, web3: Web3, privateKeyWrapper: (account: string) => IKeyringPair) {117 const alice = privateKeyWrapper('//Alice');118 const account = createEthAccount(web3);119 await transferBalanceToEth(api, alice, account);120121 return account;122}123124export async function transferBalanceToEth(api: ApiPromise, source: IKeyringPair, target: string, amount = 1000n * UNIQUE) {125 const tx = api.tx.balances.transfer(evmToAddress(target), amount);126 const events = await submitTransactionAsync(source, tx);127 const result = getGenericResult(events);128 expect(result.success).to.be.true;129}130131export async function itWeb3(name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any, opts: { only?: boolean, skip?: boolean } = {}) {132 let i: any = it;133 if (opts.only) i = i.only;134 else if (opts.skip) i = i.skip;135 i(name, async () => {136 await usingApi(async (api, privateKeyWrapper) => {137 await usingWeb3(async web3 => {138 await cb({api, web3, privateKeyWrapper});139 });140 });141 });142}143itWeb3.only = (name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itWeb3(name, cb, {only: true});144itWeb3.skip = (name: string, cb: (apis: { web3: Web3, api: ApiPromise, privateKeyWrapper: (account: string) => IKeyringPair }) => any) => itWeb3(name, cb, {skip: true});145146export async function generateSubstrateEthPair(web3: Web3) {147 const account = web3.eth.accounts.create();148 evmToAddress(account.address);149}150151type NormalizedEvent = {152 address: string,153 event: string,154 args: { [key: string]: string }155};156157export function normalizeEvents(events: any): NormalizedEvent[] {158 const output = [];159 for (const key of Object.keys(events)) {160 if (key.match(/^[0-9]+$/)) {161 output.push(events[key]);162 } else if (Array.isArray(events[key])) {163 output.push(...events[key]);164 } else {165 output.push(events[key]);166 }167 }168 output.sort((a, b) => a.logIndex - b.logIndex);169 return output.map(({address, event, returnValues}) => {170 const args: { [key: string]: string } = {};171 for (const key of Object.keys(returnValues)) {172 if (!key.match(/^[0-9]+$/)) {173 args[key] = returnValues[key];174 }175 }176 return {177 address,178 event,179 args,180 };181 });182}183184export async function recordEvents(contract: any, action: () => Promise<void>): Promise<NormalizedEvent[]> {185 const out: any = [];186 contract.events.allEvents((_: any, event: any) => {187 out.push(event);188 });189 await action();190 return normalizeEvents(out);191}192193export function subToEthLowercase(eth: string): string {194 const bytes = addressToEvm(eth);195 return '0x' + Buffer.from(bytes).toString('hex');196}197198export function subToEth(eth: string): string {199 return Web3.utils.toChecksumAddress(subToEthLowercase(eth));200}201202export function compileContract(name: string, src: string) {203 const out = JSON.parse(solc.compile(JSON.stringify({204 language: 'Solidity',205 sources: {206 [`${name}.sol`]: {207 content: `208 // SPDX-License-Identifier: UNLICENSED209 pragma solidity ^0.8.6;210211 ${src}212 `,213 },214 },215 settings: {216 outputSelection: {217 '*': {218 '*': ['*'],219 },220 },221 },222 }))).contracts[`${name}.sol`][name];223224 return {225 abi: out.abi,226 object: '0x' + out.evm.bytecode.object,227 };228}229230export async function deployFlipper(web3: Web3, deployer: string) {231 const compiled = compileContract('Flipper', `232 contract Flipper {233 bool value = false;234 function flip() public {235 value = !value;236 }237 function getValue() public view returns (bool) {238 return value;239 }240 }241 `);242 const flipperContract = new web3.eth.Contract(compiled.abi, undefined, {243 data: compiled.object,244 from: deployer,245 ...GAS_ARGS,246 });247 const flipper = await flipperContract.deploy({data: compiled.object}).send({from: deployer});248249 return flipper;250}251252export async function deployCollector(web3: Web3, deployer: string) {253 const compiled = compileContract('Collector', `254 contract Collector {255 uint256 collected;256 fallback() external payable {257 giveMoney();258 }259 function giveMoney() public payable {260 collected += msg.value;261 }262 function getCollected() public view returns (uint256) {263 return collected;264 }265 function getUnaccounted() public view returns (uint256) {266 return address(this).balance - collected;267 }268269 function withdraw(address payable target) public {270 target.transfer(collected);271 collected = 0;272 }273 }274 `);275 const collectorContract = new web3.eth.Contract(compiled.abi, undefined, {276 data: compiled.object,277 from: deployer,278 ...GAS_ARGS,279 });280 const collector = await collectorContract.deploy({data: compiled.object}).send({from: deployer});281282 return collector;283}284285/** 286 * pallet evm_contract_helpers287 * @param web3 288 * @param caller - eth address289 * @returns 290 */291export function contractHelpers(web3: Web3, caller: string) {292 return new web3.eth.Contract(contractHelpersAbi as any, '0x842899ECF380553E8a4de75bF534cdf6fBF64049', {from: caller, ...GAS_ARGS});293}294295/** 296 * evm collection helper297 * @param web3 298 * @param caller - eth address299 * @returns 300 */301export function evmCollectionHelpers(web3: Web3, caller: string) {302 return new web3.eth.Contract(collectionHelpersAbi as any, '0x6c4e9fe1ae37a41e93cee429e8e1881abdcbb54f', {from: caller, ...GAS_ARGS});303}304305/** 306 * evm collection307 * @param web3 308 * @param caller - eth address309 * @returns 310 */311export function evmCollection(web3: Web3, caller: string, collection: string, mode: CollectionMode = {type: 'NFT'}) {312 let abi;313 switch (mode.type) {314 case 'Fungible':315 abi = fungibleAbi;316 break;317 318 case 'NFT':319 abi = nonFungibleAbi;320 break;321 322 case 'ReFungible':323 abi = refungibleAbi;324 break;325326 default:327 throw 'Bad collection mode';328 }329 const contract = new web3.eth.Contract(abi as any, collection, {from: caller, ...GAS_ARGS});330 return contract;331}332333/**334 * Execute ethereum method call using substrate account335 * @param to target contract336 * @param mkTx - closure, receiving `contract.methods`, and returning method call,337 * to be used as following (assuming `to` = erc20 contract):338 * `m => m.transfer(to, amount)`339 *340 * # Example341 * ```ts342 * executeEthTxOnSub(api, alice, erc20Contract, m => m.transfer(target, amount));343 * ```344 */345export async function executeEthTxOnSub(web3: Web3, api: ApiPromise, from: IKeyringPair, to: any, mkTx: (methods: any) => any, {value = 0}: {value?: bigint | number} = { }) {346 const tx = api.tx.evm.call(347 subToEth(from.address),348 to.options.address,349 mkTx(to.methods).encodeABI(),350 value,351 GAS_ARGS.gas,352 await web3.eth.getGasPrice(),353 null,354 null,355 [],356 );357 const events = await submitTransactionAsync(from, tx);358 expect(events.some(({event: {section, method}}) => section == 'evm' && method == 'Executed')).to.be.true;359}360361export async function ethBalanceViaSub(api: ApiPromise, address: string): Promise<bigint> {362 return (await getBalance(api, [evmToAddress(address)]))[0];363}364365/**366 * Measure how much gas given closure consumes367 *368 * @param user which user balance will be checked369 */370export async function recordEthFee(api: ApiPromise, user: string, call: () => Promise<any>): Promise<bigint> {371 const before = await ethBalanceViaSub(api, user);372373 await call();374375 // In dev mode, the transaction might not finish processing in time376 await waitNewBlocks(api, 1);377 const after = await ethBalanceViaSub(api, user);378379 // Can't use .to.be.less, because chai doesn't supports bigint380 expect(after < before).to.be.true;381382 return before - after;383}384385type ElementOf<A> = A extends readonly (infer T)[] ? T : never;386// I want a fancier api, not a memory efficiency387export function* cartesian<T extends Array<Array<any>>, R extends Array<any>>(internalRest: [...R], ...args: [...T]): Generator<[...R, ...{[K in keyof T]: ElementOf<T[K]>}]> {388 if(args.length === 0) {389 yield internalRest as any;390 return;391 }392 for(const value of args[0]) {393 yield* cartesian([...internalRest, value], ...args.slice(1)) as any;394 }395}