From 73d8244f0c811f1d74b93791f3905a67bf0c9c0b Mon Sep 17 00:00:00 2001 From: Ivo Yankov Date: Wed, 13 Jul 2022 04:45:46 +0300 Subject: [PATCH] HTS acceptance tests (#298) Update HTS acceptance tests Signed-off-by: Ivo Yankov --- .../server/tests/acceptance/erc20.spec.ts | 369 +++++++++++------- .../server/tests/clients/servicesClient.ts | 79 +++- 2 files changed, 309 insertions(+), 139 deletions(-) diff --git a/packages/server/tests/acceptance/erc20.spec.ts b/packages/server/tests/acceptance/erc20.spec.ts index 7d2dff20d..aec811d6d 100644 --- a/packages/server/tests/acceptance/erc20.spec.ts +++ b/packages/server/tests/acceptance/erc20.spec.ts @@ -25,29 +25,34 @@ chai.use(solidity); import { AliasAccount } from '../clients/servicesClient'; import { ethers, BigNumber } from 'ethers'; - import ERC20MockJson from '../contracts/ERC20Mock.json'; -import ERC20DecimalsMockJson from '../contracts/ERC20DecimalsMock.json'; - import Assertions from '../helpers/assertions'; import {Utils} from '../helpers/utils'; -describe('ERC20 Acceptance Tests', function () { +describe('ERC20 Acceptance Tests', async function () { this.timeout(240 * 1000); // 240 seconds - const {servicesNode, relay} = global; + const {servicesNode, relay, logger} = global; // cached entities const accounts: AliasAccount[] = []; - let contract; let initialHolder; let recipient; let anotherAccount; + const contracts:[any] = []; const name = Utils.randomString(10); const symbol = Utils.randomString(5); const initialSupply = BigNumber.from(10000); + const ERC20 = 'ERC20 Contract'; + const HTS = 'HTS token'; + + const testTitles = [ + {testName: ERC20, expectedBytecode: ERC20MockJson.deployedBytecode}, + {testName: HTS, expectedBytecode: '0x0'} + ]; + before(async () => { accounts[0] = await servicesNode.createAliasAccount(30, relay.provider); accounts[1] = await servicesNode.createAliasAccount(10, relay.provider); @@ -57,193 +62,258 @@ describe('ERC20 Acceptance Tests', function () { recipient = accounts[1].address; anotherAccount = accounts[2].address; - contract = await deployErc20([name, symbol, initialHolder, initialSupply], ERC20MockJson); + contracts.push(await deployErc20([name, symbol, initialHolder, initialSupply], ERC20MockJson)); + contracts.push(await createHTS(name, symbol, accounts[0], 10000, ERC20MockJson.abi, [accounts[1], accounts[2]])); }); - describe('Mocked ERC20 Contract', function () { - it('has a name', async function () { - expect(await contract.name()).to.equal(name); - }); - - it('has a symbol', async function () { - expect(await contract.symbol()).to.equal(symbol); - }); - - it('has 18 decimals', async function () { - expect(await contract.decimals()).to.be.equal(18); - }); - - it('Relay can execute "eth_getCode" for ERC20 contract with evmAddress', async function () { - const res = await relay.call('eth_getCode', [contract.address]); - expect(res).to.eq(ERC20MockJson.deployedBytecode); - }); + for (const i in testTitles) { + describe(testTitles[i].testName, async function () { + let contract; - describe('set decimals', function () { - const decimals = 6; + before(async function () { + contract = contracts[i]; + }); - it('can set decimals during construction', async function () { + it('has a name', async function () { + expect(await contract.name()).to.equal(name); + }); - const decimalsContract = await deployErc20([name, symbol, decimals], ERC20DecimalsMockJson); - expect(await decimalsContract.decimals()).to.be.equal(decimals); + it('has a symbol', async function () { + expect(await contract.symbol()).to.equal(symbol); }); - }); - describe('should behave like erc20', function() { - describe('total supply', function () { - it('returns the total amount of tokens', async function () { - const supply = await contract.totalSupply(); - expect(supply.toString()).to.be.equal(initialSupply.toString()); - }); + it('has 18 decimals', async function () { + expect(await contract.decimals()).to.be.equal(18); }); - describe('balanceOf', function () { - describe('when the requested account has no tokens', function () { - it('returns zero', async function () { - const otherBalance = await contract.balanceOf(anotherAccount); - expect(otherBalance.toString()).to.be.equal('0'); - }); - }); + it('Relay can execute "eth_getCode" for ERC20 contract with evmAddress', async function () { + const res = await relay.call('eth_getCode', [contract.address]); + expect(res).to.eq(testTitles[i].expectedBytecode); + }); - describe('when the requested account has some tokens', function () { + describe('should behave like erc20', function() { + describe('total supply', function () { it('returns the total amount of tokens', async function () { - const balance = await contract.balanceOf(initialHolder); - expect(balance.toString()).to.be.equal(initialSupply.toString()); + const supply = await contract.totalSupply(); + expect(supply.toString()).to.be.equal(initialSupply.toString()); }); }); - }); - describe('transfer from', function () { - let spender; - let spenderWallet; + describe('balanceOf', function () { + describe('when the requested account has no tokens', function () { + it('returns zero', async function () { + const otherBalance = await contract.balanceOf(anotherAccount); + expect(otherBalance.toString()).to.be.equal('0'); + }); + }); - before(async function () { - spender = accounts[1].address; - spenderWallet = accounts[1].wallet; + describe('when the requested account has some tokens', function () { + it('returns the total amount of tokens', async function () { + const balance = await contract.balanceOf(initialHolder); + expect(balance.toString()).to.be.equal(initialSupply.toString()); + }); + }); }); - describe('when the token owner is not the zero address', function () { - let tokenOwner, tokenOwnerWallet; + describe('transfer from', function () { + let spender; + let spenderWallet; + before(async function () { - tokenOwner = initialHolder; - tokenOwnerWallet = accounts[0].wallet; + spender = accounts[1].address; + spenderWallet = accounts[1].wallet; }); - describe('when the recipient is not the zero address', function () { - let to, toWallet; + describe('when the token owner is not the zero address', function () { + let tokenOwner, tokenOwnerWallet; before(async function () { - to = anotherAccount; - toWallet = accounts[2].wallet; + tokenOwner = accounts[0].address; + tokenOwnerWallet = accounts[0].wallet; }); - describe('when the spender has enough allowance', function () { + describe('when the recipient is not the zero address', function () { + let to, toWallet; before(async function () { - await contract.approve(spender, initialSupply, { from: initialHolder }); + to = accounts[2].address; + toWallet = accounts[2].wallet; }); - describe('when the token owner has enough balance', function () { + describe('when the spender has enough tokens', function () { let amount, tx; before(async function () { amount = initialSupply; - tx = await contract.connect(spenderWallet).transferFrom(tokenOwner, to, amount); }); - it('transfers the requested amount', async function () { + it ('contract owner transfers tokens', async function () { + tx = await contract.connect(tokenOwnerWallet).transfer(to, amount); const ownerBalance = await contract.balanceOf(tokenOwner); const toBalance = await contract.balanceOf(to); expect(ownerBalance.toString()).to.be.equal('0'); expect(toBalance.toString()).to.be.equal(amount.toString()); - }); - - it('decreases the spender allowance', async function () { - const allowance = await contract.allowance(tokenOwner, spender); - expect(allowance.toString()).to.be.equal('0'); - }); - it('emits a transfer event', async function () { - await expect(tx) - .to.emit(contract, 'Transfer') - .withArgs(tokenOwnerWallet.address, toWallet.address, amount); }); - it('emits an approval event', async function () { - await expect(tx) - .to.emit(contract, 'Approval') - .withArgs(tokenOwnerWallet.address, spenderWallet.address, await contract.allowance(tokenOwner, spender)); + // FIXME + if (testTitles[i].testName !== HTS) { + it('emits a transfer event', async function () { + await expect(tx) + .to.emit(contract, 'Transfer') + .withArgs(tokenOwnerWallet.address, toWallet.address, amount); + }); + } + + it ('other account transfers tokens back to owner', async function () { + tx = await contract.connect(toWallet).transfer(tokenOwner, amount); + const ownerBalance = await contract.balanceOf(tokenOwner); + const toBalance = await contract.balanceOf(to); + expect(ownerBalance.toString()).to.be.equal(amount.toString()); + expect(toBalance.toString()).to.be.equal('0'); }); }); - describe('when the token owner does not have enough balance', function () { - it('reverts', async function () { - await contract.transfer(to, 1, { from: tokenOwner }); - await expectRevert( - contract.connect(spenderWallet).transferFrom(tokenOwner, to, initialSupply), - 'CALL_EXCEPTION' - ); + // FIXME + if (testTitles[i].testName !== HTS) { + describe('when the spender has enough allowance', function () { + before(async function () { + await contract.connect(tokenOwnerWallet).approve(spender, initialSupply); + }); + + describe('when the token owner has enough balance', function () { + let amount, tx; + before(async function () { + amount = initialSupply; + const ownerBalance = await contract.balanceOf(tokenOwner); + const toBalance = await contract.balanceOf(to); + expect(ownerBalance.toString()).to.be.equal(amount.toString()); + expect(toBalance.toString()).to.be.equal('0'); + }); + + it('transfers the requested amount', async function () { + tx = await contract.connect(spenderWallet).transferFrom(tokenOwner, to, initialSupply); + const ownerBalance = await contract.balanceOf(tokenOwner); + const toBalance = await contract.balanceOf(to); + expect(ownerBalance.toString()).to.be.equal('0'); + expect(toBalance.toString()).to.be.equal(amount.toString()); + }); + + it('decreases the spender allowance', async function () { + const allowance = await contract.allowance(tokenOwner, spender); + expect(allowance.toString()).to.be.equal('0'); + }); + + it('emits a transfer event', async function () { + await expect(tx) + .to.emit(contract, 'Transfer') + .withArgs(tokenOwnerWallet.address, toWallet.address, amount); + }); + + it('emits an approval event', async function () { + await expect(tx) + .to.emit(contract, 'Approval') + .withArgs(tokenOwnerWallet.address, spenderWallet.address, await contract.allowance(tokenOwner, spender)); + }); + }); + + describe('when the token owner does not have enough balance', function () { + let amount; + + beforeEach('reducing balance', async function () { + amount = initialSupply; + await contract.transfer(to, 1); + }); + + it('reverts', async function () { + await expectRevert( + contract.connect(spenderWallet).transferFrom(tokenOwner, to, amount), + 'CALL_EXCEPTION' + ); + }); + }); }); - }); - }); - describe('when the spender does not have enough allowance', function () { - let allowance; - - before(async function () { - allowance = initialSupply.sub(1); - await contract.approve(spender, allowance, { from: tokenOwner }); - }); - - describe('when the token owner has enough balance', function () { - it('reverts', async function () { - await expectRevert( - contract.connect(spenderWallet).transferFrom(tokenOwner, to, initialSupply), - `CALL_EXCEPTION`, - ); + describe('when the spender does not have enough allowance', function () { + let allowance; + + before(async function () { + allowance = initialSupply.sub(1); + }); + + beforeEach(async function () { + await contract.approve(spender, allowance); + }); + + describe('when the token owner has enough balance', function () { + let amount; + before(async function () { + amount = initialSupply; + }); + + it('reverts', async function () { + await expectRevert( + contract.connect(spenderWallet).transferFrom(tokenOwner, to, amount), + `CALL_EXCEPTION`, + ); + }); + }); + + describe('when the token owner does not have enough balance', function () { + let amount; + before(async function () { + amount = allowance; + }); + + beforeEach('reducing balance', async function () { + await contract.transfer(to, 2); + }); + + it('reverts', async function () { + await expectRevert( + contract.connect(spenderWallet).transferFrom(tokenOwner, to, amount), + `CALL_EXCEPTION`, + ); + }); + }); }); - }); - describe('when the token owner does not have enough balance', function () { - it('reverts', async function () { - await contract.transfer(to, 2, { from: tokenOwner }); - await expectRevert( - contract.connect(spenderWallet).transferFrom(tokenOwner, to, allowance), - `CALL_EXCEPTION`, - ); + describe('when the spender has unlimited allowance', function () { + beforeEach(async function () { + await contract.connect(tokenOwnerWallet).approve(spender, ethers.constants.MaxUint256); + }); + + it('does not decrease the spender allowance', async function () { + await contract.connect(spenderWallet).transferFrom(tokenOwner, to, 1); + const allowance = await contract.allowance(tokenOwner, spender); + expect(allowance.toString()).to.be.equal(ethers.constants.MaxUint256.toString()); + }); + + it('does not emit an approval event', async function () { + await expect(await contract.connect(spenderWallet).transferFrom(tokenOwner, to, 1)) + .to.not.emit(contract, 'Approval'); + }); }); - }); + } }); - describe('when the spender has unlimited allowance', function () { - beforeEach(async function () { - await contract.connect(tokenOwnerWallet).approve(spender, ethers.constants.MaxUint256); - }); + describe('when the recipient is the zero address', function () { + let amount, to, tokenOwnerWallet; - it('does not decrease the spender allowance', async function () { - await contract.connect(spenderWallet).transferFrom(tokenOwner, to, 1); - const allowance = await contract.allowance(tokenOwner, spender); - expect(allowance.toString()).to.be.equal(ethers.constants.MaxUint256.toString()); + beforeEach(async function () { + amount = initialSupply; + to = ethers.constants.AddressZero; + tokenOwnerWallet = accounts[0].wallet; + await contract.connect(tokenOwnerWallet).approve(spender, amount); }); - it('does not emit an approval event', async function () { - await expect(await contract.connect(spenderWallet).transferFrom(tokenOwner, to, 1)) - .to.not.emit(contract, 'Approval'); + it('reverts', async function () { + await expectRevert(contract.connect(spenderWallet).transferFrom(tokenOwner, to, amount), + `CALL_EXCEPTION`); }); }); }); - - describe('when the recipient is the zero address', function () { - it('reverts', async function () { - const amount = initialSupply; - const to = ethers.constants.AddressZero; - const tokenOwnerWallet = accounts[0].wallet; - await contract.connect(tokenOwnerWallet).approve(spender, amount); - - await expectRevert(contract.connect(spenderWallet).transferFrom(tokenOwner, to, amount), - `CALL_EXCEPTION`); - }); - }); }); }); }); - }); + } const expectRevert = async (promise, code) => { const tx = await promise; @@ -257,7 +327,7 @@ describe('ERC20 Acceptance Tests', function () { } }; - const deployErc20 = async (constructorArgs:any[] = [], contractJson) => { + async function deployErc20(constructorArgs:any[] = [], contractJson) { const factory = new ethers.ContractFactory(contractJson.abi, contractJson.bytecode, accounts[0].wallet); let contract = await factory.deploy(...constructorArgs); await contract.deployed(); @@ -267,4 +337,27 @@ describe('ERC20 Acceptance Tests', function () { contract = new ethers.Contract(receipt.to, contractJson.abi, accounts[0].wallet); return contract; }; -}); + + const createHTS = async(tokenName, symbol, adminAccount, initialSupply, abi, associatedAccounts) => { + const htsResult = await servicesNode.createHTS({ + tokenName, + symbol, + treasuryAccountId: adminAccount.accountId.toString(), + initialSupply, + adminPrivateKey: adminAccount.privateKey, + }); + + // Associate and approve token for all accounts + for (const account of associatedAccounts) { + await servicesNode.associateHTSToken(account.accountId, htsResult.receipt.tokenId, account.privateKey, htsResult.client); + await servicesNode.approveHTSToken(account.accountId, htsResult.receipt.tokenId, htsResult.client); + } + + // Setup initial balance of token owner account + await servicesNode.transferHTSToken(accounts[0].accountId, htsResult.receipt.tokenId, initialSupply, htsResult.client); + const evmAddress = Utils.idToEvmAddress(htsResult.receipt.tokenId.toString()); + const contract = new ethers.Contract(evmAddress, abi, accounts[0].wallet); + + return contract; + }; +}); \ No newline at end of file diff --git a/packages/server/tests/clients/servicesClient.ts b/packages/server/tests/clients/servicesClient.ts index 28673ff29..419676a36 100644 --- a/packages/server/tests/clients/servicesClient.ts +++ b/packages/server/tests/clients/servicesClient.ts @@ -38,6 +38,8 @@ import { TransferTransaction, ContractCreateFlow, FileUpdateTransaction, + TransactionId, + AccountAllowanceApproveTransaction, AccountBalance, } from '@hashgraph/sdk'; import { Logger } from 'pino'; @@ -243,6 +245,7 @@ export default class ServicesClient { accountInfo.accountId, accountInfo.contractAccountId, servicesClient, + privateKey, wallet ); }; @@ -289,6 +292,78 @@ export default class ServicesClient { return ethers.BigNumber.from(balance.hbars.toTinybars().toString()).mul(ServicesClient.TINYBAR_TO_WEIBAR_COEF); } + + async createHTS( args = { + tokenName: 'Default Name', + symbol: 'HTS', + treasuryAccountId: '0.0.2', + initialSupply: 5000, + adminPrivateKey: this.DEFAULT_KEY, + }) { + const {} = args; + + const expiration = new Date(); + expiration.setDate(expiration.getDate() + 30); + + const htsClient = Client.forNetwork(JSON.parse(this.network)); + htsClient.setOperator(AccountId.fromString(args.treasuryAccountId), args.adminPrivateKey); + + const tokenCreate = await (await new TokenCreateTransaction() + .setTokenName(args.tokenName) + .setTokenSymbol(args.symbol) + .setExpirationTime(expiration) + .setDecimals(18) + .setTreasuryAccountId(AccountId.fromString(args.treasuryAccountId)) + .setInitialSupply(args.initialSupply) + .setTransactionId(TransactionId.generate(AccountId.fromString(args.treasuryAccountId))) + .setNodeAccountIds([htsClient._network.getNodeAccountIdsForExecute()[0]])) + .execute(htsClient); + + const receipt = await tokenCreate.getReceipt(this.client); + return { + client: htsClient, + receipt + }; + } + + async associateHTSToken(accountId, tokenId, privateKey, htsClient) { + const tokenAssociate = await (await new TokenAssociateTransaction() + .setAccountId(accountId) + .setTokenIds([tokenId]) + .freezeWith(htsClient) + .sign(privateKey)) + .execute(htsClient); + + await tokenAssociate.getReceipt(htsClient); + this.logger.info(`HTS Token ${tokenId} associated to : ${accountId}`); + }; + + async approveHTSToken(spenderId, tokenId, htsClient) { + const amount = 10000; + const tokenApprove = await (new AccountAllowanceApproveTransaction() + .addTokenAllowance(tokenId, spenderId, amount)) + .execute(htsClient); + + await tokenApprove.getReceipt(htsClient); + this.logger.info(`${amount} of HTS Token ${tokenId} can be spent by ${spenderId}`); + }; + + async transferHTSToken(accountId, tokenId, amount, fromId = this.client.operatorAccountId) { + try { + const tokenTransfer = await (await new TransferTransaction() + .addTokenTransfer(tokenId, fromId, -amount) + .addTokenTransfer(tokenId, accountId, amount)) + .execute(this.client); + + const rec = await tokenTransfer.getReceipt(this.client); + this.logger.info(`${amount} of HTS Token ${tokenId} can be spent by ${accountId}`); + this.logger.debug(rec); + } + catch(e) { + this.logger.debug(e); + } + }; + } export class AliasAccount { @@ -297,13 +372,15 @@ export class AliasAccount { public readonly accountId: AccountId; public readonly address: string; public readonly client: ServicesClient; + public readonly privateKey: PrivateKey; public readonly wallet: ethers.Wallet; - constructor(_alias, _accountId, _address, _client, _wallet) { + constructor(_alias, _accountId, _address, _client, _privateKey, _wallet) { this.alias = _alias; this.accountId = _accountId; this.address = _address; this.client = _client; + this.privateKey = _privateKey; this.wallet = _wallet; }