From d446ed56ceba5fd4c0d612a3708e52aba9d0ff00 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Fri, 11 Nov 2022 12:27:41 +0800 Subject: [PATCH 01/39] Add burntokens rpc --- .../category/token/burnTokens.test.ts | 104 ++++++++++++++++++ .../jellyfish-api-core/src/category/token.ts | 15 +++ .../src/containers/DeFiDContainer.ts | 2 +- .../src/containers/RegTestContainer/index.ts | 1 + 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 packages/jellyfish-api-core/__tests__/category/token/burnTokens.test.ts diff --git a/packages/jellyfish-api-core/__tests__/category/token/burnTokens.test.ts b/packages/jellyfish-api-core/__tests__/category/token/burnTokens.test.ts new file mode 100644 index 0000000000..08f9721d20 --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/token/burnTokens.test.ts @@ -0,0 +1,104 @@ +import { Testing } from '@defichain/jellyfish-testing' +import { MasterNodeRegTestContainer } from '@defichain/testcontainers/dist/index' + +describe('burnTokens', () => { + const container = new MasterNodeRegTestContainer() + const testing = Testing.create(container) + + let address: string + const symbolDBTC = 'DBTC' + + beforeAll(async () => { + await container.start() + await setup() + }) + + afterAll(async () => { + await container.start() + }) + + async function setup (): Promise { + await container.waitForWalletCoinbaseMaturity() + + address = await testing.generateAddress() + + await testing.rpc.token.createToken({ + symbol: symbolDBTC, + name: symbolDBTC, + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: address + }) + + await testing.generate(1) + + await testing.generate(1) + await testing.rpc.token.mintTokens(`10@${symbolDBTC}`) + await testing.generate(1) + + await testing.container.fundAddress(address, 10) + await testing.generate(1) + } + + it('should throw error if called before GrandCentral height', async () => { + const promise = testing.rpc.token.burnTokens(`2@${symbolDBTC}`, address) + await expect(promise).rejects.toThrowError('called before GrandCentral height') + }) + + it('should throw an error if invalid value is provided for amount', async () => { + // Move to grand central height + await testing.generate(150) + + // @ts-expect-error + await expect(testing.rpc.token.burnTokens(null, address)).rejects.toThrow('Invalid parameters, argument "amounts" must not be null') + await expect(testing.rpc.token.burnTokens('', address)).rejects.toThrow(': Invalid amount') + await expect(testing.rpc.token.burnTokens(`A@${symbolDBTC}`, address)).rejects.toThrow(': Invalid amount') + await expect(testing.rpc.token.burnTokens('2@ABC', address)).rejects.toThrow(': Invalid Defi token: ABC') + await expect(testing.rpc.token.burnTokens(`-2@${symbolDBTC}`, address)).rejects.toThrow('RpcApiError: \': Amount out of range\', code: -3, method: burntokens') + }) + + it('should throw an error if invalid value is provided for from', async () => { + // @ts-expect-error + await expect(testing.rpc.token.burnTokens(`2@${symbolDBTC}`, null)).rejects.toThrow('Invalid parameters, argument "from" must not be null') + await expect(testing.rpc.token.burnTokens(`2@${symbolDBTC}`, '')).rejects.toThrow('recipient () does not refer to any valid address') + await expect(testing.rpc.token.burnTokens(`2@${symbolDBTC}`, 'ABC')).rejects.toThrow('recipient (ABC) does not refer to any valid address') + }) + + it('should throw an error if not enough tokens are available to burn', async () => { + const promise = testing.rpc.token.burnTokens(`11@${symbolDBTC}`, address) + await expect(promise).rejects.toThrow('RpcApiError: \'Test BurnTokenTx execution failed:\nnot enough tokens exist to subtract this amount\', code: -32600, method: burntokens') + }) + + it('should burn tokens without context', async () => { + const burnTxId = await testing.rpc.token.burnTokens(`1@${symbolDBTC}`, address) + expect(burnTxId).not.toBe(null) + + await testing.generate(1) + + const tokensAfterBurn = await testing.rpc.account.getAccount(address) + expect(tokensAfterBurn[0]).toStrictEqual(`9.00000000@${symbolDBTC}`) + }) + + it('should burn tokens with context', async () => { + const burnTxId = await testing.rpc.token.burnTokens(`1@${symbolDBTC}`, address, address) + expect(burnTxId).not.toBe(null) + + await testing.generate(1) + + const tokensAfterBurn = await testing.rpc.account.getAccount(address) + expect(tokensAfterBurn[0]).toStrictEqual(`8.00000000@${symbolDBTC}`) + }) + + it('should burn tokens with utxos', async () => { + const { txid, vout } = await testing.container.fundAddress(address, 10) + + const burnTxId = await testing.rpc.token.burnTokens(`1@${symbolDBTC}`, address, address, [{ txid, vout }]) + expect(burnTxId).not.toBe(null) + + await testing.generate(1) + + const tokensAfterBurn = await testing.rpc.account.getAccount(address) + expect(tokensAfterBurn[0]).toStrictEqual(`7.00000000@${symbolDBTC}`) + }) +}) diff --git a/packages/jellyfish-api-core/src/category/token.ts b/packages/jellyfish-api-core/src/category/token.ts index e4b80847d6..4a9b596309 100644 --- a/packages/jellyfish-api-core/src/category/token.ts +++ b/packages/jellyfish-api-core/src/category/token.ts @@ -98,6 +98,21 @@ export class Token { async mintTokens (amountToken: string, utxos: UTXO[] = []): Promise { return await this.client.call('minttokens', [amountToken, utxos], 'number') } + + /** + * Creates a transaction to burn tokens. + * + * @param {string} amounts Amount as json string, or array. Example: '[ \"amount@token\" ]' + * @param {string} from Address containing tokens to be burned + * @param {string} context Additional data necessary for specific burn type + * @param {UTXO[]} [utxos = []] A json array of json objects. Provide it if you want to spent specific UTXOs + * @param {string} [utxos.txid] The transaction id + * @param {number} [utxos.vout] The output number + * @return {Promise} Transaction hash + */ + async burnTokens (amounts: string, from: string, context?: string, utxos: UTXO[] = []): Promise { + return await this.client.call('burntokens', [{ amounts, from, context }, utxos], 'number') + } } export interface TokenResult { diff --git a/packages/testcontainers/src/containers/DeFiDContainer.ts b/packages/testcontainers/src/containers/DeFiDContainer.ts index a7eca2268c..f20923ae07 100644 --- a/packages/testcontainers/src/containers/DeFiDContainer.ts +++ b/packages/testcontainers/src/containers/DeFiDContainer.ts @@ -35,7 +35,7 @@ export abstract class DeFiDContainer extends DockerContainer { if (process?.env?.DEFICHAIN_DOCKER_IMAGE !== undefined) { return process.env.DEFICHAIN_DOCKER_IMAGE } - return 'defi/defichain:HEAD-49fba65ce' + return 'defi/defichain:HEAD-9422accf1' } public static readonly DefaultStartOptions = { diff --git a/packages/testcontainers/src/containers/RegTestContainer/index.ts b/packages/testcontainers/src/containers/RegTestContainer/index.ts index 1d9daaccbf..341e1c96a7 100644 --- a/packages/testcontainers/src/containers/RegTestContainer/index.ts +++ b/packages/testcontainers/src/containers/RegTestContainer/index.ts @@ -40,6 +40,7 @@ export class RegTestContainer extends DeFiDContainer { '-fortcanningspringheight=13', '-fortcanninggreatworldheight=14', '-fortcanningepilogueheight=15', + '-grandcentralheight=150', '-regtest-skip-loan-collateral-validation' ] From 5d20befaedad41925c8ed9b6888cff759a79129d Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Fri, 11 Nov 2022 12:52:27 +0800 Subject: [PATCH 02/39] Add minttokens tests --- .../category/token/mintTokens.test.ts | 212 +++++++++++++++++- 1 file changed, 210 insertions(+), 2 deletions(-) diff --git a/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts b/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts index 46e980329d..ba279de2ae 100644 --- a/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts @@ -1,6 +1,7 @@ import { MasterNodeRegTestContainer } from '@defichain/testcontainers' import { ContainerAdapterClient } from '../../container_adapter_client' -import { RpcApiError } from '../../../src' +import { BigNumber, RpcApiError } from '../../../src' +import { TestingGroup } from '@defichain/jellyfish-testing' describe('Token', () => { const container = new MasterNodeRegTestContainer() @@ -8,7 +9,6 @@ describe('Token', () => { beforeAll(async () => { await container.start() - await container.waitForReady() await container.waitForWalletCoinbaseMaturity() await setup() }) @@ -86,3 +86,211 @@ describe('Token', () => { expect(tokenBalances[1]).toStrictEqual('5.00000000@2') }) }) + +describe('Consortium', () => { + const tGroup = TestingGroup.create(4) + let account0: string, account1: string, account2: string, account3: string + let idBTC: string, idDOGE: string + const symbolBTC = 'BTC' + const symbolDOGE = 'DOGE' + + beforeAll(async () => { + await tGroup.start() + + account0 = await tGroup.get(0).generateAddress() + account1 = await tGroup.get(1).generateAddress() + account2 = await tGroup.get(2).generateAddress() + account3 = await tGroup.get(3).generateAddress() + + await tGroup.get(0).token.create({ + symbol: symbolBTC, + name: symbolBTC, + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: account0 + }) + await tGroup.get(0).generate(1) + + await tGroup.get(0).token.create({ + symbol: symbolDOGE, + name: symbolDOGE, + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: account0 + }) + await tGroup.get(0).generate(1) + + await tGroup.get(0).container.fundAddress(account0, 10) + await tGroup.get(0).container.fundAddress(account1, 10) + await tGroup.get(0).container.fundAddress(account2, 10) + await tGroup.get(0).container.fundAddress(account3, 10) + + await tGroup.get(0).generate(1) + + idBTC = await tGroup.get(0).token.getTokenId(symbolBTC) + idDOGE = await tGroup.get(0).token.getTokenId(symbolDOGE) + + // Move to grand central height + await tGroup.get(0).container.generate(150 - await tGroup.get(0).container.getBlockCount()) + }) + + afterAll(async () => { + await tGroup.stop() + }) + + async function setGovAttr (ATTRIBUTES: object): Promise { + const hash = await tGroup.get(0).rpc.masternode.setGov({ ATTRIBUTES }) + expect(hash).toBeTruthy() + await tGroup.get(0).generate(1) + } + + it('should throw an error if foundation or consortium member authorization is not present', async () => { + await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolBTC}`)).rejects.toThrow('Need foundation or consortium member authorization') + await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow('Need foundation or consortium member authorization') + await expect(tGroup.get(3).rpc.token.mintTokens(`1@${symbolBTC}`)).rejects.toThrow('Need foundation or consortium member authorization') + }) + + it('should throw an error if consortium is enabled but members are not set', async () => { + // Enable consortium + await setGovAttr({ 'v0/params/feature/consortium_enabled': 'true' }) + + await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolBTC}`)).rejects.toThrow('Need foundation or consortium member authorization') + await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow('Need foundation or consortium member authorization') + await expect(tGroup.get(3).rpc.token.mintTokens(`1@${symbolBTC}`)).rejects.toThrow('Need foundation or consortium member authorization') + }) + + it('should throw an error if the token is not specified in governance vars', async () => { + // Set consortium members for BTC + await setGovAttr({ + [`v0/consortium/${idBTC}/members`]: `{ + "01":{ + "name":"account2BTC", + "ownerAddress":"${account2}", + "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", + "dailyMintLimit":2.00000000, + "mintLimit":5.00000000 + }, + "02":{ + "name":"account3BTC", + "ownerAddress":"${account3}", + "backingId":"6c67fe93cad3d6a4982469a9b6708cdde2364f183d3698d3745f86eeb8ba99d5", + "dailyMintLimit":2.00000000, + "mintLimit":5.00000000 + } + }` + }) + + // Set global consortium mint limit for BTC + await setGovAttr({ [`v0/consortium/${idBTC}/mint_limit`]: '1' }) + + // Trying to mint DOGE + await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow('Need foundation or consortium member authorization') + }) + + it('should throw an error if member daily mint limit exceeds', async () => { + await expect(tGroup.get(2).rpc.token.mintTokens(`3@${symbolBTC}`)).rejects.toThrow(`RpcApiError: 'Test MintTokenTx execution failed:\nYou will exceed your daily mint limit for ${symbolBTC} token by minting this amount', code: -32600, method: minttokens`) + }) + + it('should throw an error if member maximum mint limit exceeds', async () => { + await expect(tGroup.get(2).rpc.token.mintTokens(`6@${symbolBTC}`)).rejects.toThrow(`RpcApiError: 'Test MintTokenTx execution failed:\nYou will exceed your maximum mint limit for ${symbolBTC} token by minting this amount!', code: -32600, method: minttokens`) + }) + + it('should throw an error if global mint limit exceeds', async () => { + await expect(tGroup.get(2).rpc.token.mintTokens(`1.00000001@${symbolBTC}`)).rejects.toThrow(`RpcApiError: 'Test MintTokenTx execution failed:\nYou will exceed global maximum consortium mint limit for ${symbolBTC} token by minting this amount!', code: -32600, method: minttokens`) + }) + + it('should be able to mint tokens', async () => { + const hash = await tGroup.get(2).rpc.token.mintTokens(`1@${symbolBTC}`) + expect(hash).toBeTruthy() + await tGroup.get(2).generate(1) + + expect((await tGroup.get(2).rpc.account.getAccount(account2))[0]).toStrictEqual(`1.00000000@${symbolBTC}`) + + // Check global consortium attributes + const attr = (await tGroup.get(2).rpc.masternode.getGov('ATTRIBUTES')).ATTRIBUTES + expect(attr[`v0/live/economy/consortium/${idBTC}/minted`]).toStrictEqual(new BigNumber('1')) + expect(attr[`v0/live/economy/consortium/${idBTC}/burnt`]).toStrictEqual(new BigNumber('0')) + expect(attr[`v0/live/economy/consortium/${idBTC}/supply`]).toStrictEqual(new BigNumber('1')) + }) + + it('should return correct governance attribute values', async () => { + // Set global mint limits for DOGE + await setGovAttr({ + [`v0/consortium/${idDOGE}/mint_limit`]: '6', + [`v0/consortium/${idDOGE}/daily_mint_limit`]: '6' + }) + + // Add consortium members for DOGE + await setGovAttr({ + [`v0/consortium/${idDOGE}/members`]: `{ + "01": { + "name":"account2DOGE", + "ownerAddress":"${account2}", + "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", + "dailyMintLimit":2.00000000, + "mintLimit":5.00000000 + }, + "02": { + "name":"account1DOGE", + "ownerAddress":"${account1}", + "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", + "dailyMintLimit":2.00000000, + "mintLimit":5.00000000 + } + }` + }) + + const attr0 = (await tGroup.get(0).rpc.masternode.getGov('ATTRIBUTES')).ATTRIBUTES + expect(attr0[`v0/consortium/${idBTC}/members`]).toStrictEqual(`{"01":{"name":"account2BTC","ownerAddress":"${account2}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":2.00000000,"status":0},"02":{"name":"account3BTC","ownerAddress":"${account3}","backingId":"6c67fe93cad3d6a4982469a9b6708cdde2364f183d3698d3745f86eeb8ba99d5","mintLimit":5.00000000,"dailyMintLimit":2.00000000,"status":0}}`) + expect(attr0[`v0/consortium/${idBTC}/mint_limit`]).toStrictEqual('1') + expect(attr0[`v0/consortium/${idDOGE}/members`]).toStrictEqual(`{"01":{"name":"account2DOGE","ownerAddress":"${account2}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":2.00000000,"status":0},"02":{"name":"account1DOGE","ownerAddress":"${account1}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":2.00000000,"status":0}}`) + expect(attr0[`v0/consortium/${idDOGE}/mint_limit`]).toStrictEqual('6') + expect(attr0[`v0/consortium/${idDOGE}/daily_mint_limit`]).toStrictEqual('6') + + const hash = await tGroup.get(2).rpc.token.mintTokens(`2@${symbolDOGE}`) + expect(hash).toBeTruthy() + await tGroup.get(2).generate(1) + + expect((await tGroup.get(2).rpc.account.getAccount(account2))).toStrictEqual([ + `1.00000000@${symbolBTC}`, + `2.00000000@${symbolDOGE}` + ]) + + const attr2 = (await tGroup.get(2).rpc.masternode.getGov('ATTRIBUTES')).ATTRIBUTES + expect(attr2[`v0/live/economy/consortium/${idBTC}/minted`]).toStrictEqual(new BigNumber(1)) + expect(attr2[`v0/live/economy/consortium/${idBTC}/burnt`]).toStrictEqual(new BigNumber(0)) + expect(attr2[`v0/live/economy/consortium/${idBTC}/supply`]).toStrictEqual(new BigNumber(1)) + + expect(attr2[`v0/live/economy/consortium/${idDOGE}/minted`]).toStrictEqual(new BigNumber(2)) + expect(attr2[`v0/live/economy/consortium/${idDOGE}/burnt`]).toStrictEqual(new BigNumber(0)) + expect(attr2[`v0/live/economy/consortium/${idDOGE}/supply`]).toStrictEqual(new BigNumber(2)) + + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/minted`]).toStrictEqual(new BigNumber(1)) + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/daily_minted`]).toStrictEqual('144/1.00000000') + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/burnt`]).toStrictEqual(new BigNumber(0)) + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/supply`]).toStrictEqual(new BigNumber(1)) + expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/01/minted`]).toStrictEqual(new BigNumber(2)) + expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/01/daily_minted`]).toStrictEqual('144/2.00000000') + expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/01/burnt`]).toStrictEqual(new BigNumber(0)) + expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/01/supply`]).toStrictEqual(new BigNumber(2)) + }) + + it('should throw an error if tried to mint a token while not being an active member of the consortium', async () => { + await setGovAttr({ + [`v0/consortium/${idDOGE}/members`]: `{ + "01": { + "name":"account2DOGE", + "ownerAddress":"${account2}", + "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", + "dailyMintLimit":2.00000000, + "mintLimit":5.00000000, + "status":1 + } + }` + }) + + await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow(`Cannot mint token, not an active member of consortium for ${symbolDOGE}!`) + }) +}) From 28aebb9826aba3302a20599b053d98a532d57b2b Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Sun, 13 Nov 2022 15:21:15 +0800 Subject: [PATCH 03/39] Add setgov tests --- .../category/account/getBurnInfo.test.ts | 1 + .../category/masternode/setGov.test.ts | 189 +++++++++++++++++- .../category/token/mintTokens.test.ts | 159 +++++++++------ .../src/containers/DeFiDContainer.ts | 2 +- 4 files changed, 292 insertions(+), 59 deletions(-) diff --git a/packages/jellyfish-api-core/__tests__/category/account/getBurnInfo.test.ts b/packages/jellyfish-api-core/__tests__/category/account/getBurnInfo.test.ts index b92d2f0135..fd2aa51ea0 100644 --- a/packages/jellyfish-api-core/__tests__/category/account/getBurnInfo.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/account/getBurnInfo.test.ts @@ -50,6 +50,7 @@ it('should getBurnInfo', async () => { tokens: ['50.00000000@GOLD'], feeburn: new BigNumber('2'), auctionburn: new BigNumber(0), + consortiumtokens: [], emissionburn: new BigNumber('6274'), paybackburn: [], paybackfees: [], diff --git a/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts b/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts index 0a8e952f9e..6f7496494e 100644 --- a/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts @@ -2,7 +2,7 @@ import { BigNumber, RpcApiError } from '@defichain/jellyfish-api-core' import { GenesisKeys, MasterNodeRegTestContainer, StartFlags } from '@defichain/testcontainers' import { createPoolPair, createToken } from '@defichain/testing' import { ContainerAdapterClient } from '../../container_adapter_client' -import { Testing } from '@defichain/jellyfish-testing' +import { Testing, TestingGroup } from '@defichain/jellyfish-testing' import { UTXO } from '@defichain/jellyfish-api-core/dist/category/masternode' describe('Masternode', () => { @@ -516,3 +516,190 @@ describe('setGov ATTRIBUTES loan dusd burn keys', () => { } }) }) + +describe('setGov consortium ATTRIBUTES', () => { + const tGroup = TestingGroup.create(2) + let account0: string, account1: string + let idBTC: string + const symbolBTC = 'BTC' + + beforeAll(async () => { + await tGroup.start() + await setup() + }) + + afterAll(async () => { + await tGroup.stop() + }) + + async function setup (): Promise { + account0 = await tGroup.get(0).generateAddress() + account1 = await tGroup.get(1).generateAddress() + + await tGroup.get(0).token.create({ + symbol: symbolBTC, + name: symbolBTC, + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: account0 + }) + + await tGroup.get(0).generate(1) + + await tGroup.get(0).container.fundAddress(account1, 10) + + await tGroup.get(0).generate(1) + + idBTC = await tGroup.get(0).token.getTokenId(symbolBTC) + } + + async function setGovAttr (ATTRIBUTES: object): Promise { + return await tGroup.get(0).rpc.masternode.setGov({ ATTRIBUTES }) + } + + it('should throw an error if \'v0/params/feature/consortium\' is not assigned a boolean', async () => { + const promise = tGroup.get(0).rpc.masternode.setGov({ + ATTRIBUTES: { 'v0/params/feature/consortium': '1' } + }) + await expect(promise).rejects.toThrow('RpcApiError: \'Boolean value must be either "true" or "false"\', code: -5, method: setgov') + }) + + it('should throw an error if the member owner address is empty', async () => { + await expect(setGovAttr({ + [`v0/consortium/${idBTC}/members`]: `{ + "01":{ + "name":"test", + "ownerAddress":"", + "backingId":"blablabla", + "mintLimit":10.00000000 + } + }` + })).rejects.toThrow('Invalid ownerAddress in consortium member data') + }) + + it('should throw an error if the member owner address is invalid', async () => { + await expect(setGovAttr({ + [`v0/consortium/${idBTC}/members`]: `{ + "01":{ + "name":"test", + "ownerAddress":"abc", + "backingId":"blablabla", + "mintLimit":10.00000000 + } + }` + })).rejects.toThrow('Invalid ownerAddress in consortium member data') + }) + + it('should throw an error if the consortium member name length is less than 3', async () => { + await expect(setGovAttr({ + [`v0/consortium/${idBTC}/members`]: `{ + "01":{ + "name":"ab", + "ownerAddress":"${account1}", + "backingId":"blablabla", + "mintLimit":10.00000000 + } + }` + })).rejects.toThrow('Member name too short, must be at least 3 chars long') + }) + + it('should throw an error if the member mint limit is invalid', async () => { + await expect(setGovAttr({ + [`v0/consortium/${idBTC}/members`]: `{ + "01":{ + "name":"test", + "ownerAddress":"${account1}", + "backingId":"blablabla", + "mintLimit":-10.00000000 + } + }` + })).rejects.toThrow('Mint limit is an invalid amount') + }) + + it('should throw an error if the member daily mint limit is invalid', async () => { + await expect(setGovAttr({ + [`v0/consortium/${idBTC}/members`]: `{ + "01":{ + "name":"test", + "ownerAddress":"${account1}", + "backingId":"blablabla", + "mintLimit":10.00000000, + "dailyMintLimit":-10.00000000 + } + }` + })).rejects.toThrow('Daily mint limit is an invalid amount') + }) + + it('should throw an error if the member object is invalid', async () => { + await expect(setGovAttr({ + [`v0/consortium/${idBTC}/members`]: `{ + "01":{ + "name":"test", + "ownerAddress":"${account1}", + "backingId":"blablabla", + "mintLimit":10.00000000, + } + }` + // An extra comma at line 662 that makes the object invalid + })).rejects.toThrow('RpcApiError: \'Not a valid consortium member object!\', code: -5, method: setgov') + }) + + it('should throw an error if the member status is invalid', async () => { + await expect(setGovAttr({ + [`v0/consortium/${idBTC}/members`]: `{ + "01":{ + "name":"test", + "ownerAddress":"${account1}", + "backingId":"blablabla", + "mintLimit":10.00000000, + "dailyMintLimit":1.00000000, + "status":-1 + } + }` + })).rejects.toThrow('Status must be a positive number') + + await expect(setGovAttr({ + [`v0/consortium/${idBTC}/members`]: `{ + "01":{ + "name":"test", + "ownerAddress":"${account1}", + "backingId":"blablabla", + "mintLimit":10.00000000, + "dailyMintLimit":1.00000000, + "status":2 + } + }` + })).rejects.toThrow('Status can be either 0 or 1') + }) + + it('should set member information', async () => { + // Move to grand central height + await tGroup.get(0).generate(150 - await tGroup.get(0).container.getBlockCount()) + + expect(await setGovAttr({ + [`v0/consortium/${idBTC}/mint_limit`]: '10', + [`v0/consortium/${idBTC}/mint_limit_daily`]: '1' + })).toBeTruthy() + + await tGroup.get(0).generate(1) + + expect(setGovAttr({ + [`v0/consortium/${idBTC}/members`]: `{ + "01":{ + "name":"test", + "ownerAddress":"${account1}", + "backingId":"blablabla", + "mintLimit":10.00000000, + "dailyMintLimit":1.00000000, + "status":1 + } + }` + })).toBeTruthy() + + await tGroup.get(0).generate(1) + + const attr = (await tGroup.get(0).rpc.masternode.getGov('ATTRIBUTES')).ATTRIBUTES + expect(attr['v0/consortium/1/members']).toStrictEqual(`{"01":{"name":"test","ownerAddress":"${account1}","backingId":"blablabla","mintLimit":10.00000000,"dailyMintLimit":1.00000000,"status":1}}`) + }) +}) diff --git a/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts b/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts index ba279de2ae..3c6b591e5b 100644 --- a/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts @@ -93,6 +93,7 @@ describe('Consortium', () => { let idBTC: string, idDOGE: string const symbolBTC = 'BTC' const symbolDOGE = 'DOGE' + const blocksPerDay = (60 * 60 * 24) / (10 * 60) // 144 in regtest beforeAll(async () => { await tGroup.start() @@ -146,62 +147,101 @@ describe('Consortium', () => { await tGroup.get(0).generate(1) } - it('should throw an error if foundation or consortium member authorization is not present', async () => { - await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolBTC}`)).rejects.toThrow('Need foundation or consortium member authorization') - await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow('Need foundation or consortium member authorization') - await expect(tGroup.get(3).rpc.token.mintTokens(`1@${symbolBTC}`)).rejects.toThrow('Need foundation or consortium member authorization') - }) + async function setMemberInfo (tokenId: string, memberInfo: Array<{ id: string, name: string, ownerAddress: string, mintLimit: string, dailyMintLimit: string }>): Promise { + const infoObjs = memberInfo.map(mi => ` + "${mi.id}":{ + "name":"${mi.name}", + "ownerAddress":"${mi.ownerAddress}", + "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", + "dailyMintLimit":${mi.dailyMintLimit}, + "mintLimit":${mi.mintLimit} + }` + ) - it('should throw an error if consortium is enabled but members are not set', async () => { - // Enable consortium - await setGovAttr({ 'v0/params/feature/consortium_enabled': 'true' }) + return await setGovAttr({ [`v0/consortium/${tokenId}/members`]: `{${infoObjs.join(',')}}` }) + } + it('should throw an error if foundation or consortium member authorization is not present', async () => { await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolBTC}`)).rejects.toThrow('Need foundation or consortium member authorization') - await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow('Need foundation or consortium member authorization') await expect(tGroup.get(3).rpc.token.mintTokens(`1@${symbolBTC}`)).rejects.toThrow('Need foundation or consortium member authorization') + await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow('Need foundation or consortium member authorization') + await expect(tGroup.get(3).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow('Need foundation or consortium member authorization') }) it('should throw an error if the token is not specified in governance vars', async () => { - // Set consortium members for BTC + // Enable consortium + await setGovAttr({ 'v0/params/feature/consortium': 'true' }) + + // Set global consortium mint limit for BTC await setGovAttr({ - [`v0/consortium/${idBTC}/members`]: `{ - "01":{ - "name":"account2BTC", - "ownerAddress":"${account2}", - "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", - "dailyMintLimit":2.00000000, - "mintLimit":5.00000000 - }, - "02":{ - "name":"account3BTC", - "ownerAddress":"${account3}", - "backingId":"6c67fe93cad3d6a4982469a9b6708cdde2364f183d3698d3745f86eeb8ba99d5", - "dailyMintLimit":2.00000000, - "mintLimit":5.00000000 - } - }` + [`v0/consortium/${idBTC}/mint_limit`]: '10', + [`v0/consortium/${idBTC}/mint_limit_daily`]: '5' }) - // Set global consortium mint limit for BTC - await setGovAttr({ [`v0/consortium/${idBTC}/mint_limit`]: '1' }) + // Set consortium members for BTC + await setMemberInfo(idBTC, [{ + id: '01', + name: 'account1BTC', + ownerAddress: account1, + dailyMintLimit: '5.00000000', + mintLimit: '10.00000000' + }, { + id: '02', + name: 'account2BTC', + ownerAddress: account2, + dailyMintLimit: '5.00000000', + mintLimit: '10.00000000' + }, { + id: '03', + name: 'account3BTC', + ownerAddress: account3, + dailyMintLimit: '5.00000000', + mintLimit: '10.00000000' + }]) // Trying to mint DOGE await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow('Need foundation or consortium member authorization') }) it('should throw an error if member daily mint limit exceeds', async () => { - await expect(tGroup.get(2).rpc.token.mintTokens(`3@${symbolBTC}`)).rejects.toThrow(`RpcApiError: 'Test MintTokenTx execution failed:\nYou will exceed your daily mint limit for ${symbolBTC} token by minting this amount', code: -32600, method: minttokens`) + await expect(tGroup.get(2).rpc.token.mintTokens(`6@${symbolBTC}`)).rejects.toThrow(`RpcApiError: 'Test MintTokenTx execution failed:\nYou will exceed your daily mint limit for ${symbolBTC} token by minting this amount', code: -32600, method: minttokens`) }) it('should throw an error if member maximum mint limit exceeds', async () => { - await expect(tGroup.get(2).rpc.token.mintTokens(`6@${symbolBTC}`)).rejects.toThrow(`RpcApiError: 'Test MintTokenTx execution failed:\nYou will exceed your maximum mint limit for ${symbolBTC} token by minting this amount!', code: -32600, method: minttokens`) + await expect(tGroup.get(2).rpc.token.mintTokens(`11@${symbolBTC}`)).rejects.toThrow(`RpcApiError: 'Test MintTokenTx execution failed:\nYou will exceed your maximum mint limit for ${symbolBTC} token by minting this amount!', code: -32600, method: minttokens`) + }) + + it('should throw an error if global daily mint limit exceeds', async () => { + // Hit global daily mint limit + await tGroup.get(1).rpc.token.mintTokens(`5.0000000@${symbolBTC}`) + await tGroup.get(1).generate(5) + + await expect(tGroup.get(3).rpc.token.mintTokens(`1.00000000@${symbolBTC}`)).rejects.toThrow(`RpcApiError: 'Test MintTokenTx execution failed:\nYou will exceed global daily maximum consortium mint limit for ${symbolBTC} token by minting this amount.', code: -32600, method: minttokens`) }) it('should throw an error if global mint limit exceeds', async () => { - await expect(tGroup.get(2).rpc.token.mintTokens(`1.00000001@${symbolBTC}`)).rejects.toThrow(`RpcApiError: 'Test MintTokenTx execution failed:\nYou will exceed global maximum consortium mint limit for ${symbolBTC} token by minting this amount!', code: -32600, method: minttokens`) + // Move to next day + await tGroup.get(1).generate(blocksPerDay) + + // Hit global mint limit + await tGroup.get(1).rpc.token.mintTokens(`5.0000000@${symbolBTC}`) + await tGroup.get(1).generate(5) + + await expect(tGroup.get(3).rpc.token.mintTokens(`1.00000000@${symbolBTC}`)).rejects.toThrow(`RpcApiError: 'Test MintTokenTx execution failed:\nYou will exceed global maximum consortium mint limit for ${symbolBTC} token by minting this amount!', code: -32600, method: minttokens`) }) it('should be able to mint tokens', async () => { + // Move to next day + await tGroup.get(1).generate(blocksPerDay) + + await tGroup.get(1).rpc.token.burnTokens(`10.0000000@${symbolBTC}`, account1) + + await tGroup.get(1).generate(1) + + await setGovAttr({ + [`v0/consortium/${idBTC}/mint_limit`]: '20' + }) + const hash = await tGroup.get(2).rpc.token.mintTokens(`1@${symbolBTC}`) expect(hash).toBeTruthy() await tGroup.get(2).generate(1) @@ -210,8 +250,8 @@ describe('Consortium', () => { // Check global consortium attributes const attr = (await tGroup.get(2).rpc.masternode.getGov('ATTRIBUTES')).ATTRIBUTES - expect(attr[`v0/live/economy/consortium/${idBTC}/minted`]).toStrictEqual(new BigNumber('1')) - expect(attr[`v0/live/economy/consortium/${idBTC}/burnt`]).toStrictEqual(new BigNumber('0')) + expect(attr[`v0/live/economy/consortium/${idBTC}/minted`]).toStrictEqual(new BigNumber('11')) + expect(attr[`v0/live/economy/consortium/${idBTC}/burnt`]).toStrictEqual(new BigNumber('10')) expect(attr[`v0/live/economy/consortium/${idBTC}/supply`]).toStrictEqual(new BigNumber('1')) }) @@ -219,22 +259,22 @@ describe('Consortium', () => { // Set global mint limits for DOGE await setGovAttr({ [`v0/consortium/${idDOGE}/mint_limit`]: '6', - [`v0/consortium/${idDOGE}/daily_mint_limit`]: '6' + [`v0/consortium/${idDOGE}/mint_limit_daily`]: '6' }) // Add consortium members for DOGE await setGovAttr({ [`v0/consortium/${idDOGE}/members`]: `{ "01": { - "name":"account2DOGE", - "ownerAddress":"${account2}", + "name":"account1DOGE", + "ownerAddress":"${account1}", "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", "dailyMintLimit":2.00000000, "mintLimit":5.00000000 }, "02": { - "name":"account1DOGE", - "ownerAddress":"${account1}", + "name":"account2DOGE", + "ownerAddress":"${account2}", "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", "dailyMintLimit":2.00000000, "mintLimit":5.00000000 @@ -243,15 +283,17 @@ describe('Consortium', () => { }) const attr0 = (await tGroup.get(0).rpc.masternode.getGov('ATTRIBUTES')).ATTRIBUTES - expect(attr0[`v0/consortium/${idBTC}/members`]).toStrictEqual(`{"01":{"name":"account2BTC","ownerAddress":"${account2}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":2.00000000,"status":0},"02":{"name":"account3BTC","ownerAddress":"${account3}","backingId":"6c67fe93cad3d6a4982469a9b6708cdde2364f183d3698d3745f86eeb8ba99d5","mintLimit":5.00000000,"dailyMintLimit":2.00000000,"status":0}}`) - expect(attr0[`v0/consortium/${idBTC}/mint_limit`]).toStrictEqual('1') - expect(attr0[`v0/consortium/${idDOGE}/members`]).toStrictEqual(`{"01":{"name":"account2DOGE","ownerAddress":"${account2}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":2.00000000,"status":0},"02":{"name":"account1DOGE","ownerAddress":"${account1}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":2.00000000,"status":0}}`) + expect(attr0[`v0/consortium/${idBTC}/members`]).toStrictEqual(`{"01":{"name":"account1BTC","ownerAddress":"${account1}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":10.00000000,"dailyMintLimit":5.00000000,"status":0},"02":{"name":"account2BTC","ownerAddress":"${account2}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":10.00000000,"dailyMintLimit":5.00000000,"status":0},"03":{"name":"account3BTC","ownerAddress":"${account3}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":10.00000000,"dailyMintLimit":5.00000000,"status":0}}`) + expect(attr0[`v0/consortium/${idBTC}/mint_limit`]).toStrictEqual('20') + expect(attr0[`v0/consortium/${idBTC}/mint_limit_daily`]).toStrictEqual('5') + + expect(attr0[`v0/consortium/${idDOGE}/members`]).toStrictEqual(`{"01":{"name":"account1DOGE","ownerAddress":"${account1}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":2.00000000,"status":0},"02":{"name":"account2DOGE","ownerAddress":"${account2}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":2.00000000,"status":0}}`) expect(attr0[`v0/consortium/${idDOGE}/mint_limit`]).toStrictEqual('6') - expect(attr0[`v0/consortium/${idDOGE}/daily_mint_limit`]).toStrictEqual('6') + expect(attr0[`v0/consortium/${idDOGE}/mint_limit_daily`]).toStrictEqual('6') const hash = await tGroup.get(2).rpc.token.mintTokens(`2@${symbolDOGE}`) expect(hash).toBeTruthy() - await tGroup.get(2).generate(1) + await tGroup.get(2).generate(5) expect((await tGroup.get(2).rpc.account.getAccount(account2))).toStrictEqual([ `1.00000000@${symbolBTC}`, @@ -259,30 +301,33 @@ describe('Consortium', () => { ]) const attr2 = (await tGroup.get(2).rpc.masternode.getGov('ATTRIBUTES')).ATTRIBUTES - expect(attr2[`v0/live/economy/consortium/${idBTC}/minted`]).toStrictEqual(new BigNumber(1)) - expect(attr2[`v0/live/economy/consortium/${idBTC}/burnt`]).toStrictEqual(new BigNumber(0)) + expect(attr2[`v0/live/economy/consortium/${idBTC}/minted`]).toStrictEqual(new BigNumber(11)) + expect(attr2[`v0/live/economy/consortium/${idBTC}/burnt`]).toStrictEqual(new BigNumber(10)) expect(attr2[`v0/live/economy/consortium/${idBTC}/supply`]).toStrictEqual(new BigNumber(1)) + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/minted`]).toStrictEqual(new BigNumber(10)) + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/daily_minted`]).toStrictEqual('288/5.00000000') + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/burnt`]).toStrictEqual(new BigNumber(10)) + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/supply`]).toStrictEqual(new BigNumber(0)) + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/02/minted`]).toStrictEqual(new BigNumber(1)) + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/02/daily_minted`]).toStrictEqual('432/1.00000000') + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/02/burnt`]).toStrictEqual(new BigNumber(0)) + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/02/supply`]).toStrictEqual(new BigNumber(1)) expect(attr2[`v0/live/economy/consortium/${idDOGE}/minted`]).toStrictEqual(new BigNumber(2)) expect(attr2[`v0/live/economy/consortium/${idDOGE}/burnt`]).toStrictEqual(new BigNumber(0)) expect(attr2[`v0/live/economy/consortium/${idDOGE}/supply`]).toStrictEqual(new BigNumber(2)) - - expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/minted`]).toStrictEqual(new BigNumber(1)) - expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/daily_minted`]).toStrictEqual('144/1.00000000') - expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/burnt`]).toStrictEqual(new BigNumber(0)) - expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/supply`]).toStrictEqual(new BigNumber(1)) - expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/01/minted`]).toStrictEqual(new BigNumber(2)) - expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/01/daily_minted`]).toStrictEqual('144/2.00000000') - expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/01/burnt`]).toStrictEqual(new BigNumber(0)) - expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/01/supply`]).toStrictEqual(new BigNumber(2)) + expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/02/minted`]).toStrictEqual(new BigNumber(2)) + expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/02/daily_minted`]).toStrictEqual('432/2.00000000') + expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/02/burnt`]).toStrictEqual(new BigNumber(0)) + expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/02/supply`]).toStrictEqual(new BigNumber(2)) }) it('should throw an error if tried to mint a token while not being an active member of the consortium', async () => { await setGovAttr({ [`v0/consortium/${idDOGE}/members`]: `{ "01": { - "name":"account2DOGE", - "ownerAddress":"${account2}", + "name":"account1DOGE", + "ownerAddress":"${account1}", "backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf", "dailyMintLimit":2.00000000, "mintLimit":5.00000000, @@ -291,6 +336,6 @@ describe('Consortium', () => { }` }) - await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow(`Cannot mint token, not an active member of consortium for ${symbolDOGE}!`) + await expect(tGroup.get(1).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow(`Cannot mint token, not an active member of consortium for ${symbolDOGE}!`) }) }) diff --git a/packages/testcontainers/src/containers/DeFiDContainer.ts b/packages/testcontainers/src/containers/DeFiDContainer.ts index f20923ae07..475e8f510e 100644 --- a/packages/testcontainers/src/containers/DeFiDContainer.ts +++ b/packages/testcontainers/src/containers/DeFiDContainer.ts @@ -35,7 +35,7 @@ export abstract class DeFiDContainer extends DockerContainer { if (process?.env?.DEFICHAIN_DOCKER_IMAGE !== undefined) { return process.env.DEFICHAIN_DOCKER_IMAGE } - return 'defi/defichain:HEAD-9422accf1' + return 'defi/defichain:HEAD-01633efee' } public static readonly DefaultStartOptions = { From ced9ac162d4b4a61b49953f9ccfe31bfe224781e Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Sun, 13 Nov 2022 21:10:07 +0800 Subject: [PATCH 04/39] Add listprices tests for pagination --- .../category/oracle/listPrices.test.ts | 96 +++++++++++++++---- .../jellyfish-api-core/src/category/oracle.ts | 9 +- .../jellyfish-api-core/src/category/token.ts | 2 +- 3 files changed, 84 insertions(+), 23 deletions(-) diff --git a/packages/jellyfish-api-core/__tests__/category/oracle/listPrices.test.ts b/packages/jellyfish-api-core/__tests__/category/oracle/listPrices.test.ts index 733174e9b6..e59b63cf48 100644 --- a/packages/jellyfish-api-core/__tests__/category/oracle/listPrices.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/oracle/listPrices.test.ts @@ -8,32 +8,23 @@ describe('Oracle', () => { beforeAll(async () => { await container.start() - await container.waitForReady() await container.waitForWalletCoinbaseMaturity() }) - afterEach(async () => { - const data = await container.call('listoracles') - - for (let i = 0; i < data.length; i += 1) { - await container.call('removeoracle', [data[i]]) - } - - await container.generate(1) - }) - - afterAll(async () => { - await container.stop() - }) - - it('should listPrices', async () => { + beforeAll(async () => { const priceFeeds1 = { token: 'AAPL', currency: 'EUR' } const priceFeeds2 = { token: 'TSLA', currency: 'USD' } + const priceFeeds3 = { token: 'ABCD', currency: 'EUR' } + const priceFeeds4 = { token: 'EFGH', currency: 'USD' } const oracleId1 = await container.call('appointoracle', [await container.getNewAddress(), [priceFeeds1], 1]) const oracleId2 = await container.call('appointoracle', [await container.getNewAddress(), [priceFeeds1], 2]) const oracleId3 = await container.call('appointoracle', [await container.getNewAddress(), [priceFeeds2], 3]) const oracleId4 = await container.call('appointoracle', [await container.getNewAddress(), [priceFeeds2], 4]) + const oracleId5 = await container.call('appointoracle', [await container.getNewAddress(), [priceFeeds3], 1]) + const oracleId6 = await container.call('appointoracle', [await container.getNewAddress(), [priceFeeds3], 2]) + const oracleId7 = await container.call('appointoracle', [await container.getNewAddress(), [priceFeeds4], 3]) + const oracleId8 = await container.call('appointoracle', [await container.getNewAddress(), [priceFeeds4], 4]) await container.generate(1) @@ -51,23 +42,90 @@ describe('Oracle', () => { const prices4 = [{ tokenAmount: '2.0@TSLA', currency: 'USD' }] await container.call('setoracledata', [oracleId4, timestamp, prices4]) + const prices5 = [{ tokenAmount: '0.5@ABCD', currency: 'EUR' }] + await container.call('setoracledata', [oracleId5, timestamp, prices5]) + + const prices6 = [{ tokenAmount: '1.0@ABCD', currency: 'EUR' }] + await container.call('setoracledata', [oracleId6, timestamp, prices6]) + + const prices7 = [{ tokenAmount: '1.5@EFGH', currency: 'USD' }] + await container.call('setoracledata', [oracleId7, timestamp, prices7]) + + const prices8 = [{ tokenAmount: '2.0@EFGH', currency: 'USD' }] + await container.call('setoracledata', [oracleId8, timestamp, prices8]) + await container.generate(1) + }) + + afterAll(async () => { + await container.stop() + }) + + it('should throw an error if start index is greater than the number of prices available', async () => { + await expect(client.oracle.listPrices(100, false, 1)).rejects.toThrow('start index greater than number of prices available') + }) + it('should listPrices', async () => { const data = await client.oracle.listPrices() // NOTE(jingyi2811): 0.83333333000000 = (0.5 * 1 + 1 * 2) / 3 // NOTE(jingyi2811): 1.78571428000000 = (1.5 * 3 + 2 * 4) / 7 expect(data).toStrictEqual([ { token: 'AAPL', currency: 'EUR', price: new BigNumber(0.83333333000000), ok: true }, + { token: 'ABCD', currency: 'EUR', price: new BigNumber(0.83333333000000), ok: true }, + { token: 'EFGH', currency: 'USD', price: new BigNumber(1.78571428000000), ok: true }, { token: 'TSLA', currency: 'USD', price: new BigNumber(1.78571428000000), ok: true } ]) await container.generate(1) }) - it('should listPrices with empty array if no oracle is appointed', async () => { - const data = await client.oracle.listPrices() - expect(data.length).toStrictEqual(0) + it('should list prices with a limit', async () => { + const prices = await client.oracle.listPrices(0, false, 1) + expect(prices.length).toStrictEqual(1) + expect(prices).toStrictEqual([ + { token: 'ABCD', currency: 'EUR', price: new BigNumber(0.83333333000000), ok: true } + ]) + }) + + it('should list prices including start enabled', async () => { + const prices1 = await client.oracle.listPrices(0, true, 100) + expect(prices1.length).toStrictEqual(4) + expect(prices1).toStrictEqual([ + { token: 'AAPL', currency: 'EUR', price: new BigNumber(0.83333333000000), ok: true }, + { token: 'ABCD', currency: 'EUR', price: new BigNumber(0.83333333000000), ok: true }, + { token: 'EFGH', currency: 'USD', price: new BigNumber(1.78571428000000), ok: true }, + { token: 'TSLA', currency: 'USD', price: new BigNumber(1.78571428000000), ok: true } + ]) + }) + + it('should list prices with including start disabled', async () => { + const prices = await client.oracle.listPrices(0, false, 100) + expect(prices.length).toStrictEqual(3) + expect(prices).toStrictEqual([ + { token: 'ABCD', currency: 'EUR', price: new BigNumber(0.83333333000000), ok: true }, + { token: 'EFGH', currency: 'USD', price: new BigNumber(1.78571428000000), ok: true }, + { token: 'TSLA', currency: 'USD', price: new BigNumber(1.78571428000000), ok: true } + ]) + }) + + it('should list prices with a starting value and with including start enaled', async () => { + const prices = await client.oracle.listPrices(1, true, 100) + expect(prices.length).toStrictEqual(3) + expect(prices).toStrictEqual([ + { token: 'ABCD', currency: 'EUR', price: new BigNumber(0.83333333000000), ok: true }, + { token: 'EFGH', currency: 'USD', price: new BigNumber(1.78571428000000), ok: true }, + { token: 'TSLA', currency: 'USD', price: new BigNumber(1.78571428000000), ok: true } + ]) + }) + + it('should list prices with a starting value and a limit', async () => { + const prices = await client.oracle.listPrices(1, false, 2) + expect(prices.length).toStrictEqual(2) + expect(prices).toStrictEqual([ + { token: 'EFGH', currency: 'USD', price: new BigNumber(1.78571428000000), ok: true }, + { token: 'TSLA', currency: 'USD', price: new BigNumber(1.78571428000000), ok: true } + ]) }) it.skip('should listPrices with error msg for price timestamps 4200 seconds before the current time', async () => { diff --git a/packages/jellyfish-api-core/src/category/oracle.ts b/packages/jellyfish-api-core/src/category/oracle.ts index 940c2f155b..ff0bb038a4 100644 --- a/packages/jellyfish-api-core/src/category/oracle.ts +++ b/packages/jellyfish-api-core/src/category/oracle.ts @@ -124,10 +124,13 @@ export class Oracle { /** * List all aggregated prices. * - * @return {Promise} + * @param {string} startIndex Optional first key to iterate from, in lexicographical order. + * @param {string} includingStart If true, then iterate including starting position. False by default + * @param {string} limit Maximum number of orders to return, 100 by default + * @return {Promise} Array of list price data objects */ - async listPrices (): Promise { - return await this.client.call('listprices', [], 'bignumber') + async listPrices (startIndex?: number, includingStart?: boolean, limit?: number): Promise { + return await this.client.call('listprices', [{ start: startIndex, including_start: includingStart, limit }], 'bignumber') } /** diff --git a/packages/jellyfish-api-core/src/category/token.ts b/packages/jellyfish-api-core/src/category/token.ts index 4a9b596309..3044661312 100644 --- a/packages/jellyfish-api-core/src/category/token.ts +++ b/packages/jellyfish-api-core/src/category/token.ts @@ -108,7 +108,7 @@ export class Token { * @param {UTXO[]} [utxos = []] A json array of json objects. Provide it if you want to spent specific UTXOs * @param {string} [utxos.txid] The transaction id * @param {number} [utxos.vout] The output number - * @return {Promise} Transaction hash + * @return {Promise} The hex-encoded hash of broadcasted transaction */ async burnTokens (amounts: string, from: string, context?: string, utxos: UTXO[] = []): Promise { return await this.client.call('burntokens', [{ amounts, from, context }, utxos], 'number') From a2d3c7a8c0f744ce85fad8fe14c21510dc89ebbc Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Sun, 13 Nov 2022 21:13:16 +0800 Subject: [PATCH 05/39] Fix types in jsDocs --- packages/jellyfish-api-core/src/category/oracle.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/jellyfish-api-core/src/category/oracle.ts b/packages/jellyfish-api-core/src/category/oracle.ts index ff0bb038a4..4a4c3358f1 100644 --- a/packages/jellyfish-api-core/src/category/oracle.ts +++ b/packages/jellyfish-api-core/src/category/oracle.ts @@ -124,10 +124,10 @@ export class Oracle { /** * List all aggregated prices. * - * @param {string} startIndex Optional first key to iterate from, in lexicographical order. - * @param {string} includingStart If true, then iterate including starting position. False by default - * @param {string} limit Maximum number of orders to return, 100 by default - * @return {Promise} Array of list price data objects + * @param {number} startIndex First key to iterate from, in lexicographical order. + * @param {boolean} includingStart If true, then iterate including starting position. False by default + * @param {number} limit Maximum number of orders to return, 100 by default + * @return {Promise} Array of ListPricesData objects */ async listPrices (startIndex?: number, includingStart?: boolean, limit?: number): Promise { return await this.client.call('listprices', [{ start: startIndex, including_start: includingStart, limit }], 'bignumber') From abdeb7fab09f7fb55eb5b2a01194fb4948109771 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Tue, 15 Nov 2022 14:13:55 +0800 Subject: [PATCH 06/39] Use regtest-minttoken-simulate-mainnet flag --- apps/rich-list-api/docker-compose.yml | 4 ++- apps/whale-api/docker-compose.yml | 4 ++- .../category/masternode/setGov.test.ts | 7 +----- .../category/token/burnTokens.test.ts | 8 ------ .../category/token/mintTokens.test.ts | 25 +++++++++---------- .../__tests__/RichListCore.test.ts | 4 +-- .../src/containers/DeFiDContainer.ts | 2 +- .../src/containers/RegTestContainer/index.ts | 5 ++-- 8 files changed, 25 insertions(+), 34 deletions(-) diff --git a/apps/rich-list-api/docker-compose.yml b/apps/rich-list-api/docker-compose.yml index d7e556c4f2..11f6b964bd 100644 --- a/apps/rich-list-api/docker-compose.yml +++ b/apps/rich-list-api/docker-compose.yml @@ -13,7 +13,7 @@ services: POSTGRES_DB: defichainrichlist defi-blockchain: - image: defi/defichain:HEAD-49fba65ce + image: defi/defichain:HEAD-453b1c948 ports: - "19554:19554" command: > @@ -47,4 +47,6 @@ services: -fortcanningspringheight=13 -fortcanninggreatworldheight=14 -fortcanningepilogueheight=15 + -grandcentralheight=16 -regtest-skip-loan-collateral-validation + -regtest-minttoken-simulate-mainnet=0 diff --git a/apps/whale-api/docker-compose.yml b/apps/whale-api/docker-compose.yml index 57c6151e88..80033a1c30 100644 --- a/apps/whale-api/docker-compose.yml +++ b/apps/whale-api/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' services: defi-blockchain: - image: defi/defichain:HEAD-49fba65ce + image: defi/defichain:HEAD-453b1c948 ports: - "19554:19554" command: > @@ -36,7 +36,9 @@ services: -fortcanningspringheight=13 -fortcanninggreatworldheight=14 -fortcanningepilogueheight=15 + -grandcentralheight=16 -regtest-skip-loan-collateral-validation + -regtest-minttoken-simulate-mainnet=0 defi-whale: build: diff --git a/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts b/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts index 6f7496494e..03926408cb 100644 --- a/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts @@ -549,8 +549,6 @@ describe('setGov consortium ATTRIBUTES', () => { await tGroup.get(0).container.fundAddress(account1, 10) - await tGroup.get(0).generate(1) - idBTC = await tGroup.get(0).token.getTokenId(symbolBTC) } @@ -674,9 +672,6 @@ describe('setGov consortium ATTRIBUTES', () => { }) it('should set member information', async () => { - // Move to grand central height - await tGroup.get(0).generate(150 - await tGroup.get(0).container.getBlockCount()) - expect(await setGovAttr({ [`v0/consortium/${idBTC}/mint_limit`]: '10', [`v0/consortium/${idBTC}/mint_limit_daily`]: '1' @@ -697,7 +692,7 @@ describe('setGov consortium ATTRIBUTES', () => { }` })).toBeTruthy() - await tGroup.get(0).generate(1) + await tGroup.get(0).generate(5) const attr = (await tGroup.get(0).rpc.masternode.getGov('ATTRIBUTES')).ATTRIBUTES expect(attr['v0/consortium/1/members']).toStrictEqual(`{"01":{"name":"test","ownerAddress":"${account1}","backingId":"blablabla","mintLimit":10.00000000,"dailyMintLimit":1.00000000,"status":1}}`) diff --git a/packages/jellyfish-api-core/__tests__/category/token/burnTokens.test.ts b/packages/jellyfish-api-core/__tests__/category/token/burnTokens.test.ts index 08f9721d20..bbc5cd9fd6 100644 --- a/packages/jellyfish-api-core/__tests__/category/token/burnTokens.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/token/burnTokens.test.ts @@ -30,22 +30,14 @@ describe('burnTokens', () => { tradeable: true, collateralAddress: address }) - await testing.generate(1) - await testing.generate(1) await testing.rpc.token.mintTokens(`10@${symbolDBTC}`) await testing.generate(1) await testing.container.fundAddress(address, 10) - await testing.generate(1) } - it('should throw error if called before GrandCentral height', async () => { - const promise = testing.rpc.token.burnTokens(`2@${symbolDBTC}`, address) - await expect(promise).rejects.toThrowError('called before GrandCentral height') - }) - it('should throw an error if invalid value is provided for amount', async () => { // Move to grand central height await testing.generate(150) diff --git a/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts b/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts index 3c6b591e5b..3932770c1f 100644 --- a/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts @@ -1,4 +1,4 @@ -import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { MasterNodeRegTestContainer, StartFlags } from '@defichain/testcontainers' import { ContainerAdapterClient } from '../../container_adapter_client' import { BigNumber, RpcApiError } from '../../../src' import { TestingGroup } from '@defichain/jellyfish-testing' @@ -96,7 +96,8 @@ describe('Consortium', () => { const blocksPerDay = (60 * 60 * 24) / (10 * 60) // 144 in regtest beforeAll(async () => { - await tGroup.start() + const startFlags: StartFlags[] = [{ name: 'regtest-minttoken-simulate-mainnet', value: 0 }] + await tGroup.start({ startFlags: startFlags }) account0 = await tGroup.get(0).generateAddress() account1 = await tGroup.get(1).generateAddress() @@ -132,9 +133,6 @@ describe('Consortium', () => { idBTC = await tGroup.get(0).token.getTokenId(symbolBTC) idDOGE = await tGroup.get(0).token.getTokenId(symbolDOGE) - - // Move to grand central height - await tGroup.get(0).container.generate(150 - await tGroup.get(0).container.getBlockCount()) }) afterAll(async () => { @@ -162,10 +160,11 @@ describe('Consortium', () => { } it('should throw an error if foundation or consortium member authorization is not present', async () => { - await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolBTC}`)).rejects.toThrow('Need foundation or consortium member authorization') - await expect(tGroup.get(3).rpc.token.mintTokens(`1@${symbolBTC}`)).rejects.toThrow('Need foundation or consortium member authorization') - await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow('Need foundation or consortium member authorization') - await expect(tGroup.get(3).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow('Need foundation or consortium member authorization') + const errorMsg = 'RpcApiError: \'Test MintTokenTx execution failed:\nYou are not a foundation member or token owner and cannot mint this token!\', code: -32600, method: minttokens' + await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolBTC}`)).rejects.toThrow(errorMsg) + await expect(tGroup.get(3).rpc.token.mintTokens(`1@${symbolBTC}`)).rejects.toThrow(errorMsg) + await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow(errorMsg) + await expect(tGroup.get(3).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow(errorMsg) }) it('should throw an error if the token is not specified in governance vars', async () => { @@ -200,7 +199,7 @@ describe('Consortium', () => { }]) // Trying to mint DOGE - await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow('Need foundation or consortium member authorization') + await expect(tGroup.get(2).rpc.token.mintTokens(`1@${symbolDOGE}`)).rejects.toThrow('RpcApiError: \'Test MintTokenTx execution failed:\nYou are not a foundation member or token owner and cannot mint this token!\', code: -32600, method: minttokens') }) it('should throw an error if member daily mint limit exceeds', async () => { @@ -305,11 +304,11 @@ describe('Consortium', () => { expect(attr2[`v0/live/economy/consortium/${idBTC}/burnt`]).toStrictEqual(new BigNumber(10)) expect(attr2[`v0/live/economy/consortium/${idBTC}/supply`]).toStrictEqual(new BigNumber(1)) expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/minted`]).toStrictEqual(new BigNumber(10)) - expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/daily_minted`]).toStrictEqual('288/5.00000000') + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/daily_minted`]).toStrictEqual(`${blocksPerDay}/5.00000000`) expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/burnt`]).toStrictEqual(new BigNumber(10)) expect(attr2[`v0/live/economy/consortium_members/${idBTC}/01/supply`]).toStrictEqual(new BigNumber(0)) expect(attr2[`v0/live/economy/consortium_members/${idBTC}/02/minted`]).toStrictEqual(new BigNumber(1)) - expect(attr2[`v0/live/economy/consortium_members/${idBTC}/02/daily_minted`]).toStrictEqual('432/1.00000000') + expect(attr2[`v0/live/economy/consortium_members/${idBTC}/02/daily_minted`]).toStrictEqual(`${blocksPerDay * 2}/1.00000000`) expect(attr2[`v0/live/economy/consortium_members/${idBTC}/02/burnt`]).toStrictEqual(new BigNumber(0)) expect(attr2[`v0/live/economy/consortium_members/${idBTC}/02/supply`]).toStrictEqual(new BigNumber(1)) @@ -317,7 +316,7 @@ describe('Consortium', () => { expect(attr2[`v0/live/economy/consortium/${idDOGE}/burnt`]).toStrictEqual(new BigNumber(0)) expect(attr2[`v0/live/economy/consortium/${idDOGE}/supply`]).toStrictEqual(new BigNumber(2)) expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/02/minted`]).toStrictEqual(new BigNumber(2)) - expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/02/daily_minted`]).toStrictEqual('432/2.00000000') + expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/02/daily_minted`]).toStrictEqual(`${blocksPerDay * 2}/2.00000000`) expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/02/burnt`]).toStrictEqual(new BigNumber(0)) expect(attr2[`v0/live/economy/consortium_members/${idDOGE}/02/supply`]).toStrictEqual(new BigNumber(2)) }) diff --git a/packages/rich-list-core/__tests__/RichListCore.test.ts b/packages/rich-list-core/__tests__/RichListCore.test.ts index 7e9007d5ed..0b93b48c2a 100644 --- a/packages/rich-list-core/__tests__/RichListCore.test.ts +++ b/packages/rich-list-core/__tests__/RichListCore.test.ts @@ -16,8 +16,8 @@ const EXPECTED_RICH_LIST_ADDRESSES = [ 'mud4VMfbBqXNpbt8ur33KHKx8pk3npSq8c', 'bcrt1qyrfrpadwgw7p5eh3e9h3jmu4kwlz4prx73cqny', 'bcrt1qyeuu9rvq8a67j86pzvh5897afdmdjpyankp4mu', - 'mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy', - '2NCWAKfEehP3qibkLKYQjXaWMK23k4EDMVS' + '2NCWAKfEehP3qibkLKYQjXaWMK23k4EDMVS', + 'mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy' ] describe('RichListCore', () => { diff --git a/packages/testcontainers/src/containers/DeFiDContainer.ts b/packages/testcontainers/src/containers/DeFiDContainer.ts index 475e8f510e..5d5a8e2ca5 100644 --- a/packages/testcontainers/src/containers/DeFiDContainer.ts +++ b/packages/testcontainers/src/containers/DeFiDContainer.ts @@ -35,7 +35,7 @@ export abstract class DeFiDContainer extends DockerContainer { if (process?.env?.DEFICHAIN_DOCKER_IMAGE !== undefined) { return process.env.DEFICHAIN_DOCKER_IMAGE } - return 'defi/defichain:HEAD-01633efee' + return 'defi/defichain:HEAD-453b1c948' } public static readonly DefaultStartOptions = { diff --git a/packages/testcontainers/src/containers/RegTestContainer/index.ts b/packages/testcontainers/src/containers/RegTestContainer/index.ts index 341e1c96a7..b6789d6a1e 100644 --- a/packages/testcontainers/src/containers/RegTestContainer/index.ts +++ b/packages/testcontainers/src/containers/RegTestContainer/index.ts @@ -40,8 +40,9 @@ export class RegTestContainer extends DeFiDContainer { '-fortcanningspringheight=13', '-fortcanninggreatworldheight=14', '-fortcanningepilogueheight=15', - '-grandcentralheight=150', - '-regtest-skip-loan-collateral-validation' + '-grandcentralheight=16', + '-regtest-skip-loan-collateral-validation', + '-regtest-minttoken-simulate-mainnet=0' ] if (opts.startFlags != null && opts.startFlags.length > 0) { From c936b9796a4b7018cb9824db1adef3667bd59217 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Wed, 16 Nov 2022 18:03:01 +0800 Subject: [PATCH 07/39] Add tests for unlimited global mint limits --- .../category/token/mintTokens.test.ts | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts b/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts index 3932770c1f..5db054da16 100644 --- a/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/token/mintTokens.test.ts @@ -254,6 +254,57 @@ describe('Consortium', () => { expect(attr[`v0/live/economy/consortium/${idBTC}/supply`]).toStrictEqual(new BigNumber('1')) }) + it('should be able to set unlimited mint limits per member when global mint_limit or mint_limit_daily is set to -1', async () => { + await setGovAttr({ + [`v0/consortium/${idBTC}/mint_limit_daily`]: '-1', + [`v0/consortium/${idBTC}/mint_limit`]: '-1' + }) + + // Increase limits + await setMemberInfo(idBTC, [{ + id: '01', + name: 'account1BTC', + ownerAddress: account1, + dailyMintLimit: '10000000.00000000', + mintLimit: '10000000.00000000' + }, { + id: '02', + name: 'account2BTC', + ownerAddress: account2, + dailyMintLimit: '50000000.00000000', + mintLimit: '50000000.00000000' + }, { + id: '03', + name: 'account3BTC', + ownerAddress: account3, + dailyMintLimit: '50000000.00000000', + mintLimit: '50000000.00000000' + }]) + + // Decrease limits + await setMemberInfo(idBTC, [{ + id: '01', + name: 'account1BTC', + ownerAddress: account1, + dailyMintLimit: '5.00000000', + mintLimit: '10.00000000' + }, { + id: '02', + name: 'account2BTC', + ownerAddress: account2, + dailyMintLimit: '5.00000000', + mintLimit: '10.00000000' + }, { + id: '03', + name: 'account3BTC', + ownerAddress: account3, + dailyMintLimit: '5.00000000', + mintLimit: '10.00000000' + }]) + + await tGroup.get(0).generate(5) + }) + it('should return correct governance attribute values', async () => { // Set global mint limits for DOGE await setGovAttr({ @@ -283,8 +334,8 @@ describe('Consortium', () => { const attr0 = (await tGroup.get(0).rpc.masternode.getGov('ATTRIBUTES')).ATTRIBUTES expect(attr0[`v0/consortium/${idBTC}/members`]).toStrictEqual(`{"01":{"name":"account1BTC","ownerAddress":"${account1}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":10.00000000,"dailyMintLimit":5.00000000,"status":0},"02":{"name":"account2BTC","ownerAddress":"${account2}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":10.00000000,"dailyMintLimit":5.00000000,"status":0},"03":{"name":"account3BTC","ownerAddress":"${account3}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":10.00000000,"dailyMintLimit":5.00000000,"status":0}}`) - expect(attr0[`v0/consortium/${idBTC}/mint_limit`]).toStrictEqual('20') - expect(attr0[`v0/consortium/${idBTC}/mint_limit_daily`]).toStrictEqual('5') + expect(attr0[`v0/consortium/${idBTC}/mint_limit`]).toStrictEqual('-1') + expect(attr0[`v0/consortium/${idBTC}/mint_limit_daily`]).toStrictEqual('-1') expect(attr0[`v0/consortium/${idDOGE}/members`]).toStrictEqual(`{"01":{"name":"account1DOGE","ownerAddress":"${account1}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":2.00000000,"status":0},"02":{"name":"account2DOGE","ownerAddress":"${account2}","backingId":"ebf634ef7143bc5466995a385b842649b2037ea89d04d469bfa5ec29daf7d1cf","mintLimit":5.00000000,"dailyMintLimit":2.00000000,"status":0}}`) expect(attr0[`v0/consortium/${idDOGE}/mint_limit`]).toStrictEqual('6') From 379d77b11924a48b5108485c4ba9751176096c3f Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Wed, 16 Nov 2022 18:20:24 +0800 Subject: [PATCH 08/39] Resolve test failiure on PoolPairController.test.ts --- apps/legacy-api/src/controllers/PoolPairController.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/legacy-api/src/controllers/PoolPairController.ts b/apps/legacy-api/src/controllers/PoolPairController.ts index 7db545c70e..1427c126d7 100644 --- a/apps/legacy-api/src/controllers/PoolPairController.ts +++ b/apps/legacy-api/src/controllers/PoolPairController.ts @@ -67,8 +67,8 @@ export class PoolPairController { quote_name: quote.symbol, quote_symbol: quote.symbol, last_price: poolPair.priceRatio.ab, - base_volume: baseVolume.toNumber(), - quote_volume: quoteVolume.toNumber(), + base_volume: baseVolume.isNaN() ? 0 : baseVolume.toNumber(), + quote_volume: quoteVolume.isNaN() ? 0 : quoteVolume.toNumber(), isFrozen: (poolPair.status) ? 0 : 1 } } @@ -186,8 +186,8 @@ export class PoolPairControllerV2 { quote_name: quote.symbol, quote_symbol: quote.symbol, last_price: poolPair.priceRatio.ba, // inverted from v1 - base_volume: baseVolume.toNumber(), - quote_volume: quoteVolume.toNumber(), + base_volume: baseVolume.isNaN() ? 0 : baseVolume.toNumber(), + quote_volume: quoteVolume.isNaN() ? 0 : quoteVolume.toNumber(), isFrozen: (poolPair.status) ? 0 : 1 } } From 1e7755a17231d25a392d03370a634f59905a9a2c Mon Sep 17 00:00:00 2001 From: Shoham Chakraborty Date: Thu, 17 Nov 2022 16:36:38 +0800 Subject: [PATCH 09/39] Add new consortium attribute tests, bump docker image tag --- apps/rich-list-api/docker-compose.yml | 2 +- apps/whale-api/docker-compose.yml | 2 +- .../category/masternode/setGov.test.ts | 30 +++++++++++++++++++ .../src/containers/DeFiDContainer.ts | 2 +- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/rich-list-api/docker-compose.yml b/apps/rich-list-api/docker-compose.yml index 11f6b964bd..1737d345f4 100644 --- a/apps/rich-list-api/docker-compose.yml +++ b/apps/rich-list-api/docker-compose.yml @@ -13,7 +13,7 @@ services: POSTGRES_DB: defichainrichlist defi-blockchain: - image: defi/defichain:HEAD-453b1c948 + image: defi/defichain:master-7ce2d1721 ports: - "19554:19554" command: > diff --git a/apps/whale-api/docker-compose.yml b/apps/whale-api/docker-compose.yml index 80033a1c30..38c43b6c72 100644 --- a/apps/whale-api/docker-compose.yml +++ b/apps/whale-api/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' services: defi-blockchain: - image: defi/defichain:HEAD-453b1c948 + image: defi/defichain:master-7ce2d1721 ports: - "19554:19554" command: > diff --git a/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts b/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts index 03926408cb..28f4eecc24 100644 --- a/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts @@ -697,4 +697,34 @@ describe('setGov consortium ATTRIBUTES', () => { const attr = (await tGroup.get(0).rpc.masternode.getGov('ATTRIBUTES')).ATTRIBUTES expect(attr['v0/consortium/1/members']).toStrictEqual(`{"01":{"name":"test","ownerAddress":"${account1}","backingId":"blablabla","mintLimit":10.00000000,"dailyMintLimit":1.00000000,"status":1}}`) }) + + it('should throw an error if global limit is set to negative value other than -1', async () => { + const promise = setGovAttr({ + [`v0/consortium/${idBTC}/mint_limit`]: '-2' + }) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toMatchObject({ + payload: { + code: -5, + message: 'Amount must be positive or -1', + method: 'setgov' + } + }) + }) + + it('should throw an error if global limit is set to 0', async () => { + const promise = setGovAttr({ + [`v0/consortium/${idBTC}/mint_limit`]: '0' + }) + + await expect(promise).rejects.toThrow(RpcApiError) + await expect(promise).rejects.toMatchObject({ + payload: { + code: -5, + message: 'Amount must be positive or -1', + method: 'setgov' + } + }) + }) }) diff --git a/packages/testcontainers/src/containers/DeFiDContainer.ts b/packages/testcontainers/src/containers/DeFiDContainer.ts index 5d5a8e2ca5..429ec9cd79 100644 --- a/packages/testcontainers/src/containers/DeFiDContainer.ts +++ b/packages/testcontainers/src/containers/DeFiDContainer.ts @@ -35,7 +35,7 @@ export abstract class DeFiDContainer extends DockerContainer { if (process?.env?.DEFICHAIN_DOCKER_IMAGE !== undefined) { return process.env.DEFICHAIN_DOCKER_IMAGE } - return 'defi/defichain:HEAD-453b1c948' + return 'defi/defichain:master-7ce2d1721' } public static readonly DefaultStartOptions = { From bf10e84a6604a039af07c93682235321ac832dd3 Mon Sep 17 00:00:00 2001 From: Shoham Chakraborty Date: Thu, 17 Nov 2022 17:44:35 +0800 Subject: [PATCH 10/39] Allow minting to zero --- apps/rich-list-api/docker-compose.yml | 2 +- apps/whale-api/docker-compose.yml | 2 +- .../__tests__/category/masternode/setGov.test.ts | 15 +++------------ .../src/containers/DeFiDContainer.ts | 2 +- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/apps/rich-list-api/docker-compose.yml b/apps/rich-list-api/docker-compose.yml index 1737d345f4..ede421cc5b 100644 --- a/apps/rich-list-api/docker-compose.yml +++ b/apps/rich-list-api/docker-compose.yml @@ -13,7 +13,7 @@ services: POSTGRES_DB: defichainrichlist defi-blockchain: - image: defi/defichain:master-7ce2d1721 + image: defi/defichain:master-3a65f4571 ports: - "19554:19554" command: > diff --git a/apps/whale-api/docker-compose.yml b/apps/whale-api/docker-compose.yml index 38c43b6c72..be734e3451 100644 --- a/apps/whale-api/docker-compose.yml +++ b/apps/whale-api/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' services: defi-blockchain: - image: defi/defichain:master-7ce2d1721 + image: defi/defichain:master-3a65f4571 ports: - "19554:19554" command: > diff --git a/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts b/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts index 28f4eecc24..b5d497ace8 100644 --- a/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/masternode/setGov.test.ts @@ -713,18 +713,9 @@ describe('setGov consortium ATTRIBUTES', () => { }) }) - it('should throw an error if global limit is set to 0', async () => { - const promise = setGovAttr({ + it('allow global limit to be 0', async () => { + expect(await setGovAttr({ [`v0/consortium/${idBTC}/mint_limit`]: '0' - }) - - await expect(promise).rejects.toThrow(RpcApiError) - await expect(promise).rejects.toMatchObject({ - payload: { - code: -5, - message: 'Amount must be positive or -1', - method: 'setgov' - } - }) + })).toBeTruthy() }) }) diff --git a/packages/testcontainers/src/containers/DeFiDContainer.ts b/packages/testcontainers/src/containers/DeFiDContainer.ts index 429ec9cd79..176cb902d4 100644 --- a/packages/testcontainers/src/containers/DeFiDContainer.ts +++ b/packages/testcontainers/src/containers/DeFiDContainer.ts @@ -35,7 +35,7 @@ export abstract class DeFiDContainer extends DockerContainer { if (process?.env?.DEFICHAIN_DOCKER_IMAGE !== undefined) { return process.env.DEFICHAIN_DOCKER_IMAGE } - return 'defi/defichain:master-7ce2d1721' + return 'defi/defichain:master-3a65f4571' } public static readonly DefaultStartOptions = { From ca092bf81129848a8e8ca4e2878eaf4a957b4153 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Thu, 17 Nov 2022 21:02:24 +0800 Subject: [PATCH 11/39] Remove waiting for grandcentral height --- .../__tests__/category/token/burnTokens.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/jellyfish-api-core/__tests__/category/token/burnTokens.test.ts b/packages/jellyfish-api-core/__tests__/category/token/burnTokens.test.ts index bbc5cd9fd6..f62e3dae03 100644 --- a/packages/jellyfish-api-core/__tests__/category/token/burnTokens.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/token/burnTokens.test.ts @@ -39,9 +39,6 @@ describe('burnTokens', () => { } it('should throw an error if invalid value is provided for amount', async () => { - // Move to grand central height - await testing.generate(150) - // @ts-expect-error await expect(testing.rpc.token.burnTokens(null, address)).rejects.toThrow('Invalid parameters, argument "amounts" must not be null') await expect(testing.rpc.token.burnTokens('', address)).rejects.toThrow(': Invalid amount') From 5b5b28f1f1330ee31d5226cdadc2f1fce9264199 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Tue, 22 Nov 2022 21:50:34 +0800 Subject: [PATCH 12/39] Add GET consortium/transactions endpoint --- apps/whale-api/src/module.api/_module.ts | 8 +- .../module.api/consortium.controller.spec.ts | 254 +++++++++++++++++ .../src/module.api/consortium.controller.ts | 42 +++ .../src/module.api/consortium.service.ts | 125 +++++++++ .../src/category/account.ts | 1 + .../__tests__/api/consortium.test.ts | 259 ++++++++++++++++++ .../whale-api-client/src/api/consortium.ts | 54 ++++ packages/whale-api-client/src/index.ts | 1 + .../whale-api-client/src/whale.api.client.ts | 2 + 9 files changed, 744 insertions(+), 2 deletions(-) create mode 100644 apps/whale-api/src/module.api/consortium.controller.spec.ts create mode 100644 apps/whale-api/src/module.api/consortium.controller.ts create mode 100644 apps/whale-api/src/module.api/consortium.service.ts create mode 100644 packages/whale-api-client/__tests__/api/consortium.test.ts create mode 100644 packages/whale-api-client/src/api/consortium.ts diff --git a/apps/whale-api/src/module.api/_module.ts b/apps/whale-api/src/module.api/_module.ts index e481366b2e..1be97fe996 100644 --- a/apps/whale-api/src/module.api/_module.ts +++ b/apps/whale-api/src/module.api/_module.ts @@ -34,6 +34,8 @@ import { PoolPairPricesService } from './poolpair.prices.service' import { LegacyController } from './legacy.controller' import { LegacySubgraphService } from './legacy.subgraph.service' import { PoolPairFeesService } from './poolpair.fees.service' +import { ConsortiumController } from './consortium.controller' +import { ConsortiumService } from './consortium.service' /** * Exposed ApiModule for public interfacing @@ -56,7 +58,8 @@ import { PoolPairFeesService } from './poolpair.fees.service' FeeController, RawtxController, LoanController, - LegacyController + LegacyController, + ConsortiumController ], providers: [ { @@ -100,7 +103,8 @@ import { PoolPairFeesService } from './poolpair.fees.service' }, inject: [ConfigService] }, - LegacySubgraphService + LegacySubgraphService, + ConsortiumService ], exports: [ DeFiDCache diff --git a/apps/whale-api/src/module.api/consortium.controller.spec.ts b/apps/whale-api/src/module.api/consortium.controller.spec.ts new file mode 100644 index 0000000000..bbd4f89cb8 --- /dev/null +++ b/apps/whale-api/src/module.api/consortium.controller.spec.ts @@ -0,0 +1,254 @@ +import { ConsortiumController } from './consortium.controller' +import { TestingGroup } from '@defichain/jellyfish-testing' +import { createTestingApp, stopTestingApp, waitForIndexedHeight } from '../e2e.module' +import { NestFastifyApplication } from '@nestjs/platform-fastify' +import _ from 'lodash' + +describe('getTransactionHistory', () => { + const tGroup = TestingGroup.create(2) + const alice = tGroup.get(0) + const bob = tGroup.get(1) + const symbolBTC = 'dBTC' + const symbolETH = 'dETH' + let accountAlice: string, accountBob: string + let idBTC: string + let idETH: string + let app: NestFastifyApplication + let controller: ConsortiumController + + beforeAll(async () => { + await tGroup.start() + await alice.container.waitForWalletCoinbaseMaturity() + + app = await createTestingApp(alice.container) + controller = app.get(ConsortiumController) + + await setup() + }) + + afterAll(async () => { + await stopTestingApp(tGroup, app) + }) + + async function setGovAttr (ATTRIBUTES: object): Promise { + const hash = await alice.rpc.masternode.setGov({ ATTRIBUTES }) + expect(hash).toBeTruthy() + await alice.generate(1) + } + + async function setMemberInfo (tokenId: string, memberInfo: Array<{ id: string, name: string, backingId: string, ownerAddress: string, mintLimit: string, dailyMintLimit: string }>): Promise { + const infoObjs = memberInfo.map(mi => ` + "${mi.id}":{ + "name":"${mi.name}", + "ownerAddress":"${mi.ownerAddress}", + "backingId":"${mi.backingId}", + "dailyMintLimit":${mi.dailyMintLimit}, + "mintLimit":${mi.mintLimit} + }` + ) + + return await setGovAttr({ [`v0/consortium/${tokenId}/members`]: `{${infoObjs.join(',')}}` }) + } + + async function setup (): Promise { + accountAlice = await alice.generateAddress() + accountBob = await bob.generateAddress() + + await alice.token.create({ + symbol: symbolBTC, + name: symbolBTC, + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: accountAlice + }) + await alice.generate(1) + + await alice.token.create({ + symbol: symbolETH, + name: symbolETH, + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: accountAlice + }) + await alice.generate(1) + + await alice.container.fundAddress(accountBob, 10) + await alice.generate(1) + idBTC = await alice.token.getTokenId(symbolBTC) + idETH = await alice.token.getTokenId(symbolETH) + + await setGovAttr({ + 'v0/params/feature/consortium': 'true', + [`v0/consortium/${idBTC}/mint_limit`]: '10', + [`v0/consortium/${idBTC}/mint_limit_daily`]: '5', + [`v0/consortium/${idETH}/mint_limit`]: '20', + [`v0/consortium/${idETH}/mint_limit_daily`]: '10' + }) + + await setMemberInfo(idBTC, [{ + id: '01', + name: 'alice', + ownerAddress: accountAlice, + backingId: 'abc', + dailyMintLimit: '5.00000000', + mintLimit: '10.00000000' + }, { + id: '02', + name: 'bob', + ownerAddress: accountBob, + backingId: 'def,hij', + dailyMintLimit: '5.00000000', + mintLimit: '10.00000000' + }]) + + await setMemberInfo(idETH, [{ + id: '01', + name: 'alice', + ownerAddress: accountAlice, + backingId: '', + dailyMintLimit: '10.00000000', + mintLimit: '20.00000000' + }, { + id: '02', + name: 'bob', + ownerAddress: accountBob, + backingId: 'lmn,opq', + dailyMintLimit: '10.00000000', + mintLimit: '20.00000000' + }]) + + await alice.rpc.token.mintTokens(`1@${symbolBTC}`) + await alice.generate(5) + + await alice.rpc.token.mintTokens(`2@${symbolETH}`) + await alice.generate(5) + + await alice.rpc.token.burnTokens(`1@${symbolETH}`, accountAlice) + await alice.generate(5) + + await bob.rpc.token.mintTokens(`4@${symbolBTC}`) + await bob.generate(5) + + await bob.rpc.token.burnTokens(`2@${symbolBTC}`, accountBob) + await bob.generate(5) + + const height = await alice.container.getBlockCount() + await alice.generate(1) + await waitForIndexedHeight(app, height) + } + + it('should throw an error if the limit is invalid', async () => { + await expect(controller.getTransactionHistory({ limit: 51 })).rejects.toThrow('InvalidLimit') + await expect(controller.getTransactionHistory({ limit: 0 })).rejects.toThrow('InvalidLimit') + }) + + it('should throw an error if the search term is invalid', async () => { + await expect(controller.getTransactionHistory({ search: 'a', limit: 1 })).rejects.toThrow('InvalidSearchTerm') + await expect(controller.getTransactionHistory({ search: 'a'.repeat(65), limit: 1 })).rejects.toThrow('InvalidSearchTerm') + }) + + it('should throw an error if the max block height is invalid', async () => { + await expect(controller.getTransactionHistory({ maxBlockHeight: -2, limit: 1 })).rejects.toThrow('InvalidmaxBlockHeight') + }) + + it('should filter transactions with search term (member name)', async () => { + const info = await controller.getTransactionHistory({ search: 'alice', limit: 20 }) + + expect(info.transactions.length).toStrictEqual(3) + + expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) + expect(info.transactions[0].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) + expect(info.transactions[1].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 }) + expect(info.transactions[2].txId).not.toBe(undefined) + + expect(info.total).toStrictEqual(3) + }) + + it('should filter transactions with search term (owner address)', async () => { + const info = await controller.getTransactionHistory({ search: accountAlice, limit: 20 }) + + expect(info.transactions.length).toStrictEqual(3) + + expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) + expect(info.transactions[0].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) + expect(info.transactions[1].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 }) + expect(info.transactions[2].txId).not.toBe(undefined) + + expect(info.total).toStrictEqual(3) + }) + + it('should filter transactions with search term (transaction id)', async () => { + const tx = (await alice.rpc.account.listAccountHistory(accountAlice))[0] + + const info = await controller.getTransactionHistory({ search: tx.txid, limit: 20 }) + + expect(info.transactions.length).toStrictEqual(1) + expect(info.transactions[0]).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119, txId: tx.txid }) + expect(info.total).toStrictEqual(1) + }) + + it('should limit transactions', async () => { + const info = await controller.getTransactionHistory({ limit: 3 }) + + expect(info.transactions.length).toStrictEqual(3) + + expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 129 }) + expect(info.transactions[0].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }) + expect(info.transactions[1].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) + expect(info.transactions[2].txId).not.toBe(undefined) + + expect(info.total).toStrictEqual(5) + }) + + it('should filter and limit transactions at the same time', async () => { + const info = await controller.getTransactionHistory({ search: accountAlice, limit: 2 }) + + expect(info.transactions.length).toStrictEqual(2) + + expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) + expect(info.transactions[0].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) + expect(info.transactions[1].txId).not.toBe(undefined) + + expect(info.total).toStrictEqual(3) + }) + + it('should get transactions upto a specific block height with a limit', async () => { + const info = await controller.getTransactionHistory({ limit: 3, maxBlockHeight: 124 }) + + expect(info.transactions.length).toStrictEqual(3) + + expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }) + expect(info.transactions[0].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) + expect(info.transactions[1].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) + expect(info.transactions[2].txId).not.toBe(undefined) + + expect(info.total).toStrictEqual(5) + }) + + it('should return empty list of transactions for invalid search term', async () => { + const info = await controller.getTransactionHistory({ search: 'invalid-term', limit: 20 }) + + expect(info.transactions.length).toStrictEqual(0) + expect(info.total).toStrictEqual(0) + }) +}) diff --git a/apps/whale-api/src/module.api/consortium.controller.ts b/apps/whale-api/src/module.api/consortium.controller.ts new file mode 100644 index 0000000000..799aa6a21a --- /dev/null +++ b/apps/whale-api/src/module.api/consortium.controller.ts @@ -0,0 +1,42 @@ +import { Controller, ForbiddenException, Get, Query } from '@nestjs/common' +import { ConsortiumService } from './consortium.service' +import { ConsortiumTransactionResponse } from '@defichain/whale-api-client/dist/api/consortium' +import { SemaphoreCache } from '@defichain-apps/libs/caches' + +@Controller('/consortium') +export class ConsortiumController { + constructor ( + protected readonly consortiumService: ConsortiumService, + protected readonly cache: SemaphoreCache + ) {} + + /** + * Gets the transaction history of consortium members. + * + * @return {Promise} + */ + @Get('/transactions') + async getTransactionHistory ( + @Query() query: { limit: number, search?: string, maxBlockHeight?: number } + ): Promise { + const { limit = 20, search = undefined, maxBlockHeight = -1 } = query + + if (limit > 50 || limit < 1) { + throw new ForbiddenException('InvalidLimit') + } + + if (search !== undefined && (search.length < 3 || search.length > 64)) { + throw new ForbiddenException('InvalidSearchTerm') + } + + if (maxBlockHeight < -1) { + throw new ForbiddenException('InvalidmaxBlockHeight') + } + + return await this.cache.get(`CONSORTIUM_TRANSACTIONS_${JSON.stringify(query)}`, async () => { + return await this.consortiumService.getTransactionHistory(+limit, +maxBlockHeight, typeof search === 'string' ? search : '') + }, { + ttl: 600 // 10 mins + }) as ConsortiumTransactionResponse + } +} diff --git a/apps/whale-api/src/module.api/consortium.service.ts b/apps/whale-api/src/module.api/consortium.service.ts new file mode 100644 index 0000000000..98ac318e9d --- /dev/null +++ b/apps/whale-api/src/module.api/consortium.service.ts @@ -0,0 +1,125 @@ +import { Injectable } from '@nestjs/common' +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' +import { SemaphoreCache } from '@defichain-apps/libs/caches' +import { ConsortiumMember, ConsortiumTransactionResponse, Transaction } from '@defichain/whale-api-client/dist/api/consortium' +import { AccountHistory, DfTxType } from '@defichain/jellyfish-api-core/dist/category/account' +import { TransactionMapper } from '../module.model/transaction' + +@Injectable() +export class ConsortiumService { + constructor ( + protected readonly rpcClient: JsonRpcClient, + protected readonly cache: SemaphoreCache, + protected readonly transactionMapper: TransactionMapper + ) {} + + private formatTransactionResponse (tx: AccountHistory, members: ConsortiumMember[]): Transaction { + return { + type: tx.type === 'MintToken' ? 'Mint' : 'Burn', + member: members.find(m => m.address === tx.owner)?.name ?? '', + tokenAmounts: tx.amounts.map((a: any) => { + const splits = a.split('@') + return { token: splits[1], amount: splits[0] } + }), + txId: tx.txid, + address: tx.owner, + block: tx.blockHeight + } + } + + async getTransactionHistory (limit: number, maxBlockHeight: number, search: string): Promise { + const attrs = (await this.rpcClient.masternode.getGov('ATTRIBUTES')).ATTRIBUTES + const members: ConsortiumMember[] = [] + const searching: boolean = search !== '' + const keys: string[] = Object.keys(attrs) + const values: string[] = Object.values(attrs) + const membersKeyRegex: RegExp = /^v0\/consortium\/\d+\/members$/ + let totalTxCount: number = 0 + let searchFound: boolean = false + + keys.forEach((key: string, i: number) => { + if (membersKeyRegex.exec(key) !== null) { + const membersPerToken: object = JSON.parse(values[i]) + const memberIds: string[] = Object.keys(membersPerToken) + let memberDetails: Array<{ ownerAddress: string, name: string }> = Object.values(membersPerToken) + + const foundMembers: Array<{ ownerAddress: string, name: string }> = memberDetails.filter(m => m.ownerAddress === search || m.name.toLowerCase().includes(search)) + if (foundMembers.length > 0) { + memberDetails = foundMembers + searchFound = true + } + + for (let j = 0; j < memberDetails.length; j++) { + const memberId = memberIds[j] + if (members.find(m => m.id === memberId) === undefined) { + members.push({ + id: memberId, + name: memberDetails[j].name, + address: memberDetails[j].ownerAddress + }) + } + } + } + }) + + if (searching && !searchFound) { + const foundTx = await this.transactionMapper.get(search) + if (foundTx !== undefined) { + const transactionsOnBlock = await this.rpcClient.account.listAccountHistory('all', { maxBlockHeight: foundTx.block.height, depth: 0 }) + const transaction = transactionsOnBlock.find(tx => tx.txid === foundTx.txid) + + if (transaction === undefined) { + return { + total: 0, + transactions: [] + } + } + + return { + total: 1, + transactions: [this.formatTransactionResponse(transaction, members)] + } + } + + return { + total: 0, + transactions: [] + } + } + + let txs: AccountHistory[] = [] + + for (let i = 0; i < members.length; i++) { + const member = members[i] + + const mintTxsPromise = this.rpcClient.account.listAccountHistory(member.address, { + txtype: DfTxType.MINT_TOKEN, + maxBlockHeight: maxBlockHeight, + limit + }) + + const burnTxsPromise = this.rpcClient.account.listAccountHistory(member.address, { + txtype: DfTxType.BURN_TOKEN, + maxBlockHeight: maxBlockHeight, + limit + }) + + const burnTxCountPromise = this.rpcClient.account.historyCount(member.address, { txtype: DfTxType.BURN_TOKEN }) + const mintTxCountPromise = this.rpcClient.account.historyCount(member.address, { txtype: DfTxType.MINT_TOKEN }) + + const [mintTxs, burnTxs, burnTxCount, mintTxCount] = await Promise.all([mintTxsPromise, burnTxsPromise, burnTxCountPromise, mintTxCountPromise]) + + totalTxCount += burnTxCount + mintTxCount + txs.push(...mintTxs, ...burnTxs) + } + + txs = txs.sort((a: any, b: any) => b.blockTime - a.blockTime).slice(0, limit) + + return { + transactions: txs.map(tx => { + return this.formatTransactionResponse(tx, members) + }), + total: totalTxCount + } + } +} diff --git a/packages/jellyfish-api-core/src/category/account.ts b/packages/jellyfish-api-core/src/category/account.ts index 76071ca132..ca853326f0 100644 --- a/packages/jellyfish-api-core/src/category/account.ts +++ b/packages/jellyfish-api-core/src/category/account.ts @@ -13,6 +13,7 @@ export enum OwnerType { export enum DfTxType { MINT_TOKEN = 'M', + BURN_TOKEN = 'F', POOL_SWAP = 's', ADD_POOL_LIQUIDITY = 'l', REMOVE_POOL_LIQUIDITY = 'r', diff --git a/packages/whale-api-client/__tests__/api/consortium.test.ts b/packages/whale-api-client/__tests__/api/consortium.test.ts new file mode 100644 index 0000000000..01551dfb13 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/consortium.test.ts @@ -0,0 +1,259 @@ +import { StubWhaleApiClient } from '../stub.client' +import { TestingGroup } from '@defichain/jellyfish-testing' +import { StubService } from '../stub.service' +import { NestFastifyApplication } from '@nestjs/platform-fastify' +import _ from 'lodash' +import { createTestingApp, stopTestingApp, waitForIndexedHeight } from '../../../../apps/whale-api/src/e2e.module' + +describe('getTransactionHistory', () => { + const tGroup = TestingGroup.create(2) + const alice = tGroup.get(0) + const bob = tGroup.get(1) + const symbolBTC = 'dBTC' + const symbolETH = 'dETH' + let accountAlice: string, accountBob: string + let app: NestFastifyApplication + let idBTC: string + let idETH: string + const service = new StubService(alice.container) + const client = new StubWhaleApiClient(service) + + beforeAll(async () => { + await tGroup.start() + await service.start() + await alice.container.waitForWalletCoinbaseMaturity() + + app = await createTestingApp(alice.container) + + await setup() + }) + + afterAll(async () => { + try { + await service.stop() + } finally { + await stopTestingApp(tGroup, app) + } + }) + + async function setGovAttr (ATTRIBUTES: object): Promise { + const hash = await alice.rpc.masternode.setGov({ ATTRIBUTES }) + expect(hash).toBeTruthy() + await alice.generate(1) + } + + async function setMemberInfo (tokenId: string, memberInfo: Array<{ id: string, name: string, backingId: string, ownerAddress: string, mintLimit: string, dailyMintLimit: string }>): Promise { + const infoObjs = memberInfo.map(mi => ` + "${mi.id}":{ + "name":"${mi.name}", + "ownerAddress":"${mi.ownerAddress}", + "backingId":"${mi.backingId}", + "dailyMintLimit":${mi.dailyMintLimit}, + "mintLimit":${mi.mintLimit} + }`) + + return await setGovAttr({ [`v0/consortium/${tokenId}/members`]: `{${infoObjs.join(',')}}` }) + } + + async function setup (): Promise { + accountAlice = await alice.generateAddress() + accountBob = await bob.generateAddress() + + await alice.token.create({ + symbol: symbolBTC, + name: symbolBTC, + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: accountAlice + }) + await alice.generate(1) + + await alice.token.create({ + symbol: symbolETH, + name: symbolETH, + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: accountAlice + }) + await alice.generate(1) + + await alice.container.fundAddress(accountBob, 10) + await alice.generate(1) + idBTC = await alice.token.getTokenId(symbolBTC) + idETH = await alice.token.getTokenId(symbolETH) + + await setGovAttr({ + 'v0/params/feature/consortium': 'true', + [`v0/consortium/${idBTC}/mint_limit`]: '10', + [`v0/consortium/${idBTC}/mint_limit_daily`]: '5', + [`v0/consortium/${idETH}/mint_limit`]: '20', + [`v0/consortium/${idETH}/mint_limit_daily`]: '10' + }) + + await setMemberInfo(idBTC, [{ + id: '01', + name: 'alice', + ownerAddress: accountAlice, + backingId: 'abc', + dailyMintLimit: '5.00000000', + mintLimit: '10.00000000' + }, { + id: '02', + name: 'bob', + ownerAddress: accountBob, + backingId: 'def,hij', + dailyMintLimit: '5.00000000', + mintLimit: '10.00000000' + }]) + + await setMemberInfo(idETH, [{ + id: '01', + name: 'alice', + ownerAddress: accountAlice, + backingId: '', + dailyMintLimit: '10.00000000', + mintLimit: '20.00000000' + }, { + id: '02', + name: 'bob', + ownerAddress: accountBob, + backingId: 'lmn,opq', + dailyMintLimit: '10.00000000', + mintLimit: '20.00000000' + }]) + + await alice.rpc.token.mintTokens(`1@${symbolBTC}`) + await alice.generate(5) + + await alice.rpc.token.mintTokens(`2@${symbolETH}`) + await alice.generate(5) + + await alice.rpc.token.burnTokens(`1@${symbolETH}`, accountAlice) + await alice.generate(5) + + await bob.rpc.token.mintTokens(`4@${symbolBTC}`) + await bob.generate(5) + + await bob.rpc.token.burnTokens(`2@${symbolBTC}`, accountBob) + await bob.generate(5) + + const height = await alice.container.getBlockCount() + await alice.generate(1) + await waitForIndexedHeight(app, height) + } + + it('should throw an error if the limit is invalid', async () => { + await expect(client.consortium.getTransactionHistory(51)).rejects.toThrow('InvalidLimit') + await expect(client.consortium.getTransactionHistory(0)).rejects.toThrow('InvalidLimit') + }) + + it('should throw an error if the search term is invalid', async () => { + await expect(client.consortium.getTransactionHistory(1, 'a')).rejects.toThrow('InvalidSearchTerm') + await expect(client.consortium.getTransactionHistory(1, 'a'.repeat(65))).rejects.toThrow('InvalidSearchTerm') + }) + + it('should throw an error if the max block height is invalid', async () => { + await expect(client.consortium.getTransactionHistory(1, undefined, -2)).rejects.toThrow('InvalidmaxBlockHeight') + }) + + it('should filter transactions with search term (member name)', async () => { + const info = await client.consortium.getTransactionHistory(10, 'alice') + + expect(info.transactions.length).toStrictEqual(3) + + expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) + expect(info.transactions[0].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) + expect(info.transactions[1].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 }) + expect(info.transactions[2].txId).not.toBe(undefined) + + expect(info.total).toStrictEqual(3) + }) + + it('should filter transactions with search term (owner address)', async () => { + const info = await client.consortium.getTransactionHistory(20, accountAlice) + + expect(info.transactions.length).toStrictEqual(3) + + expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) + expect(info.transactions[0].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) + expect(info.transactions[1].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 }) + expect(info.transactions[2].txId).not.toBe(undefined) + + expect(info.total).toStrictEqual(3) + }) + + it('should filter transactions with search term (transaction id)', async () => { + const tx = (await alice.rpc.account.listAccountHistory(accountAlice))[0] + + const info = await client.consortium.getTransactionHistory(20, tx.txid) + + expect(info.transactions.length).toStrictEqual(1) + expect(info.transactions[0]).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119, txId: tx.txid }) + expect(info.total).toStrictEqual(1) + }) + + it('should limit transactions', async () => { + const info = await client.consortium.getTransactionHistory(3) + + expect(info.transactions.length).toStrictEqual(3) + + expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 129 }) + expect(info.transactions[0].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }) + expect(info.transactions[1].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) + expect(info.transactions[2].txId).not.toBe(undefined) + + expect(info.total).toStrictEqual(5) + }) + + it('should filter and limit transactions at the same time', async () => { + const info = await client.consortium.getTransactionHistory(2, accountAlice) + + expect(info.transactions.length).toStrictEqual(2) + + expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) + expect(info.transactions[0].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) + expect(info.transactions[1].txId).not.toBe(undefined) + + expect(info.total).toStrictEqual(3) + }) + + it('should get transactions upto a specific block height with a limit', async () => { + const info = await client.consortium.getTransactionHistory(3, undefined, 124) + + expect(info.transactions.length).toStrictEqual(3) + + expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }) + expect(info.transactions[0].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) + expect(info.transactions[1].txId).not.toBe(undefined) + + expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) + expect(info.transactions[2].txId).not.toBe(undefined) + + expect(info.total).toStrictEqual(5) + }) + + it('should return empty list of transactions for invalid search term', async () => { + const info = await client.consortium.getTransactionHistory(20, 'invalid-term') + + expect(info.transactions.length).toStrictEqual(0) + expect(info.total).toStrictEqual(0) + }) +}) diff --git a/packages/whale-api-client/src/api/consortium.ts b/packages/whale-api-client/src/api/consortium.ts new file mode 100644 index 0000000000..72912532b2 --- /dev/null +++ b/packages/whale-api-client/src/api/consortium.ts @@ -0,0 +1,54 @@ +import { WhaleApiClient } from '../whale.api.client' + +/** + * DeFi whale endpoint for consortium related services. + */ +export class Consortium { + constructor (private readonly client: WhaleApiClient) { + } + + /** + * Gets the transaction history of consortium members. + * @param {number} limit how many transactions to fetch + * @param {string} [search] search term, can be a transaction id, member/owner address or member name + * @param {number} [maxBlockHeight] the maximum block height to look for, -1 for current tip by default + * @return {Promise} + */ + async getTransactionHistory (limit: number, search?: string, maxBlockHeight?: number): Promise { + const query = [] + + if (limit !== undefined) { + query.push(`limit=${limit}`) + } + + if (search !== undefined) { + query.push(`search=${search}`) + } + + if (maxBlockHeight !== undefined) { + query.push(`maxBlockHeight=${maxBlockHeight}`) + } + + return await this.client.requestData('GET', `consortium/transactions?${query.join('&')}`) + } +} + +export interface ConsortiumTransactionResponse { + transactions: Transaction[] + total: number +} + +export interface Transaction { + type: 'Mint' | 'Burn' + member: string + tokenAmounts: Array<{ token: string, amount: string }> + txId: string + address: string + block: number +} + +export interface ConsortiumMember { + id: string + name: string + address: string +} diff --git a/packages/whale-api-client/src/index.ts b/packages/whale-api-client/src/index.ts index 8f12fa3c86..6457636bc7 100644 --- a/packages/whale-api-client/src/index.ts +++ b/packages/whale-api-client/src/index.ts @@ -13,6 +13,7 @@ export * as stats from './api/stats' export * as rawtx from './api/rawtx' export * as fee from './api/fee' export * as loan from './api/loan' +export * as consortium from './api/consortium' export * from './whale.api.client' export * from './whale.api.response' diff --git a/packages/whale-api-client/src/whale.api.client.ts b/packages/whale-api-client/src/whale.api.client.ts index fa43aa62ac..d37bb0b89b 100644 --- a/packages/whale-api-client/src/whale.api.client.ts +++ b/packages/whale-api-client/src/whale.api.client.ts @@ -14,6 +14,7 @@ import { Stats } from './api/stats' import { Rawtx } from './api/rawtx' import { Fee } from './api/fee' import { Loan } from './api/loan' +import { Consortium } from './api/consortium' import { ApiPagedResponse, WhaleApiResponse } from './whale.api.response' import { raiseIfError, WhaleApiException, WhaleClientException, WhaleClientTimeoutException } from './errors' import { NetworkName } from '@defichain/jellyfish-network' @@ -76,6 +77,7 @@ export class WhaleApiClient { public readonly rawtx = new Rawtx(this) public readonly fee = new Fee(this) public readonly loan = new Loan(this) + public readonly consortium = new Consortium(this) constructor ( protected readonly options: WhaleApiClientOptions From 7934b46dbd0e4f3ac34da7a606bcf37defab1fa2 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Wed, 23 Nov 2022 20:56:49 +0800 Subject: [PATCH 13/39] Fix change requests --- .../module.api/consortium.controller.spec.ts | 88 ++++++++----------- .../src/module.api/consortium.controller.ts | 2 +- .../__tests__/api/consortium.test.ts | 88 ++++++++----------- 3 files changed, 77 insertions(+), 101 deletions(-) diff --git a/apps/whale-api/src/module.api/consortium.controller.spec.ts b/apps/whale-api/src/module.api/consortium.controller.spec.ts index bbd4f89cb8..ed14dc4e83 100644 --- a/apps/whale-api/src/module.api/consortium.controller.spec.ts +++ b/apps/whale-api/src/module.api/consortium.controller.spec.ts @@ -2,7 +2,6 @@ import { ConsortiumController } from './consortium.controller' import { TestingGroup } from '@defichain/jellyfish-testing' import { createTestingApp, stopTestingApp, waitForIndexedHeight } from '../e2e.module' import { NestFastifyApplication } from '@nestjs/platform-fastify' -import _ from 'lodash' describe('getTransactionHistory', () => { const tGroup = TestingGroup.create(2) @@ -15,6 +14,7 @@ describe('getTransactionHistory', () => { let idETH: string let app: NestFastifyApplication let controller: ConsortiumController + const txIdMatcher = expect.stringMatching(/[0-f]{64}/) beforeAll(async () => { await tGroup.start() @@ -150,23 +150,18 @@ describe('getTransactionHistory', () => { }) it('should throw an error if the max block height is invalid', async () => { - await expect(controller.getTransactionHistory({ maxBlockHeight: -2, limit: 1 })).rejects.toThrow('InvalidmaxBlockHeight') + await expect(controller.getTransactionHistory({ maxBlockHeight: -2, limit: 1 })).rejects.toThrow('InvalidMaxBlockHeight') }) it('should filter transactions with search term (member name)', async () => { const info = await controller.getTransactionHistory({ search: 'alice', limit: 20 }) expect(info.transactions.length).toStrictEqual(3) - - expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) - expect(info.transactions[0].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) - expect(info.transactions[1].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 }) - expect(info.transactions[2].txId).not.toBe(undefined) - + expect(info.transactions).toStrictEqual([ + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 } + ]) expect(info.total).toStrictEqual(3) }) @@ -174,16 +169,11 @@ describe('getTransactionHistory', () => { const info = await controller.getTransactionHistory({ search: accountAlice, limit: 20 }) expect(info.transactions.length).toStrictEqual(3) - - expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) - expect(info.transactions[0].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) - expect(info.transactions[1].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 }) - expect(info.transactions[2].txId).not.toBe(undefined) - + expect(info.transactions).toStrictEqual([ + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 } + ]) expect(info.total).toStrictEqual(3) }) @@ -193,7 +183,9 @@ describe('getTransactionHistory', () => { const info = await controller.getTransactionHistory({ search: tx.txid, limit: 20 }) expect(info.transactions.length).toStrictEqual(1) - expect(info.transactions[0]).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119, txId: tx.txid }) + expect(info.transactions).toStrictEqual([ + { txId: tx.txid, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 } + ]) expect(info.total).toStrictEqual(1) }) @@ -201,16 +193,11 @@ describe('getTransactionHistory', () => { const info = await controller.getTransactionHistory({ limit: 3 }) expect(info.transactions.length).toStrictEqual(3) - - expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 129 }) - expect(info.transactions[0].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }) - expect(info.transactions[1].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) - expect(info.transactions[2].txId).not.toBe(undefined) - + expect(info.transactions).toStrictEqual([ + { txId: txIdMatcher, type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 129 }, + { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }, + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 } + ]) expect(info.total).toStrictEqual(5) }) @@ -218,13 +205,10 @@ describe('getTransactionHistory', () => { const info = await controller.getTransactionHistory({ search: accountAlice, limit: 2 }) expect(info.transactions.length).toStrictEqual(2) - - expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) - expect(info.transactions[0].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) - expect(info.transactions[1].txId).not.toBe(undefined) - + expect(info.transactions).toStrictEqual([ + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 } + ]) expect(info.total).toStrictEqual(3) }) @@ -232,16 +216,11 @@ describe('getTransactionHistory', () => { const info = await controller.getTransactionHistory({ limit: 3, maxBlockHeight: 124 }) expect(info.transactions.length).toStrictEqual(3) - - expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }) - expect(info.transactions[0].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) - expect(info.transactions[1].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) - expect(info.transactions[2].txId).not.toBe(undefined) - + expect(info.transactions).toStrictEqual([ + { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }, + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 } + ]) expect(info.total).toStrictEqual(5) }) @@ -251,4 +230,13 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(0) expect(info.total).toStrictEqual(0) }) + + it('should not return other transactions from consortium members apart from mints or burns', async () => { + const { txid } = await alice.container.fundAddress(accountBob, 11.5) + + const info = await controller.getTransactionHistory({ search: txid, limit: 20 }) + + expect(info.transactions.length).toStrictEqual(0) + expect(info.total).toStrictEqual(0) + }) }) diff --git a/apps/whale-api/src/module.api/consortium.controller.ts b/apps/whale-api/src/module.api/consortium.controller.ts index 799aa6a21a..2b330abb51 100644 --- a/apps/whale-api/src/module.api/consortium.controller.ts +++ b/apps/whale-api/src/module.api/consortium.controller.ts @@ -30,7 +30,7 @@ export class ConsortiumController { } if (maxBlockHeight < -1) { - throw new ForbiddenException('InvalidmaxBlockHeight') + throw new ForbiddenException('InvalidMaxBlockHeight') } return await this.cache.get(`CONSORTIUM_TRANSACTIONS_${JSON.stringify(query)}`, async () => { diff --git a/packages/whale-api-client/__tests__/api/consortium.test.ts b/packages/whale-api-client/__tests__/api/consortium.test.ts index 01551dfb13..0a0581442c 100644 --- a/packages/whale-api-client/__tests__/api/consortium.test.ts +++ b/packages/whale-api-client/__tests__/api/consortium.test.ts @@ -2,7 +2,6 @@ import { StubWhaleApiClient } from '../stub.client' import { TestingGroup } from '@defichain/jellyfish-testing' import { StubService } from '../stub.service' import { NestFastifyApplication } from '@nestjs/platform-fastify' -import _ from 'lodash' import { createTestingApp, stopTestingApp, waitForIndexedHeight } from '../../../../apps/whale-api/src/e2e.module' describe('getTransactionHistory', () => { @@ -17,6 +16,7 @@ describe('getTransactionHistory', () => { let idETH: string const service = new StubService(alice.container) const client = new StubWhaleApiClient(service) + const txIdMatcher = expect.stringMatching(/[0-f]{64}/) beforeAll(async () => { await tGroup.start() @@ -155,23 +155,18 @@ describe('getTransactionHistory', () => { }) it('should throw an error if the max block height is invalid', async () => { - await expect(client.consortium.getTransactionHistory(1, undefined, -2)).rejects.toThrow('InvalidmaxBlockHeight') + await expect(client.consortium.getTransactionHistory(1, undefined, -2)).rejects.toThrow('InvalidMaxBlockHeight') }) it('should filter transactions with search term (member name)', async () => { const info = await client.consortium.getTransactionHistory(10, 'alice') expect(info.transactions.length).toStrictEqual(3) - - expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) - expect(info.transactions[0].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) - expect(info.transactions[1].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 }) - expect(info.transactions[2].txId).not.toBe(undefined) - + expect(info.transactions).toStrictEqual([ + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 } + ]) expect(info.total).toStrictEqual(3) }) @@ -179,16 +174,11 @@ describe('getTransactionHistory', () => { const info = await client.consortium.getTransactionHistory(20, accountAlice) expect(info.transactions.length).toStrictEqual(3) - - expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) - expect(info.transactions[0].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) - expect(info.transactions[1].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 }) - expect(info.transactions[2].txId).not.toBe(undefined) - + expect(info.transactions).toStrictEqual([ + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 } + ]) expect(info.total).toStrictEqual(3) }) @@ -198,7 +188,9 @@ describe('getTransactionHistory', () => { const info = await client.consortium.getTransactionHistory(20, tx.txid) expect(info.transactions.length).toStrictEqual(1) - expect(info.transactions[0]).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119, txId: tx.txid }) + expect(info.transactions).toStrictEqual([ + { txId: tx.txid, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 } + ]) expect(info.total).toStrictEqual(1) }) @@ -206,16 +198,11 @@ describe('getTransactionHistory', () => { const info = await client.consortium.getTransactionHistory(3) expect(info.transactions.length).toStrictEqual(3) - - expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 129 }) - expect(info.transactions[0].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }) - expect(info.transactions[1].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) - expect(info.transactions[2].txId).not.toBe(undefined) - + expect(info.transactions).toStrictEqual([ + { txId: txIdMatcher, type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 129 }, + { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }, + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 } + ]) expect(info.total).toStrictEqual(5) }) @@ -223,13 +210,10 @@ describe('getTransactionHistory', () => { const info = await client.consortium.getTransactionHistory(2, accountAlice) expect(info.transactions.length).toStrictEqual(2) - - expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) - expect(info.transactions[0].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) - expect(info.transactions[1].txId).not.toBe(undefined) - + expect(info.transactions).toStrictEqual([ + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 } + ]) expect(info.total).toStrictEqual(3) }) @@ -237,16 +221,11 @@ describe('getTransactionHistory', () => { const info = await client.consortium.getTransactionHistory(3, undefined, 124) expect(info.transactions.length).toStrictEqual(3) - - expect(_.omit(info.transactions[0], ['txId'])).toStrictEqual({ type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }) - expect(info.transactions[0].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[1], ['txId'])).toStrictEqual({ type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }) - expect(info.transactions[1].txId).not.toBe(undefined) - - expect(_.omit(info.transactions[2], ['txId'])).toStrictEqual({ type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }) - expect(info.transactions[2].txId).not.toBe(undefined) - + expect(info.transactions).toStrictEqual([ + { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }, + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 } + ]) expect(info.total).toStrictEqual(5) }) @@ -256,4 +235,13 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(0) expect(info.total).toStrictEqual(0) }) + + it('should not return other transactions from consortium members apart from mints or burns', async () => { + const { txid } = await alice.container.fundAddress(accountBob, 10) + + const info = await client.consortium.getTransactionHistory(20, txid) + + expect(info.transactions.length).toStrictEqual(0) + expect(info.total).toStrictEqual(0) + }) }) From f2d02346eacc9f2f5486f47b59b165d412b62b92 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Thu, 24 Nov 2022 13:19:14 +0800 Subject: [PATCH 14/39] Add test to validate pagination --- .../module.api/consortium.controller.spec.ts | 66 +++++++++++++++++++ .../__tests__/api/consortium.test.ts | 66 +++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/apps/whale-api/src/module.api/consortium.controller.spec.ts b/apps/whale-api/src/module.api/consortium.controller.spec.ts index ed14dc4e83..cb96baeedf 100644 --- a/apps/whale-api/src/module.api/consortium.controller.spec.ts +++ b/apps/whale-api/src/module.api/consortium.controller.spec.ts @@ -239,4 +239,70 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(0) expect(info.total).toStrictEqual(0) }) + + it('should paginate properly', async () => { + const page1 = await controller.getTransactionHistory({ limit: 2 }) + + expect(page1).toStrictEqual({ + transactions: [ + { + type: 'Burn', + member: 'bob', + tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], + txId: txIdMatcher, + address: accountBob, + block: 129 + }, + { + type: 'Mint', + member: 'bob', + tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], + txId: txIdMatcher, + address: accountBob, + block: 124 + } + ], + total: 5 + }) + + const page2 = await controller.getTransactionHistory({ limit: 2, maxBlockHeight: page1.transactions[page1.transactions.length - 1].block - 1 }) + + expect(page2).toStrictEqual({ + transactions: [ + { + type: 'Burn', + member: 'alice', + tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], + txId: txIdMatcher, + address: accountAlice, + block: 119 + }, + { + type: 'Mint', + member: 'alice', + tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], + txId: txIdMatcher, + address: accountAlice, + block: 114 + } + ], + total: 5 + }) + + const page3 = await controller.getTransactionHistory({ limit: 2, maxBlockHeight: page2.transactions[page2.transactions.length - 1].block - 1 }) + + expect(page3).toStrictEqual({ + transactions: [ + { + type: 'Mint', + member: 'alice', + tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], + txId: txIdMatcher, + address: accountAlice, + block: 109 + } + ], + total: 5 + }) + }) }) diff --git a/packages/whale-api-client/__tests__/api/consortium.test.ts b/packages/whale-api-client/__tests__/api/consortium.test.ts index 0a0581442c..79df446318 100644 --- a/packages/whale-api-client/__tests__/api/consortium.test.ts +++ b/packages/whale-api-client/__tests__/api/consortium.test.ts @@ -244,4 +244,70 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(0) expect(info.total).toStrictEqual(0) }) + + it('should paginate properly', async () => { + const page1 = await client.consortium.getTransactionHistory(2, undefined, -1) + + expect(page1).toStrictEqual({ + transactions: [ + { + type: 'Burn', + member: 'bob', + tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], + txId: txIdMatcher, + address: accountBob, + block: 129 + }, + { + type: 'Mint', + member: 'bob', + tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], + txId: txIdMatcher, + address: accountBob, + block: 124 + } + ], + total: 5 + }) + + const page2 = await client.consortium.getTransactionHistory(2, undefined, page1.transactions[page1.transactions.length - 1].block - 1) + + expect(page2).toStrictEqual({ + transactions: [ + { + type: 'Burn', + member: 'alice', + tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], + txId: txIdMatcher, + address: accountAlice, + block: 119 + }, + { + type: 'Mint', + member: 'alice', + tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], + txId: txIdMatcher, + address: accountAlice, + block: 114 + } + ], + total: 5 + }) + + const page3 = await client.consortium.getTransactionHistory(2, undefined, page2.transactions[page2.transactions.length - 1].block - 1) + + expect(page3).toStrictEqual({ + transactions: [ + { + type: 'Mint', + member: 'alice', + tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], + txId: txIdMatcher, + address: accountAlice, + block: 109 + } + ], + total: 5 + }) + }) }) From c0e8a6e59dea040c463995c710f7cbabc3fbceb5 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Mon, 12 Dec 2022 17:44:43 +0800 Subject: [PATCH 15/39] Update to support pagination --- apps/whale-api/docker-compose.yml | 2 +- .../account/listAccountHistory.test.ts | 59 +++++++++++++++++-- .../src/category/account.ts | 7 +++ .../src/containers/DeFiDContainer.ts | 2 +- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/apps/whale-api/docker-compose.yml b/apps/whale-api/docker-compose.yml index bb7dd884aa..f956c8f2a1 100644 --- a/apps/whale-api/docker-compose.yml +++ b/apps/whale-api/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' services: defi-blockchain: - image: defi/defichain:3.1.1 + image: defi/defichain:HEAD-54ad178ed ports: - "19554:19554" command: > diff --git a/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts b/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts index faf9537bca..ea4665025b 100644 --- a/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts @@ -21,17 +21,24 @@ function createTokenForContainer (container: MasterNodeRegTestContainer) { } } -describe('Account', () => { +describe.only('Account', () => { const container = new MasterNodeRegTestContainer() const client = new ContainerAdapterClient(container) const createToken = createTokenForContainer(container) + let addr: string + beforeAll(async () => { await container.start() - await container.waitForReady() await container.waitForWalletCoinbaseMaturity() await container.waitForWalletBalanceGTE(100) - await createToken(await container.getNewAddress(), 'DBTC', 200) + + addr = await container.getNewAddress() + await createToken(addr, 'DBTC', 201) + await container.generate(1) + + await client.token.burnTokens('1@DBTC', addr) + await container.generate(1) }) afterAll(async () => { @@ -223,8 +230,8 @@ describe('Account', () => { const accountHistories = await client.account.listAccountHistory('mine', options) expect(accountHistories.length).toBeGreaterThan(0) accountHistories.forEach(accountHistory => { - expect(accountHistory.blockHeight).toBeLessThanOrEqual(103) - if (accountHistory.blockHeight === 103) { + expect(accountHistory.blockHeight).toBeLessThanOrEqual(105) + if (accountHistory.blockHeight === 105) { expect(accountHistory.txn).toBeLessThanOrEqual(options.txn) } }) @@ -358,6 +365,48 @@ describe('Account', () => { } } }) + + it('should listAccountHistory with option txtypes', async () => { + const accountHistory = await client.account.listAccountHistory('all', { txtypes: [DfTxType.MINT_TOKEN, DfTxType.BURN_TOKEN] }) + expect(accountHistory.length).toStrictEqual(3) + + const expectedOutputs = [{ + blockHeight: 103, + type: 'MintToken', + amounts: ['201.00000000@DBTC'] + }, { + blockHeight: 105, + type: 'BurnToken', + amounts: ['1.00000000@DBTC'] + }, { + blockHeight: 105, + type: 'BurnToken', + amounts: ['-1.00000000@DBTC'] + }] + + for (let i = 0; i < 3; i++) { + const output = expectedOutputs.pop()! + expect(accountHistory[i].type).toStrictEqual(output.type) + expect(accountHistory[i].blockHeight).toStrictEqual(output.blockHeight) + expect(accountHistory[i].amounts).toStrictEqual(output.amounts) + } + + expect(expectedOutputs.length).toStrictEqual(0) + }) + + it('should listAccountHistory with options start, including_start and limit', async () => { + const accountHistoryAll = await client.account.listAccountHistory('all') + + const accountHistory1 = await client.account.listAccountHistory('all', { start: 50 }) + expect(accountHistoryAll[49].txid).toStrictEqual(accountHistory1[0].txid) + + const accountHistory2 = await client.account.listAccountHistory('all', { start: 50, including_start: true }) + expect(accountHistoryAll[48].txid).toStrictEqual(accountHistory2[0].txid) + + const accountHistory3 = await client.account.listAccountHistory('all', { start: 50, including_start: true, limit: 3 }) + expect(accountHistoryAll[48].txid).toStrictEqual(accountHistory3[0].txid) + expect(accountHistory3.length).toBe(3) + }) }) describe('listAccountHistory', () => { diff --git a/packages/jellyfish-api-core/src/category/account.ts b/packages/jellyfish-api-core/src/category/account.ts index 76071ca132..f38b15e7be 100644 --- a/packages/jellyfish-api-core/src/category/account.ts +++ b/packages/jellyfish-api-core/src/category/account.ts @@ -13,6 +13,7 @@ export enum OwnerType { export enum DfTxType { MINT_TOKEN = 'M', + BURN_TOKEN = 'F', POOL_SWAP = 's', ADD_POOL_LIQUIDITY = 'l', REMOVE_POOL_LIQUIDITY = 'r', @@ -300,7 +301,10 @@ export class Account { * @param {boolean} [options.no_rewards] Filter out rewards * @param {string} [options.token] Filter by token * @param {DfTxType} [options.txtype] Filter by transaction type. See DfTxType. + * @param {DfTxType[]} [options.txtypes] Filter multiple transaction types, supported letter from {CustomTxType}. * @param {number} [options.limit=100] Maximum number of records to return, 100 by default + * @param {number} [options.start] Number of entries to skip + * @param {boolean} [options.including_start=false] If true, then iterate including starting position. False by default * @param {number} [options.txn] Order in block, unlimited by default * @param {Format} [options.format] Set the return amount format, Format.SYMBOL by default * @return {Promise} @@ -539,7 +543,10 @@ export interface AccountHistoryOptions { no_rewards?: boolean token?: string txtype?: DfTxType + txtypes?: DfTxType[] limit?: number + start?: number + including_start?: boolean txn?: number format?: Format } diff --git a/packages/testcontainers/src/containers/DeFiDContainer.ts b/packages/testcontainers/src/containers/DeFiDContainer.ts index 746501aee5..37d45c76d5 100644 --- a/packages/testcontainers/src/containers/DeFiDContainer.ts +++ b/packages/testcontainers/src/containers/DeFiDContainer.ts @@ -35,7 +35,7 @@ export abstract class DeFiDContainer extends DockerContainer { if (process?.env?.DEFICHAIN_DOCKER_IMAGE !== undefined) { return process.env.DEFICHAIN_DOCKER_IMAGE } - return 'defi/defichain:3.1.1' + return 'defi/defichain:HEAD-54ad178ed' } public static readonly DefaultStartOptions = { From 653c61df82c3a5c07d02c46648a16a938094a0f1 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Mon, 12 Dec 2022 18:53:28 +0800 Subject: [PATCH 16/39] Remove .only --- .../__tests__/category/account/listAccountHistory.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts b/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts index ea4665025b..89788e5f0e 100644 --- a/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts @@ -21,7 +21,7 @@ function createTokenForContainer (container: MasterNodeRegTestContainer) { } } -describe.only('Account', () => { +describe('Account', () => { const container = new MasterNodeRegTestContainer() const client = new ContainerAdapterClient(container) const createToken = createTokenForContainer(container) From 9328d18d675ed9169994be73db23606632626b5d Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Tue, 13 Dec 2022 14:30:32 +0800 Subject: [PATCH 17/39] Update tests --- .../account/listAccountHistory.test.ts | 64 +++++++------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts b/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts index 89788e5f0e..7a1b452949 100644 --- a/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts @@ -1,7 +1,7 @@ import { MasterNodeRegTestContainer } from '@defichain/testcontainers' import { ContainerAdapterClient } from '../../container_adapter_client' import waitForExpect from 'wait-for-expect' -import { DfTxType, BalanceTransferPayload, AccountResult, AccountOwner, Format } from '../../../src/category/account' +import { AccountOwner, AccountResult, BalanceTransferPayload, DfTxType, Format } from '../../../src/category/account' function createTokenForContainer (container: MasterNodeRegTestContainer) { return async (address: string, symbol: string, amount: number) => { @@ -35,10 +35,12 @@ describe('Account', () => { addr = await container.getNewAddress() await createToken(addr, 'DBTC', 201) - await container.generate(1) - await client.token.burnTokens('1@DBTC', addr) - await container.generate(1) + // Add many transactions to test pagination + for (let i = 1; i <= 10; i++) { + await client.token.burnTokens(`${0.01 * i}@DBTC`, addr) + await container.generate(1) + } }) afterAll(async () => { @@ -230,8 +232,8 @@ describe('Account', () => { const accountHistories = await client.account.listAccountHistory('mine', options) expect(accountHistories.length).toBeGreaterThan(0) accountHistories.forEach(accountHistory => { - expect(accountHistory.blockHeight).toBeLessThanOrEqual(105) - if (accountHistory.blockHeight === 105) { + expect(accountHistory.blockHeight).toBeLessThanOrEqual(113) + if (accountHistory.blockHeight === 113) { expect(accountHistory.txn).toBeLessThanOrEqual(options.txn) } }) @@ -366,46 +368,24 @@ describe('Account', () => { } }) - it('should listAccountHistory with option txtypes', async () => { - const accountHistory = await client.account.listAccountHistory('all', { txtypes: [DfTxType.MINT_TOKEN, DfTxType.BURN_TOKEN] }) - expect(accountHistory.length).toStrictEqual(3) - - const expectedOutputs = [{ - blockHeight: 103, - type: 'MintToken', - amounts: ['201.00000000@DBTC'] - }, { - blockHeight: 105, - type: 'BurnToken', - amounts: ['1.00000000@DBTC'] - }, { - blockHeight: 105, - type: 'BurnToken', - amounts: ['-1.00000000@DBTC'] - }] - - for (let i = 0; i < 3; i++) { - const output = expectedOutputs.pop()! - expect(accountHistory[i].type).toStrictEqual(output.type) - expect(accountHistory[i].blockHeight).toStrictEqual(output.blockHeight) - expect(accountHistory[i].amounts).toStrictEqual(output.amounts) - } - - expect(expectedOutputs.length).toStrictEqual(0) - }) + it('should listAccountHistory with options start, including_start, limit and txtypes', async () => { + const txtypes = [DfTxType.BURN_TOKEN, DfTxType.MINT_TOKEN] - it('should listAccountHistory with options start, including_start and limit', async () => { - const accountHistoryAll = await client.account.listAccountHistory('all') + const accountHistoryAll = await client.account.listAccountHistory('mine', { txtypes }) + expect(accountHistoryAll.length).toStrictEqual(11) - const accountHistory1 = await client.account.listAccountHistory('all', { start: 50 }) - expect(accountHistoryAll[49].txid).toStrictEqual(accountHistory1[0].txid) + const accountHistory1 = await client.account.listAccountHistory('mine', { start: 2, txtypes }) + expect(accountHistoryAll[3].txid).toStrictEqual(accountHistory1[0].txid) + expect(accountHistory1.length).toStrictEqual(8) - const accountHistory2 = await client.account.listAccountHistory('all', { start: 50, including_start: true }) - expect(accountHistoryAll[48].txid).toStrictEqual(accountHistory2[0].txid) + const accountHistory2 = await client.account.listAccountHistory('mine', { start: 2, including_start: true, txtypes }) + expect(accountHistoryAll[2].txid).toStrictEqual(accountHistory2[0].txid) + expect(accountHistory2.length).toStrictEqual(9) - const accountHistory3 = await client.account.listAccountHistory('all', { start: 50, including_start: true, limit: 3 }) - expect(accountHistoryAll[48].txid).toStrictEqual(accountHistory3[0].txid) - expect(accountHistory3.length).toBe(3) + const accountHistory3 = await client.account.listAccountHistory('mine', { start: 2, including_start: true, limit: 3, txtypes }) + expect(accountHistory3[0].txid).toStrictEqual(accountHistoryAll[2].txid) + expect(accountHistory3[2].txid).toStrictEqual(accountHistoryAll[4].txid) + expect(accountHistory3.length).toStrictEqual(3) }) }) From fe89127a5b321f6a1e148150842aa8eb7577ef93 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Thu, 22 Dec 2022 18:24:52 +0800 Subject: [PATCH 18/39] Add multi-address support --- apps/whale-api/docker-compose.yml | 2 +- .../account/listAccountHistory.test.ts | 67 ++++++++++++------- .../src/category/account.ts | 4 +- .../src/containers/DeFiDContainer.ts | 2 +- 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/apps/whale-api/docker-compose.yml b/apps/whale-api/docker-compose.yml index f956c8f2a1..8b754d1bfe 100644 --- a/apps/whale-api/docker-compose.yml +++ b/apps/whale-api/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' services: defi-blockchain: - image: defi/defichain:HEAD-54ad178ed + image: defi/defichain:HEAD-b7a621dc2 ports: - "19554:19554" command: > diff --git a/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts b/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts index 7a1b452949..b30f15460f 100644 --- a/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts @@ -25,20 +25,29 @@ describe('Account', () => { const container = new MasterNodeRegTestContainer() const client = new ContainerAdapterClient(container) const createToken = createTokenForContainer(container) + const txtypes = [DfTxType.BURN_TOKEN, DfTxType.MINT_TOKEN] - let addr: string + let addr1: string + let addr2: string beforeAll(async () => { await container.start() await container.waitForWalletCoinbaseMaturity() await container.waitForWalletBalanceGTE(100) - addr = await container.getNewAddress() - await createToken(addr, 'DBTC', 201) + addr1 = await container.getNewAddress() + addr2 = await container.getNewAddress() + + await createToken(addr1, 'dBTC', 201) + await createToken(addr2, 'dETH', 100) + + for (let i = 1; i <= 10; i++) { + await client.token.burnTokens(`${0.01 * i}@dBTC`, addr1) + await container.generate(1) + } - // Add many transactions to test pagination for (let i = 1; i <= 10; i++) { - await client.token.burnTokens(`${0.01 * i}@DBTC`, addr) + await client.token.burnTokens(`${0.01 * i}@dETH`, addr2) await container.generate(1) } }) @@ -167,7 +176,7 @@ describe('Account', () => { it('should listAccountHistory with options token', async () => { const options = { - token: 'DBTC' + token: 'dBTC' } await waitForExpect(async () => { const accountHistories = await client.account.listAccountHistory('mine', options) @@ -181,7 +190,7 @@ describe('Account', () => { for (let j = 0; j < accountHistory.amounts.length; j += 1) { const amount = accountHistory.amounts[j] const symbol = amount.split('@')[1] - expect(symbol).toStrictEqual('DBTC') + expect(symbol).toStrictEqual('dBTC') } } }) @@ -232,8 +241,8 @@ describe('Account', () => { const accountHistories = await client.account.listAccountHistory('mine', options) expect(accountHistories.length).toBeGreaterThan(0) accountHistories.forEach(accountHistory => { - expect(accountHistory.blockHeight).toBeLessThanOrEqual(113) - if (accountHistory.blockHeight === 113) { + expect(accountHistory.blockHeight).toBeLessThanOrEqual(125) + if (accountHistory.blockHeight === 125) { expect(accountHistory.txn).toBeLessThanOrEqual(options.txn) } }) @@ -278,7 +287,7 @@ describe('Account', () => { const { hex, addresses } = owner const options = { - maxBlockHeight: 103, + maxBlockHeight: 105, txn: 1 } const accountHistories = await client.account.listAccountHistory(hex, options) @@ -299,7 +308,7 @@ describe('Account', () => { const { hex, addresses } = owner const options = { - maxBlockHeight: 103, + maxBlockHeight: 105, txn: 0 } const accountHistories = await client.account.listAccountHistory(hex, options) @@ -316,7 +325,7 @@ describe('Account', () => { it('should listAccountHistory with options format', async () => { { // amount format should be id const options = { - token: 'DBTC', + token: 'dBTC', format: Format.ID } const accountHistories = await client.account.listAccountHistory('mine', options) @@ -334,7 +343,7 @@ describe('Account', () => { { // amount format should be symbol const options = { - token: 'DBTC', + token: 'dBTC', format: Format.SYMBOL } const accountHistories = await client.account.listAccountHistory('mine', options) @@ -345,7 +354,7 @@ describe('Account', () => { for (let j = 0; j < accountHistory.amounts.length; j += 1) { const amount = accountHistory.amounts[j] const symbol = amount.split('@')[1] - expect(symbol).toStrictEqual('DBTC') + expect(symbol).toStrictEqual('dBTC') } } } @@ -368,21 +377,29 @@ describe('Account', () => { } }) - it('should listAccountHistory with options start, including_start, limit and txtypes', async () => { - const txtypes = [DfTxType.BURN_TOKEN, DfTxType.MINT_TOKEN] + it('should listAccountHistory with multiple addresses', async () => { + const accountHistoryAll = await client.account.listAccountHistory([addr1, addr2]) + expect(accountHistoryAll.length).toStrictEqual(34) + }) + + it('should listAccountHistory with multiple addresses and txtypes', async () => { + const accountHistoryAll = await client.account.listAccountHistory([addr1, addr2], { txtypes }) + expect(accountHistoryAll.length).toStrictEqual(22) + }) - const accountHistoryAll = await client.account.listAccountHistory('mine', { txtypes }) - expect(accountHistoryAll.length).toStrictEqual(11) + it('should listAccountHistory with multiple addresses, txtypes along with index based pagination', async () => { + const accountHistoryAll = await client.account.listAccountHistory([addr1, addr2], { txtypes }) + expect(accountHistoryAll.length).toStrictEqual(22) - const accountHistory1 = await client.account.listAccountHistory('mine', { start: 2, txtypes }) - expect(accountHistoryAll[3].txid).toStrictEqual(accountHistory1[0].txid) - expect(accountHistory1.length).toStrictEqual(8) + const accountHistory1 = await client.account.listAccountHistory([addr1, addr2], { start: 2, txtypes }) + expect(accountHistory1[0].txid).toStrictEqual(accountHistoryAll[3].txid) + expect(accountHistory1.length).toStrictEqual(19) - const accountHistory2 = await client.account.listAccountHistory('mine', { start: 2, including_start: true, txtypes }) - expect(accountHistoryAll[2].txid).toStrictEqual(accountHistory2[0].txid) - expect(accountHistory2.length).toStrictEqual(9) + const accountHistory2 = await client.account.listAccountHistory([addr1, addr2], { start: 2, including_start: true, txtypes }) + expect(accountHistory2[0].txid).toStrictEqual(accountHistoryAll[2].txid) + expect(accountHistory2.length).toStrictEqual(20) - const accountHistory3 = await client.account.listAccountHistory('mine', { start: 2, including_start: true, limit: 3, txtypes }) + const accountHistory3 = await client.account.listAccountHistory([addr1, addr2], { start: 2, including_start: true, limit: 3, txtypes }) expect(accountHistory3[0].txid).toStrictEqual(accountHistoryAll[2].txid) expect(accountHistory3[2].txid).toStrictEqual(accountHistoryAll[4].txid) expect(accountHistory3.length).toStrictEqual(3) diff --git a/packages/jellyfish-api-core/src/category/account.ts b/packages/jellyfish-api-core/src/category/account.ts index f38b15e7be..27ab2712c0 100644 --- a/packages/jellyfish-api-core/src/category/account.ts +++ b/packages/jellyfish-api-core/src/category/account.ts @@ -294,7 +294,7 @@ export class Account { /** * Returns information about account history * - * @param {OwnerType | string} [owner=OwnerType.MINE] single account ID (CScript or address) or reserved words 'mine' to list history for all owned accounts or 'all' to list whole DB + * @param {OwnerType | string | string[]} [owner=OwnerType.MINE] Single/multiple account ID(s) (CScript or address) or reserved words 'mine' to list history for all owned accounts or 'all' to list whole DB * @param {AccountHistoryOptions} [options] * @param {number} [options.maxBlockHeight] Optional height to iterate from (down to genesis block), (default = chaintip). * @param {number} [options.depth] Maximum depth, from the genesis block is the default @@ -310,7 +310,7 @@ export class Account { * @return {Promise} */ async listAccountHistory ( - owner: OwnerType | string = OwnerType.MINE, + owner: OwnerType | string | string[] = OwnerType.MINE, options: AccountHistoryOptions = { limit: 100 } diff --git a/packages/testcontainers/src/containers/DeFiDContainer.ts b/packages/testcontainers/src/containers/DeFiDContainer.ts index 37d45c76d5..ac20a23362 100644 --- a/packages/testcontainers/src/containers/DeFiDContainer.ts +++ b/packages/testcontainers/src/containers/DeFiDContainer.ts @@ -35,7 +35,7 @@ export abstract class DeFiDContainer extends DockerContainer { if (process?.env?.DEFICHAIN_DOCKER_IMAGE !== undefined) { return process.env.DEFICHAIN_DOCKER_IMAGE } - return 'defi/defichain:HEAD-54ad178ed' + return 'defi/defichain:HEAD-b7a621dc2' } public static readonly DefaultStartOptions = { From 1d72f28c75267c8c62012230f0eccc4bb002ba4f Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Thu, 22 Dec 2022 22:07:00 +0800 Subject: [PATCH 19/39] Use multi-address support --- .../module.api/consortium.controller.spec.ts | 112 ++++++++-------- .../src/module.api/consortium.controller.ts | 12 +- .../src/module.api/consortium.service.ts | 55 ++++---- .../__tests__/api/consortium.test.ts | 126 +++++++++--------- .../whale-api-client/src/api/consortium.ts | 26 ++-- 5 files changed, 162 insertions(+), 169 deletions(-) diff --git a/apps/whale-api/src/module.api/consortium.controller.spec.ts b/apps/whale-api/src/module.api/consortium.controller.spec.ts index cb96baeedf..e4b48b4c44 100644 --- a/apps/whale-api/src/module.api/consortium.controller.spec.ts +++ b/apps/whale-api/src/module.api/consortium.controller.spec.ts @@ -2,6 +2,7 @@ import { ConsortiumController } from './consortium.controller' import { TestingGroup } from '@defichain/jellyfish-testing' import { createTestingApp, stopTestingApp, waitForIndexedHeight } from '../e2e.module' import { NestFastifyApplication } from '@nestjs/platform-fastify' +import { StartFlags } from '@defichain/testcontainers' describe('getTransactionHistory', () => { const tGroup = TestingGroup.create(2) @@ -15,9 +16,10 @@ describe('getTransactionHistory', () => { let app: NestFastifyApplication let controller: ConsortiumController const txIdMatcher = expect.stringMatching(/[0-f]{64}/) + const startFlags: StartFlags[] = [{ name: 'regtest-minttoken-simulate-mainnet', value: 1 }] beforeAll(async () => { - await tGroup.start() + await tGroup.start({ startFlags }) await alice.container.waitForWalletCoinbaseMaturity() app = await createTestingApp(alice.container) @@ -34,20 +36,23 @@ describe('getTransactionHistory', () => { const hash = await alice.rpc.masternode.setGov({ ATTRIBUTES }) expect(hash).toBeTruthy() await alice.generate(1) + await tGroup.waitForSync() } - async function setMemberInfo (tokenId: string, memberInfo: Array<{ id: string, name: string, backingId: string, ownerAddress: string, mintLimit: string, dailyMintLimit: string }>): Promise { - const infoObjs = memberInfo.map(mi => ` - "${mi.id}":{ - "name":"${mi.name}", - "ownerAddress":"${mi.ownerAddress}", - "backingId":"${mi.backingId}", - "dailyMintLimit":${mi.dailyMintLimit}, - "mintLimit":${mi.mintLimit} - }` - ) - - return await setGovAttr({ [`v0/consortium/${tokenId}/members`]: `{${infoObjs.join(',')}}` }) + async function setMemberInfo (tokenId: string, memberInfo: Array<{ id: string, name: string, backingId: string, ownerAddress: string, mintLimit: string, mintLimitDaily: string }>): Promise { + const infoObjs: { [key: string]: object } = {} + + memberInfo.forEach(mi => { + infoObjs[mi.id] = { + name: mi.name, + ownerAddress: mi.ownerAddress, + backingId: mi.backingId, + mintLimitDaily: mi.mintLimitDaily, + mintLimit: mi.mintLimit + } + }) + + return await setGovAttr({ [`v0/consortium/${tokenId}/members`]: infoObjs }) } async function setup (): Promise { @@ -92,15 +97,15 @@ describe('getTransactionHistory', () => { name: 'alice', ownerAddress: accountAlice, backingId: 'abc', - dailyMintLimit: '5.00000000', - mintLimit: '10.00000000' + mintLimitDaily: '5', + mintLimit: '10' }, { id: '02', name: 'bob', ownerAddress: accountBob, backingId: 'def,hij', - dailyMintLimit: '5.00000000', - mintLimit: '10.00000000' + mintLimitDaily: '5', + mintLimit: '10' }]) await setMemberInfo(idETH, [{ @@ -108,34 +113,37 @@ describe('getTransactionHistory', () => { name: 'alice', ownerAddress: accountAlice, backingId: '', - dailyMintLimit: '10.00000000', + mintLimitDaily: '10.00000000', mintLimit: '20.00000000' }, { id: '02', name: 'bob', ownerAddress: accountBob, backingId: 'lmn,opq', - dailyMintLimit: '10.00000000', + mintLimitDaily: '10.00000000', mintLimit: '20.00000000' }]) await alice.rpc.token.mintTokens(`1@${symbolBTC}`) - await alice.generate(5) + await alice.generate(1) await alice.rpc.token.mintTokens(`2@${symbolETH}`) - await alice.generate(5) + await alice.generate(1) await alice.rpc.token.burnTokens(`1@${symbolETH}`, accountAlice) - await alice.generate(5) + await alice.generate(1) + await tGroup.waitForSync() await bob.rpc.token.mintTokens(`4@${symbolBTC}`) - await bob.generate(5) + await bob.generate(1) await bob.rpc.token.burnTokens(`2@${symbolBTC}`, accountBob) - await bob.generate(5) + await bob.generate(1) + await tGroup.waitForSync() const height = await alice.container.getBlockCount() await alice.generate(1) + await tGroup.waitForSync() await waitForIndexedHeight(app, height) } @@ -149,17 +157,17 @@ describe('getTransactionHistory', () => { await expect(controller.getTransactionHistory({ search: 'a'.repeat(65), limit: 1 })).rejects.toThrow('InvalidSearchTerm') }) - it('should throw an error if the max block height is invalid', async () => { - await expect(controller.getTransactionHistory({ maxBlockHeight: -2, limit: 1 })).rejects.toThrow('InvalidMaxBlockHeight') + it('should throw an error if the start index is invalid', async () => { + await expect(controller.getTransactionHistory({ start: -2, limit: 1 })).rejects.toThrow('InvalidStart') }) it('should filter transactions with search term (member name)', async () => { - const info = await controller.getTransactionHistory({ search: 'alice', limit: 20 }) + const info = await controller.getTransactionHistory({ search: 'alice' }) expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }, + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 110 }, { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 } ]) expect(info.total).toStrictEqual(3) @@ -170,8 +178,8 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }, + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 110 }, { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 } ]) expect(info.total).toStrictEqual(3) @@ -184,7 +192,7 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(1) expect(info.transactions).toStrictEqual([ - { txId: tx.txid, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 } + { txId: tx.txid, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 } ]) expect(info.total).toStrictEqual(1) }) @@ -194,9 +202,9 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 129 }, - { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }, - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 } + { txId: txIdMatcher, type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 113 }, + { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 112 }, + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 } ]) expect(info.total).toStrictEqual(5) }) @@ -206,26 +214,14 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(2) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 } + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 110 } ]) expect(info.total).toStrictEqual(3) }) - it('should get transactions upto a specific block height with a limit', async () => { - const info = await controller.getTransactionHistory({ limit: 3, maxBlockHeight: 124 }) - - expect(info.transactions.length).toStrictEqual(3) - expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }, - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 } - ]) - expect(info.total).toStrictEqual(5) - }) - - it('should return empty list of transactions for invalid search term', async () => { - const info = await controller.getTransactionHistory({ search: 'invalid-term', limit: 20 }) + it('should return empty list of transactions for not-found search term', async () => { + const info = await controller.getTransactionHistory({ search: 'not-found-term', limit: 20 }) expect(info.transactions.length).toStrictEqual(0) expect(info.total).toStrictEqual(0) @@ -241,7 +237,7 @@ describe('getTransactionHistory', () => { }) it('should paginate properly', async () => { - const page1 = await controller.getTransactionHistory({ limit: 2 }) + const page1 = await controller.getTransactionHistory({ start: 0, limit: 2 }) expect(page1).toStrictEqual({ transactions: [ @@ -251,7 +247,7 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], txId: txIdMatcher, address: accountBob, - block: 129 + block: 113 }, { type: 'Mint', @@ -259,13 +255,13 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], txId: txIdMatcher, address: accountBob, - block: 124 + block: 112 } ], total: 5 }) - const page2 = await controller.getTransactionHistory({ limit: 2, maxBlockHeight: page1.transactions[page1.transactions.length - 1].block - 1 }) + const page2 = await controller.getTransactionHistory({ start: 1, limit: 2 }) expect(page2).toStrictEqual({ transactions: [ @@ -275,7 +271,7 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], txId: txIdMatcher, address: accountAlice, - block: 119 + block: 111 }, { type: 'Mint', @@ -283,13 +279,13 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], txId: txIdMatcher, address: accountAlice, - block: 114 + block: 110 } ], total: 5 }) - const page3 = await controller.getTransactionHistory({ limit: 2, maxBlockHeight: page2.transactions[page2.transactions.length - 1].block - 1 }) + const page3 = await controller.getTransactionHistory({ start: 2, limit: 2 }) expect(page3).toStrictEqual({ transactions: [ diff --git a/apps/whale-api/src/module.api/consortium.controller.ts b/apps/whale-api/src/module.api/consortium.controller.ts index b51d6a90a0..a946e3bda3 100644 --- a/apps/whale-api/src/module.api/consortium.controller.ts +++ b/apps/whale-api/src/module.api/consortium.controller.ts @@ -17,9 +17,9 @@ export class ConsortiumController { */ @Get('/transactions') async getTransactionHistory ( - @Query() query: { limit: number, search?: string, maxBlockHeight?: number } + @Query() query: { start?: number, limit?: number, search?: string } ): Promise { - const { limit = 20, search = undefined, maxBlockHeight = -1 } = query + const { start = 0, limit = 20, search = undefined } = query if (limit > 50 || limit < 1) { throw new ForbiddenException('InvalidLimit') @@ -29,12 +29,12 @@ export class ConsortiumController { throw new ForbiddenException('InvalidSearchTerm') } - if (maxBlockHeight < -1) { - throw new ForbiddenException('InvalidMaxBlockHeight') + if (start < 0) { + throw new ForbiddenException('InvalidStart') } - return await this.cache.get(`CONSORTIUM_TRANSACTIONS_${JSON.stringify(query)}`, async () => { - return await this.consortiumService.getTransactionHistory(+limit, +maxBlockHeight, typeof search === 'string' ? search : '') + return await this.cache.get(`CONSORTIUM_TRANSACTIONS_${JSON.stringify({ start, limit, search })}`, async () => { + return await this.consortiumService.getTransactionHistory(+start, +limit, typeof search === 'string' ? search : '') }, { ttl: 600 // 10 mins }) as ConsortiumTransactionResponse diff --git a/apps/whale-api/src/module.api/consortium.service.ts b/apps/whale-api/src/module.api/consortium.service.ts index afa2474345..d3c84a660b 100644 --- a/apps/whale-api/src/module.api/consortium.service.ts +++ b/apps/whale-api/src/module.api/consortium.service.ts @@ -38,26 +38,28 @@ export class ConsortiumService { } } - async getTransactionHistory (limit: number, maxBlockHeight: number, search: string): Promise { + async getTransactionHistory (start: number, limit: number, search: string): Promise { const attrs = (await this.rpcClient.masternode.getGov('ATTRIBUTES')).ATTRIBUTES const members: ConsortiumMember[] = [] const searching: boolean = search !== '' const keys: string[] = Object.keys(attrs) - const values: string[] = Object.values(attrs) + const values: object[] = Object.values(attrs) const membersKeyRegex: RegExp = /^v0\/consortium\/\d+\/members$/ let totalTxCount: number = 0 let searchFound: boolean = false keys.forEach((key: string, i: number) => { if (membersKeyRegex.exec(key) !== null) { - const membersPerToken: object = JSON.parse(values[i]) + const membersPerToken = values[i] const memberIds: string[] = Object.keys(membersPerToken) let memberDetails: Array<{ ownerAddress: string, name: string }> = Object.values(membersPerToken) - const foundMembers: Array<{ ownerAddress: string, name: string }> = memberDetails.filter(m => m.ownerAddress === search || m.name.toLowerCase().includes(search)) - if (foundMembers.length > 0) { - memberDetails = foundMembers - searchFound = true + if (searching) { + const foundMembers = memberDetails.filter(m => m.ownerAddress === search || m.name.toLowerCase().includes(search)) + if (foundMembers.length > 0) { + memberDetails = foundMembers + searchFound = true + } } for (let j = 0; j < memberDetails.length; j++) { @@ -98,36 +100,25 @@ export class ConsortiumService { } } - let txs: AccountHistory[] = [] + const transactions: AccountHistory[] = await this.rpcClient.account.listAccountHistory(members.map(m => m.address), { + txtypes: [DfTxType.MINT_TOKEN, DfTxType.BURN_TOKEN], + including_start: true, + start: start * limit, + limit + }) + const promises = [] for (let i = 0; i < members.length; i++) { - const member = members[i] - - const mintTxsPromise = this.rpcClient.account.listAccountHistory(member.address, { - txtype: DfTxType.MINT_TOKEN, - maxBlockHeight: maxBlockHeight, - limit - }) - - const burnTxsPromise = this.rpcClient.account.listAccountHistory(member.address, { - txtype: DfTxType.BURN_TOKEN, - maxBlockHeight: maxBlockHeight, - limit - }) - - const burnTxCountPromise = this.rpcClient.account.historyCount(member.address, { txtype: DfTxType.BURN_TOKEN }) - const mintTxCountPromise = this.rpcClient.account.historyCount(member.address, { txtype: DfTxType.MINT_TOKEN }) - - const [mintTxs, burnTxs, burnTxCount, mintTxCount] = await Promise.all([mintTxsPromise, burnTxsPromise, burnTxCountPromise, mintTxCountPromise]) - - totalTxCount += burnTxCount + mintTxCount - txs.push(...mintTxs, ...burnTxs) + promises.push(this.rpcClient.account.historyCount(members[i].address, { txtype: DfTxType.BURN_TOKEN })) + promises.push(this.rpcClient.account.historyCount(members[i].address, { txtype: DfTxType.MINT_TOKEN })) } - - txs = txs.sort((a: any, b: any) => b.blockTime - a.blockTime).slice(0, limit) + const counts = await Promise.all(promises) + totalTxCount = counts.reduce((prev, curr) => { + return prev + curr + }, 0) return { - transactions: txs.map(tx => { + transactions: transactions.map(tx => { return this.formatTransactionResponse(tx, members) }), total: totalTxCount diff --git a/packages/whale-api-client/__tests__/api/consortium.test.ts b/packages/whale-api-client/__tests__/api/consortium.test.ts index e9f5fe6e6a..40e083b030 100644 --- a/packages/whale-api-client/__tests__/api/consortium.test.ts +++ b/packages/whale-api-client/__tests__/api/consortium.test.ts @@ -204,9 +204,10 @@ describe('getTransactionHistory', () => { const service = new StubService(alice.container) const client = new StubWhaleApiClient(service) const txIdMatcher = expect.stringMatching(/[0-f]{64}/) + const startFlags: StartFlags[] = [{ name: 'regtest-minttoken-simulate-mainnet', value: 1 }] beforeAll(async () => { - await tGroup.start() + await tGroup.start({ startFlags }) await service.start() await alice.container.waitForWalletCoinbaseMaturity() @@ -227,19 +228,23 @@ describe('getTransactionHistory', () => { const hash = await alice.rpc.masternode.setGov({ ATTRIBUTES }) expect(hash).toBeTruthy() await alice.generate(1) + await tGroup.waitForSync() } - async function setMemberInfo (tokenId: string, memberInfo: Array<{ id: string, name: string, backingId: string, ownerAddress: string, mintLimit: string, dailyMintLimit: string }>): Promise { - const infoObjs = memberInfo.map(mi => ` - "${mi.id}":{ - "name":"${mi.name}", - "ownerAddress":"${mi.ownerAddress}", - "backingId":"${mi.backingId}", - "dailyMintLimit":${mi.dailyMintLimit}, - "mintLimit":${mi.mintLimit} - }`) - - return await setGovAttr({ [`v0/consortium/${tokenId}/members`]: `{${infoObjs.join(',')}}` }) + async function setMemberInfo (tokenId: string, memberInfo: Array<{ id: string, name: string, backingId: string, ownerAddress: string, mintLimit: string, mintLimitDaily: string }>): Promise { + const infoObjs: { [key: string]: object } = {} + + memberInfo.forEach(mi => { + infoObjs[mi.id] = { + name: mi.name, + ownerAddress: mi.ownerAddress, + backingId: mi.backingId, + mintLimitDaily: mi.mintLimitDaily, + mintLimit: mi.mintLimit + } + }) + + return await setGovAttr({ [`v0/consortium/${tokenId}/members`]: infoObjs }) } async function setup (): Promise { @@ -284,15 +289,15 @@ describe('getTransactionHistory', () => { name: 'alice', ownerAddress: accountAlice, backingId: 'abc', - dailyMintLimit: '5.00000000', - mintLimit: '10.00000000' + mintLimitDaily: '5', + mintLimit: '10' }, { id: '02', name: 'bob', ownerAddress: accountBob, backingId: 'def,hij', - dailyMintLimit: '5.00000000', - mintLimit: '10.00000000' + mintLimitDaily: '5', + mintLimit: '10' }]) await setMemberInfo(idETH, [{ @@ -300,70 +305,73 @@ describe('getTransactionHistory', () => { name: 'alice', ownerAddress: accountAlice, backingId: '', - dailyMintLimit: '10.00000000', + mintLimitDaily: '10.00000000', mintLimit: '20.00000000' }, { id: '02', name: 'bob', ownerAddress: accountBob, backingId: 'lmn,opq', - dailyMintLimit: '10.00000000', + mintLimitDaily: '10.00000000', mintLimit: '20.00000000' }]) await alice.rpc.token.mintTokens(`1@${symbolBTC}`) - await alice.generate(5) + await alice.generate(1) await alice.rpc.token.mintTokens(`2@${symbolETH}`) - await alice.generate(5) + await alice.generate(1) await alice.rpc.token.burnTokens(`1@${symbolETH}`, accountAlice) - await alice.generate(5) + await alice.generate(1) + await tGroup.waitForSync() await bob.rpc.token.mintTokens(`4@${symbolBTC}`) - await bob.generate(5) + await bob.generate(1) await bob.rpc.token.burnTokens(`2@${symbolBTC}`, accountBob) - await bob.generate(5) + await bob.generate(1) + await tGroup.waitForSync() const height = await alice.container.getBlockCount() await alice.generate(1) + await tGroup.waitForSync() await waitForIndexedHeight(app, height) } it('should throw an error if the limit is invalid', async () => { - await expect(client.consortium.getTransactionHistory(51)).rejects.toThrow('InvalidLimit') - await expect(client.consortium.getTransactionHistory(0)).rejects.toThrow('InvalidLimit') + await expect(client.consortium.getTransactionHistory(0, 51)).rejects.toThrow('InvalidLimit') + await expect(client.consortium.getTransactionHistory(0, 0)).rejects.toThrow('InvalidLimit') }) it('should throw an error if the search term is invalid', async () => { - await expect(client.consortium.getTransactionHistory(1, 'a')).rejects.toThrow('InvalidSearchTerm') - await expect(client.consortium.getTransactionHistory(1, 'a'.repeat(65))).rejects.toThrow('InvalidSearchTerm') + await expect(client.consortium.getTransactionHistory(1, 10, 'a')).rejects.toThrow('InvalidSearchTerm') + await expect(client.consortium.getTransactionHistory(1, 10, 'a'.repeat(65))).rejects.toThrow('InvalidSearchTerm') }) it('should throw an error if the max block height is invalid', async () => { - await expect(client.consortium.getTransactionHistory(1, undefined, -2)).rejects.toThrow('InvalidMaxBlockHeight') + await expect(client.consortium.getTransactionHistory(-1, 10)).rejects.toThrow('InvalidStart') }) it('should filter transactions with search term (member name)', async () => { - const info = await client.consortium.getTransactionHistory(10, 'alice') + const info = await client.consortium.getTransactionHistory(0, 10, 'alice') expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }, + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 110 }, { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 } ]) expect(info.total).toStrictEqual(3) }) it('should filter transactions with search term (owner address)', async () => { - const info = await client.consortium.getTransactionHistory(20, accountAlice) + const info = await client.consortium.getTransactionHistory(0, 10, accountAlice) expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 }, + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 110 }, { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 } ]) expect(info.total).toStrictEqual(3) @@ -372,52 +380,40 @@ describe('getTransactionHistory', () => { it('should filter transactions with search term (transaction id)', async () => { const tx = (await alice.rpc.account.listAccountHistory(accountAlice))[0] - const info = await client.consortium.getTransactionHistory(20, tx.txid) + const info = await client.consortium.getTransactionHistory(0, 10, tx.txid) expect(info.transactions.length).toStrictEqual(1) expect(info.transactions).toStrictEqual([ - { txId: tx.txid, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 } + { txId: tx.txid, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 } ]) expect(info.total).toStrictEqual(1) }) it('should limit transactions', async () => { - const info = await client.consortium.getTransactionHistory(3) + const info = await client.consortium.getTransactionHistory(0, 3) expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 129 }, - { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }, - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 } + { txId: txIdMatcher, type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 113 }, + { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 112 }, + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 } ]) expect(info.total).toStrictEqual(5) }) it('should filter and limit transactions at the same time', async () => { - const info = await client.consortium.getTransactionHistory(2, accountAlice) + const info = await client.consortium.getTransactionHistory(0, 2, accountAlice) expect(info.transactions.length).toStrictEqual(2) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 } + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 110 } ]) expect(info.total).toStrictEqual(3) }) - it('should get transactions upto a specific block height with a limit', async () => { - const info = await client.consortium.getTransactionHistory(3, undefined, 124) - - expect(info.transactions.length).toStrictEqual(3) - expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 124 }, - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 119 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 114 } - ]) - expect(info.total).toStrictEqual(5) - }) - - it('should return empty list of transactions for invalid search term', async () => { - const info = await client.consortium.getTransactionHistory(20, 'invalid-term') + it('should return empty list of transactions for not-found search term', async () => { + const info = await client.consortium.getTransactionHistory(0, 10, 'not-found-term') expect(info.transactions.length).toStrictEqual(0) expect(info.total).toStrictEqual(0) @@ -426,14 +422,14 @@ describe('getTransactionHistory', () => { it('should not return other transactions from consortium members apart from mints or burns', async () => { const { txid } = await alice.container.fundAddress(accountBob, 10) - const info = await client.consortium.getTransactionHistory(20, txid) + const info = await client.consortium.getTransactionHistory(0, 20, txid) expect(info.transactions.length).toStrictEqual(0) expect(info.total).toStrictEqual(0) }) it('should paginate properly', async () => { - const page1 = await client.consortium.getTransactionHistory(2, undefined, -1) + const page1 = await client.consortium.getTransactionHistory(0, 2) expect(page1).toStrictEqual({ transactions: [ @@ -443,7 +439,7 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], txId: txIdMatcher, address: accountBob, - block: 129 + block: 113 }, { type: 'Mint', @@ -451,13 +447,13 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], txId: txIdMatcher, address: accountBob, - block: 124 + block: 112 } ], total: 5 }) - const page2 = await client.consortium.getTransactionHistory(2, undefined, page1.transactions[page1.transactions.length - 1].block - 1) + const page2 = await client.consortium.getTransactionHistory(1, 2) expect(page2).toStrictEqual({ transactions: [ @@ -467,7 +463,7 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], txId: txIdMatcher, address: accountAlice, - block: 119 + block: 111 }, { type: 'Mint', @@ -475,13 +471,13 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], txId: txIdMatcher, address: accountAlice, - block: 114 + block: 110 } ], total: 5 }) - const page3 = await client.consortium.getTransactionHistory(2, undefined, page2.transactions[page2.transactions.length - 1].block - 1) + const page3 = await client.consortium.getTransactionHistory(2, 2) expect(page3).toStrictEqual({ transactions: [ diff --git a/packages/whale-api-client/src/api/consortium.ts b/packages/whale-api-client/src/api/consortium.ts index 5d968ad21e..8c0f174002 100644 --- a/packages/whale-api-client/src/api/consortium.ts +++ b/packages/whale-api-client/src/api/consortium.ts @@ -7,16 +7,30 @@ export class Consortium { constructor (private readonly client: WhaleApiClient) { } + /** + * Gets the asset breakdown information of consortium members. + * + * @return {Promise} + */ + async getAssetBreakdown (): Promise { + return await this.client.requestData('GET', 'consortium/assetbreakdown') + } + /** * Gets the transaction history of consortium members. - * @param {number} limit how many transactions to fetch - * @param {string} [search] search term, can be a transaction id, member/owner address or member name - * @param {number} [maxBlockHeight] the maximum block height to look for, -1 for current tip by default + * + * @param {number} start The starting index for pagination + * @param {number} [limit] How many transactions to fetch + * @param {string} [search] Search term, can be a transaction id, member/owner address or member name * @return {Promise} */ - async getTransactionHistory (limit: number, search?: string, maxBlockHeight?: number): Promise { + async getTransactionHistory (start: number, limit: number, search?: string): Promise { const query = [] + if (start !== undefined) { + query.push(`start=${start}`) + } + if (limit !== undefined) { query.push(`limit=${limit}`) } @@ -25,10 +39,6 @@ export class Consortium { query.push(`search=${search}`) } - if (maxBlockHeight !== undefined) { - query.push(`maxBlockHeight=${maxBlockHeight}`) - } - return await this.client.requestData('GET', `consortium/transactions?${query.join('&')}`) } } From 52064f3dd9840af5776cfc8a3eb415d15734453a Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Thu, 22 Dec 2022 23:12:26 +0800 Subject: [PATCH 20/39] Revert missing tests while fixing MC --- .../module.api/consortium.controller.spec.ts | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/apps/whale-api/src/module.api/consortium.controller.spec.ts b/apps/whale-api/src/module.api/consortium.controller.spec.ts index e4b48b4c44..85c7ba61c9 100644 --- a/apps/whale-api/src/module.api/consortium.controller.spec.ts +++ b/apps/whale-api/src/module.api/consortium.controller.spec.ts @@ -4,6 +4,189 @@ import { createTestingApp, stopTestingApp, waitForIndexedHeight } from '../e2e.m import { NestFastifyApplication } from '@nestjs/platform-fastify' import { StartFlags } from '@defichain/testcontainers' +describe('getAssetBreakdown', () => { + const tGroup = TestingGroup.create(2) + const alice = tGroup.get(0) + const bob = tGroup.get(1) + const symbolBTC = 'BTC' + const symbolETH = 'ETH' + let accountAlice: string, accountBob: string + let idBTC: string + let idETH: string + let app: NestFastifyApplication + let controller: ConsortiumController + const startFlags: StartFlags[] = [{ name: 'regtest-minttoken-simulate-mainnet', value: 1 }] + + beforeEach(async () => { + await tGroup.start({ startFlags }) + await alice.container.waitForWalletCoinbaseMaturity() + + app = await createTestingApp(alice.container) + controller = app.get(ConsortiumController) + }) + + afterEach(async () => { + await stopTestingApp(tGroup, app) + }) + + async function setGovAttr (attributes: object): Promise { + const hash = await alice.rpc.masternode.setGov({ ATTRIBUTES: attributes }) + expect(hash).toBeTruthy() + await alice.generate(1) + await tGroup.waitForSync() + } + + async function setMemberInfo (tokenId: string, memberInfo: Array<{ id: string, name: string, backingId: string, ownerAddress: string, mintLimit: string, dailyMintLimit: string }>): Promise { + const members: any = {} + + memberInfo.forEach(mi => { + members[mi.id] = { + name: mi.name, + ownerAddress: mi.ownerAddress, + backingId: mi.backingId, + mintLimitDaily: mi.dailyMintLimit, + mintLimit: mi.mintLimit + } + }) + + return await setGovAttr({ [`v0/consortium/${tokenId}/members`]: members }) + } + + async function setup (): Promise { + accountAlice = await alice.generateAddress() + accountBob = await bob.generateAddress() + + await alice.token.create({ + symbol: symbolBTC, + name: symbolBTC, + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: accountAlice + }) + await alice.generate(1) + + await alice.token.create({ + symbol: symbolETH, + name: symbolETH, + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: accountAlice + }) + await alice.generate(1) + + await alice.container.fundAddress(accountBob, 10) + await alice.generate(1) + idBTC = await alice.token.getTokenId(symbolBTC) + idETH = await alice.token.getTokenId(symbolETH) + + await setGovAttr({ + 'v0/params/feature/consortium': 'true', + [`v0/consortium/${idBTC}/mint_limit`]: '10', + [`v0/consortium/${idBTC}/mint_limit_daily`]: '5', + [`v0/consortium/${idETH}/mint_limit`]: '20', + [`v0/consortium/${idETH}/mint_limit_daily`]: '10' + }) + + await setMemberInfo(idBTC, [{ + id: '01', + name: 'alice', + ownerAddress: accountAlice, + backingId: 'abc', + dailyMintLimit: '5.00000000', + mintLimit: '10.00000000' + }, { + id: '02', + name: 'bob', + ownerAddress: accountBob, + backingId: 'def,hij', + dailyMintLimit: '5.00000000', + mintLimit: '10.00000000' + }]) + + await setMemberInfo(idETH, [{ + id: '01', + name: 'alice', + ownerAddress: accountAlice, + backingId: '', + dailyMintLimit: '10.00000000', + mintLimit: '20.00000000' + }, { + id: '02', + name: 'bob', + ownerAddress: accountBob, + backingId: ' lmn , opq', + dailyMintLimit: '10.00000000', + mintLimit: '20.00000000' + }]) + } + + it('should return an empty list if theres no consortium members or tokens initialized', async () => { + const info = await controller.getAssetBreakdown() + expect(info).toStrictEqual([]) + }) + + it('should return a list with amounts set to 0 if the mint/burn transactions does not exists', async () => { + await setup() + + const info = await controller.getAssetBreakdown() + expect(info).toStrictEqual([{ + tokenSymbol: symbolBTC, + tokenDisplaySymbol: `d${symbolBTC}`, + memberInfo: [ + { id: '01', name: 'alice', minted: '0.00000000', burned: '0.00000000', backingAddresses: ['abc'], tokenId: idBTC }, + { id: '02', name: 'bob', minted: '0.00000000', burned: '0.00000000', backingAddresses: ['def', 'hij'], tokenId: idBTC } + ] + }, { + tokenSymbol: symbolETH, + tokenDisplaySymbol: `d${symbolETH}`, + memberInfo: [ + { id: '01', name: 'alice', minted: '0.00000000', burned: '0.00000000', backingAddresses: [], tokenId: idETH }, + { id: '02', name: 'bob', minted: '0.00000000', burned: '0.00000000', backingAddresses: ['lmn', 'opq'], tokenId: idETH } + ] + }]) + }) + + it('should return a list with valid mint/burn information', async () => { + await setup() + + await alice.rpc.token.mintTokens(`1@${symbolBTC}`) + await alice.generate(1) + + await alice.rpc.token.mintTokens(`2@${symbolETH}`) + await alice.generate(1) + + await alice.rpc.token.burnTokens(`1@${symbolETH}`, accountAlice) + await alice.generate(1) + + await bob.rpc.token.mintTokens(`4@${symbolBTC}`) + await bob.generate(1) + + await bob.rpc.token.burnTokens(`2@${symbolBTC}`, accountBob) + await bob.generate(1) + + await tGroup.waitForSync() + + const info = await controller.getAssetBreakdown() + expect(info).toStrictEqual([{ + tokenSymbol: symbolBTC, + tokenDisplaySymbol: `d${symbolBTC}`, + memberInfo: [ + { id: '01', name: 'alice', minted: '1.00000000', burned: '0.00000000', backingAddresses: ['abc'], tokenId: idBTC }, + { id: '02', name: 'bob', minted: '4.00000000', burned: '2.00000000', backingAddresses: ['def', 'hij'], tokenId: idBTC } + ] + }, { + tokenSymbol: symbolETH, + tokenDisplaySymbol: `d${symbolETH}`, + memberInfo: [ + { id: '01', name: 'alice', minted: '2.00000000', burned: '1.00000000', backingAddresses: [], tokenId: idETH }, + { id: '02', name: 'bob', minted: '0.00000000', burned: '0.00000000', backingAddresses: ['lmn', 'opq'], tokenId: idETH } + ] + }]) + }) +}) + describe('getTransactionHistory', () => { const tGroup = TestingGroup.create(2) const alice = tGroup.get(0) From 6b12ea04d14f7caeed1b5d8cde6bef48d7f43a56 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Fri, 23 Dec 2022 13:08:50 +0800 Subject: [PATCH 21/39] Refactor: start -> pageIndex --- .../src/module.api/consortium.controller.spec.ts | 10 +++++----- .../src/module.api/consortium.controller.ts | 12 ++++++------ apps/whale-api/src/module.api/consortium.service.ts | 4 ++-- .../__tests__/api/consortium.test.ts | 4 ++-- packages/whale-api-client/src/api/consortium.ts | 6 +++--- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/apps/whale-api/src/module.api/consortium.controller.spec.ts b/apps/whale-api/src/module.api/consortium.controller.spec.ts index 85c7ba61c9..35351e2db8 100644 --- a/apps/whale-api/src/module.api/consortium.controller.spec.ts +++ b/apps/whale-api/src/module.api/consortium.controller.spec.ts @@ -340,8 +340,8 @@ describe('getTransactionHistory', () => { await expect(controller.getTransactionHistory({ search: 'a'.repeat(65), limit: 1 })).rejects.toThrow('InvalidSearchTerm') }) - it('should throw an error if the start index is invalid', async () => { - await expect(controller.getTransactionHistory({ start: -2, limit: 1 })).rejects.toThrow('InvalidStart') + it('should throw an error if the pageIndex is invalid', async () => { + await expect(controller.getTransactionHistory({ pageIndex: -1, limit: 1 })).rejects.toThrow('InvalidPageIndex') }) it('should filter transactions with search term (member name)', async () => { @@ -420,7 +420,7 @@ describe('getTransactionHistory', () => { }) it('should paginate properly', async () => { - const page1 = await controller.getTransactionHistory({ start: 0, limit: 2 }) + const page1 = await controller.getTransactionHistory({ pageIndex: 0, limit: 2 }) expect(page1).toStrictEqual({ transactions: [ @@ -444,7 +444,7 @@ describe('getTransactionHistory', () => { total: 5 }) - const page2 = await controller.getTransactionHistory({ start: 1, limit: 2 }) + const page2 = await controller.getTransactionHistory({ pageIndex: 1, limit: 2 }) expect(page2).toStrictEqual({ transactions: [ @@ -468,7 +468,7 @@ describe('getTransactionHistory', () => { total: 5 }) - const page3 = await controller.getTransactionHistory({ start: 2, limit: 2 }) + const page3 = await controller.getTransactionHistory({ pageIndex: 2, limit: 2 }) expect(page3).toStrictEqual({ transactions: [ diff --git a/apps/whale-api/src/module.api/consortium.controller.ts b/apps/whale-api/src/module.api/consortium.controller.ts index a946e3bda3..d10ad5ce99 100644 --- a/apps/whale-api/src/module.api/consortium.controller.ts +++ b/apps/whale-api/src/module.api/consortium.controller.ts @@ -17,9 +17,9 @@ export class ConsortiumController { */ @Get('/transactions') async getTransactionHistory ( - @Query() query: { start?: number, limit?: number, search?: string } + @Query() query: { pageIndex?: number, limit?: number, search?: string } ): Promise { - const { start = 0, limit = 20, search = undefined } = query + const { pageIndex = 0, limit = 20, search = undefined } = query if (limit > 50 || limit < 1) { throw new ForbiddenException('InvalidLimit') @@ -29,12 +29,12 @@ export class ConsortiumController { throw new ForbiddenException('InvalidSearchTerm') } - if (start < 0) { - throw new ForbiddenException('InvalidStart') + if (pageIndex < 0) { + throw new ForbiddenException('InvalidPageIndex') } - return await this.cache.get(`CONSORTIUM_TRANSACTIONS_${JSON.stringify({ start, limit, search })}`, async () => { - return await this.consortiumService.getTransactionHistory(+start, +limit, typeof search === 'string' ? search : '') + return await this.cache.get(`CONSORTIUM_TRANSACTIONS_${JSON.stringify({ pageIndex, limit, search })}`, async () => { + return await this.consortiumService.getTransactionHistory(+pageIndex, +limit, typeof search === 'string' ? search : '') }, { ttl: 600 // 10 mins }) as ConsortiumTransactionResponse diff --git a/apps/whale-api/src/module.api/consortium.service.ts b/apps/whale-api/src/module.api/consortium.service.ts index d3c84a660b..28bc9bed46 100644 --- a/apps/whale-api/src/module.api/consortium.service.ts +++ b/apps/whale-api/src/module.api/consortium.service.ts @@ -38,7 +38,7 @@ export class ConsortiumService { } } - async getTransactionHistory (start: number, limit: number, search: string): Promise { + async getTransactionHistory (pageIndex: number, limit: number, search: string): Promise { const attrs = (await this.rpcClient.masternode.getGov('ATTRIBUTES')).ATTRIBUTES const members: ConsortiumMember[] = [] const searching: boolean = search !== '' @@ -103,7 +103,7 @@ export class ConsortiumService { const transactions: AccountHistory[] = await this.rpcClient.account.listAccountHistory(members.map(m => m.address), { txtypes: [DfTxType.MINT_TOKEN, DfTxType.BURN_TOKEN], including_start: true, - start: start * limit, + start: pageIndex * limit, limit }) diff --git a/packages/whale-api-client/__tests__/api/consortium.test.ts b/packages/whale-api-client/__tests__/api/consortium.test.ts index 40e083b030..6f389f113e 100644 --- a/packages/whale-api-client/__tests__/api/consortium.test.ts +++ b/packages/whale-api-client/__tests__/api/consortium.test.ts @@ -349,8 +349,8 @@ describe('getTransactionHistory', () => { await expect(client.consortium.getTransactionHistory(1, 10, 'a'.repeat(65))).rejects.toThrow('InvalidSearchTerm') }) - it('should throw an error if the max block height is invalid', async () => { - await expect(client.consortium.getTransactionHistory(-1, 10)).rejects.toThrow('InvalidStart') + it('should throw an error if the pageIndex is invalid', async () => { + await expect(client.consortium.getTransactionHistory(-1, 10)).rejects.toThrow('InvalidPageIndex') }) it('should filter transactions with search term (member name)', async () => { diff --git a/packages/whale-api-client/src/api/consortium.ts b/packages/whale-api-client/src/api/consortium.ts index 8c0f174002..6fda00b392 100644 --- a/packages/whale-api-client/src/api/consortium.ts +++ b/packages/whale-api-client/src/api/consortium.ts @@ -24,11 +24,11 @@ export class Consortium { * @param {string} [search] Search term, can be a transaction id, member/owner address or member name * @return {Promise} */ - async getTransactionHistory (start: number, limit: number, search?: string): Promise { + async getTransactionHistory (pageIndex: number, limit: number, search?: string): Promise { const query = [] - if (start !== undefined) { - query.push(`start=${start}`) + if (pageIndex !== undefined) { + query.push(`pageIndex=${pageIndex}`) } if (limit !== undefined) { From 9feacb4e5e792a5f0128a0eb36313bbad5a6ccce Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Fri, 23 Dec 2022 15:52:22 +0800 Subject: [PATCH 22/39] Update to support multi-txtype multi-address filter --- apps/whale-api/docker-compose.yml | 2 +- .../account/accountHistoryCount.test.ts | 28 ++++++++++++++++--- .../src/category/account.ts | 6 ++-- .../src/containers/DeFiDContainer.ts | 2 +- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/apps/whale-api/docker-compose.yml b/apps/whale-api/docker-compose.yml index bb7dd884aa..4e6f5a659e 100644 --- a/apps/whale-api/docker-compose.yml +++ b/apps/whale-api/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' services: defi-blockchain: - image: defi/defichain:3.1.1 + image: defi/defichain:HEAD-22a537405 ports: - "19554:19554" command: > diff --git a/packages/jellyfish-api-core/__tests__/category/account/accountHistoryCount.test.ts b/packages/jellyfish-api-core/__tests__/category/account/accountHistoryCount.test.ts index 800914996a..1e36ebeede 100644 --- a/packages/jellyfish-api-core/__tests__/category/account/accountHistoryCount.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/account/accountHistoryCount.test.ts @@ -6,6 +6,7 @@ import { AccountHistoryCountOptions, DfTxType } from '../../../src/category/acco describe('Account', () => { const container = new MasterNodeRegTestContainer() const client = new ContainerAdapterClient(container) + let from: string, to: string beforeAll(async () => { await container.start() @@ -19,10 +20,10 @@ describe('Account', () => { }) async function setup (): Promise { - const from = await container.call('getnewaddress') + from = await container.call('getnewaddress') await createToken(from, 'DBTC', 200) - const to = await accountToAccount('DBTC', 5, from) + to = await accountToAccount('DBTC', 5, from) await accountToAccount('DBTC', 18, from, to) await createToken(from, 'DETH', 200) @@ -102,7 +103,7 @@ describe('Account', () => { }) }) - it('should get accountHistory with txtype option', async () => { + it('should get accountHistoryCount with txtype option', async () => { await waitForExpect(async () => { const options: AccountHistoryCountOptions = { txtype: DfTxType.MINT_TOKEN @@ -121,7 +122,6 @@ describe('Account', () => { } const options2: AccountHistoryCountOptions = { txtype: DfTxType.POOL_SWAP - } const count1 = await client.account.historyCount('mine', options1) const count2 = await client.account.historyCount('mine', options2) @@ -129,4 +129,24 @@ describe('Account', () => { expect(count1 === count2).toStrictEqual(false) }) }) + + it('should get accountHistoryCount for multiple txtypes at once', async () => { + const mintCount = await client.account.historyCount('mine', { txtype: DfTxType.MINT_TOKEN }) + const poolSwapCount = await client.account.historyCount('mine', { txtype: DfTxType.POOL_SWAP }) + + await waitForExpect(async () => { + const combinedCount = await client.account.historyCount('mine', { txtypes: [DfTxType.MINT_TOKEN, DfTxType.POOL_SWAP] }) + expect(combinedCount).toStrictEqual(mintCount + poolSwapCount) + }) + }) + + it('should get accountHistoryCount for multiple addresses at once', async () => { + const fromCount = await client.account.historyCount(from) + const toCount = await client.account.historyCount(to) + + await waitForExpect(async () => { + const combinedCount = await client.account.historyCount([from, to]) + expect(combinedCount).toStrictEqual(fromCount + toCount) + }) + }) }) diff --git a/packages/jellyfish-api-core/src/category/account.ts b/packages/jellyfish-api-core/src/category/account.ts index 76071ca132..76e626af5a 100644 --- a/packages/jellyfish-api-core/src/category/account.ts +++ b/packages/jellyfish-api-core/src/category/account.ts @@ -333,15 +333,16 @@ export class Account { /** * Returns count of account history * - * @param {OwnerType | string} [owner=OwnerType.MINE] single account ID (CScript or address) or reserved words 'mine' to list history count for all owned accounts or 'all' to list whole DB + * @param {OwnerType | string | string[]} [owner=OwnerType.MINE] Single/multiple account ID(s) (CScript or address) or reserved words 'mine' to list history count for all owned accounts or 'all' to list whole DB * @param {AccountHistoryCountOptions} [options] * @param {boolean} [options.no_rewards] Filter out rewards * @param {string} [options.token] Filter by token * @param {DfTxType} [options.txtype] Filter by transaction type. See DfTxType. + * @param {DfTxType[]} [options.txtypes] Filter by multiple transaction types. See DfTxType. * @return {Promise} count of account history */ async historyCount ( - owner: OwnerType | string = OwnerType.MINE, + owner: OwnerType | string | string [] = OwnerType.MINE, options: AccountHistoryCountOptions = {} ): Promise { return await this.client.call('accounthistorycount', [owner, options], 'number') @@ -547,6 +548,7 @@ export interface AccountHistoryOptions { export interface AccountHistoryCountOptions { token?: string txtype?: DfTxType + txtypes?: DfTxType[] no_rewards?: boolean } diff --git a/packages/testcontainers/src/containers/DeFiDContainer.ts b/packages/testcontainers/src/containers/DeFiDContainer.ts index f9aeb7e1a5..faa2eb7cb8 100644 --- a/packages/testcontainers/src/containers/DeFiDContainer.ts +++ b/packages/testcontainers/src/containers/DeFiDContainer.ts @@ -36,7 +36,7 @@ export abstract class DeFiDContainer extends DockerContainer { if (process?.env?.DEFICHAIN_DOCKER_IMAGE !== undefined) { return process.env.DEFICHAIN_DOCKER_IMAGE } - return 'defi/defichain:3.1.1' + return 'defi/defichain:HEAD-22a537405' } public static readonly DefaultStartOptions = { From 679270838aeaef109e13f5bceb4a8d58a7390600 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Tue, 3 Jan 2023 13:55:44 +0800 Subject: [PATCH 23/39] Bump image to 1929da31d --- apps/whale-api/docker-compose.yml | 2 +- packages/testcontainers/src/containers/DeFiDContainer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/whale-api/docker-compose.yml b/apps/whale-api/docker-compose.yml index 8b754d1bfe..fc10b9c0c4 100644 --- a/apps/whale-api/docker-compose.yml +++ b/apps/whale-api/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' services: defi-blockchain: - image: defi/defichain:HEAD-b7a621dc2 + image: defi/defichain:HEAD-1929da31d ports: - "19554:19554" command: > diff --git a/packages/testcontainers/src/containers/DeFiDContainer.ts b/packages/testcontainers/src/containers/DeFiDContainer.ts index 76fceb2f6b..b1f6fdd750 100644 --- a/packages/testcontainers/src/containers/DeFiDContainer.ts +++ b/packages/testcontainers/src/containers/DeFiDContainer.ts @@ -36,7 +36,7 @@ export abstract class DeFiDContainer extends DockerContainer { if (process?.env?.DEFICHAIN_DOCKER_IMAGE !== undefined) { return process.env.DEFICHAIN_DOCKER_IMAGE } - return 'defi/defichain:HEAD-b7a621dc2' + return 'defi/defichain:HEAD-1929da31d' } public static readonly DefaultStartOptions = { From 1d88862c2f377f3d438921eda9d4429c22792c83 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Tue, 3 Jan 2023 14:08:58 +0800 Subject: [PATCH 24/39] Bump image to fe4ccb39d --- apps/whale-api/docker-compose.yml | 2 +- packages/testcontainers/src/containers/DeFiDContainer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/whale-api/docker-compose.yml b/apps/whale-api/docker-compose.yml index 4e6f5a659e..336d10eec7 100644 --- a/apps/whale-api/docker-compose.yml +++ b/apps/whale-api/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.7' services: defi-blockchain: - image: defi/defichain:HEAD-22a537405 + image: defi/defichain:HEAD-fe4ccb39d ports: - "19554:19554" command: > diff --git a/packages/testcontainers/src/containers/DeFiDContainer.ts b/packages/testcontainers/src/containers/DeFiDContainer.ts index faa2eb7cb8..21857480c0 100644 --- a/packages/testcontainers/src/containers/DeFiDContainer.ts +++ b/packages/testcontainers/src/containers/DeFiDContainer.ts @@ -36,7 +36,7 @@ export abstract class DeFiDContainer extends DockerContainer { if (process?.env?.DEFICHAIN_DOCKER_IMAGE !== undefined) { return process.env.DEFICHAIN_DOCKER_IMAGE } - return 'defi/defichain:HEAD-22a537405' + return 'defi/defichain:HEAD-fe4ccb39d' } public static readonly DefaultStartOptions = { From c3acc51df0d31dfe5372f7073dd6aad8f0405aeb Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Tue, 3 Jan 2023 14:54:20 +0800 Subject: [PATCH 25/39] Update docs --- docs/node/CATEGORIES/08-account.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/node/CATEGORIES/08-account.md b/docs/node/CATEGORIES/08-account.md index e2c03c0d86..818ed69ff3 100644 --- a/docs/node/CATEGORIES/08-account.md +++ b/docs/node/CATEGORIES/08-account.md @@ -276,7 +276,7 @@ Returns count of account history ```ts title="client.account.historyCount()" interface account { historyCount ( - owner: OwnerType | string = OwnerType.MINE, + owner: OwnerType | string | string[] = OwnerType.MINE, options: AccountHistoryCountOptions = {} ): Promise } @@ -310,6 +310,7 @@ enum DfTxType { interface AccountHistoryCountOptions { token?: string txtype?: DfTxType + txtypes?: DfTxType[] no_rewards?: boolean } ``` @@ -588,4 +589,4 @@ interface DusdSwapsInfo { owner: string amount: BigNumber } -``` \ No newline at end of file +``` From e6b2319e91fc173965b9a9a04e1850e80faacb2d Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Tue, 3 Jan 2023 15:02:22 +0800 Subject: [PATCH 26/39] Update docs --- docs/node/CATEGORIES/08-account.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/node/CATEGORIES/08-account.md b/docs/node/CATEGORIES/08-account.md index e2c03c0d86..8818af1ec0 100644 --- a/docs/node/CATEGORIES/08-account.md +++ b/docs/node/CATEGORIES/08-account.md @@ -183,7 +183,7 @@ Returns information about account history ```ts title="client.account.listAccountHistory()" interface account { listAccountHistory ( - owner: OwnerType | string = OwnerType.MINE, + owner: OwnerType | string | string[] = OwnerType.MINE, options: AccountHistoryOptions = { limit: 100 } @@ -238,7 +238,10 @@ interface AccountHistoryOptions { no_rewards?: boolean token?: string txtype?: DfTxType + txtypes?: DfTxType[] limit?: number + start?: number + including_start?: boolean txn?: number format?: Format } From 8eb155b69f12b810d13a60a3ce53bbec7e804636 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Tue, 3 Jan 2023 15:04:42 +0800 Subject: [PATCH 27/39] Remove extra line --- docs/node/CATEGORIES/08-account.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/node/CATEGORIES/08-account.md b/docs/node/CATEGORIES/08-account.md index 818ed69ff3..540f5fb2ff 100644 --- a/docs/node/CATEGORIES/08-account.md +++ b/docs/node/CATEGORIES/08-account.md @@ -589,4 +589,4 @@ interface DusdSwapsInfo { owner: string amount: BigNumber } -``` +``` \ No newline at end of file From 7433ea47126009577c54b3dd0cba90891c453668 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Tue, 3 Jan 2023 15:12:20 +0800 Subject: [PATCH 28/39] Update docs --- docs/node/CATEGORIES/08-account.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/node/CATEGORIES/08-account.md b/docs/node/CATEGORIES/08-account.md index 8818af1ec0..e5871936b2 100644 --- a/docs/node/CATEGORIES/08-account.md +++ b/docs/node/CATEGORIES/08-account.md @@ -197,6 +197,7 @@ enum OwnerType { enum DfTxType { MINT_TOKEN = 'M', + BURN_TOKEN = 'F', POOL_SWAP = 's', ADD_POOL_LIQUIDITY = 'l', REMOVE_POOL_LIQUIDITY = 'r', @@ -291,6 +292,7 @@ enum OwnerType { enum DfTxType { MINT_TOKEN = 'M', + BURN_TOKEN = 'F', POOL_SWAP = 's', ADD_POOL_LIQUIDITY = 'l', REMOVE_POOL_LIQUIDITY = 'r', @@ -379,6 +381,7 @@ interface account { enum DfTxType { MINT_TOKEN = 'M', + BURN_TOKEN = 'F', POOL_SWAP = 's', ADD_POOL_LIQUIDITY = 'l', REMOVE_POOL_LIQUIDITY = 'r', From 081995df4be374e35a989f1e5bb8a4448f885df3 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Tue, 3 Jan 2023 18:23:33 +0800 Subject: [PATCH 29/39] Update packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts Co-authored-by: Isaac Yong Signed-off-by: Dilshan Madushanka --- .../__tests__/category/account/listAccountHistory.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts b/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts index b30f15460f..9505741ff8 100644 --- a/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/account/listAccountHistory.test.ts @@ -43,10 +43,6 @@ describe('Account', () => { for (let i = 1; i <= 10; i++) { await client.token.burnTokens(`${0.01 * i}@dBTC`, addr1) - await container.generate(1) - } - - for (let i = 1; i <= 10; i++) { await client.token.burnTokens(`${0.01 * i}@dETH`, addr2) await container.generate(1) } From c37c4b80bd1ac5c235bc8bfa16b8bfed7e7e88f6 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Wed, 4 Jan 2023 19:57:25 +0800 Subject: [PATCH 30/39] Add tests to validate pagination of txs in the same block --- .../module.api/consortium.controller.spec.ts | 137 +++++++++++------- .../src/module.api/consortium.service.ts | 8 +- .../__tests__/api/consortium.test.ts | 137 +++++++++++------- 3 files changed, 176 insertions(+), 106 deletions(-) diff --git a/apps/whale-api/src/module.api/consortium.controller.spec.ts b/apps/whale-api/src/module.api/consortium.controller.spec.ts index 35351e2db8..313ff3266a 100644 --- a/apps/whale-api/src/module.api/consortium.controller.spec.ts +++ b/apps/whale-api/src/module.api/consortium.controller.spec.ts @@ -200,6 +200,7 @@ describe('getTransactionHistory', () => { let controller: ConsortiumController const txIdMatcher = expect.stringMatching(/[0-f]{64}/) const startFlags: StartFlags[] = [{ name: 'regtest-minttoken-simulate-mainnet', value: 1 }] + const txIds: string[] = [] beforeAll(async () => { await tGroup.start({ startFlags }) @@ -262,17 +263,15 @@ describe('getTransactionHistory', () => { }) await alice.generate(1) - await alice.container.fundAddress(accountBob, 10) - await alice.generate(1) idBTC = await alice.token.getTokenId(symbolBTC) idETH = await alice.token.getTokenId(symbolETH) await setGovAttr({ 'v0/params/feature/consortium': 'true', - [`v0/consortium/${idBTC}/mint_limit`]: '10', - [`v0/consortium/${idBTC}/mint_limit_daily`]: '5', - [`v0/consortium/${idETH}/mint_limit`]: '20', - [`v0/consortium/${idETH}/mint_limit_daily`]: '10' + [`v0/consortium/${idBTC}/mint_limit`]: '20', + [`v0/consortium/${idBTC}/mint_limit_daily`]: '10', + [`v0/consortium/${idETH}/mint_limit`]: '40', + [`v0/consortium/${idETH}/mint_limit_daily`]: '20' }) await setMemberInfo(idBTC, [{ @@ -280,15 +279,15 @@ describe('getTransactionHistory', () => { name: 'alice', ownerAddress: accountAlice, backingId: 'abc', - mintLimitDaily: '5', - mintLimit: '10' + mintLimitDaily: '10', + mintLimit: '20' }, { id: '02', name: 'bob', ownerAddress: accountBob, backingId: 'def,hij', - mintLimitDaily: '5', - mintLimit: '10' + mintLimitDaily: '10', + mintLimit: '20' }]) await setMemberInfo(idETH, [{ @@ -296,17 +295,31 @@ describe('getTransactionHistory', () => { name: 'alice', ownerAddress: accountAlice, backingId: '', - mintLimitDaily: '10.00000000', - mintLimit: '20.00000000' + mintLimitDaily: '20.00000000', + mintLimit: '40.00000000' }, { id: '02', name: 'bob', ownerAddress: accountBob, backingId: 'lmn,opq', - mintLimitDaily: '10.00000000', - mintLimit: '20.00000000' + mintLimitDaily: '20.00000000', + mintLimit: '40.00000000' }]) + txIds.push(await alice.rpc.token.mintTokens(`0.5@${symbolBTC}`)) + txIds.push(await bob.rpc.token.mintTokens(`0.5@${symbolBTC}`)) + await alice.generate(1) + await bob.generate(1) + await tGroup.waitForSync() + + txIds.push(await alice.rpc.token.mintTokens(`0.1@${symbolBTC}`)) + txIds.push(await alice.rpc.token.burnTokens(`0.1@${symbolBTC}`, accountAlice)) + txIds.push(await bob.rpc.token.mintTokens(`0.1@${symbolBTC}`)) + txIds.push(await bob.rpc.token.burnTokens(`0.1@${symbolBTC}`, accountBob)) + await alice.generate(1) + await bob.generate(1) + await tGroup.waitForSync() + await alice.rpc.token.mintTokens(`1@${symbolBTC}`) await alice.generate(1) @@ -345,27 +358,27 @@ describe('getTransactionHistory', () => { }) it('should filter transactions with search term (member name)', async () => { - const info = await controller.getTransactionHistory({ search: 'alice' }) + const info = await controller.getTransactionHistory({ search: 'alice', limit: 3 }) expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 110 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 } + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 113 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 112 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 111 } ]) - expect(info.total).toStrictEqual(3) + expect(info.total).toStrictEqual(6) }) it('should filter transactions with search term (owner address)', async () => { - const info = await controller.getTransactionHistory({ search: accountAlice, limit: 20 }) + const info = await controller.getTransactionHistory({ search: accountAlice, limit: 3 }) expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 110 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 } + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 113 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 112 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 111 } ]) - expect(info.total).toStrictEqual(3) + expect(info.total).toStrictEqual(6) }) it('should filter transactions with search term (transaction id)', async () => { @@ -375,7 +388,7 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(1) expect(info.transactions).toStrictEqual([ - { txId: tx.txid, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 } + { txId: tx.txid, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 113 } ]) expect(info.total).toStrictEqual(1) }) @@ -385,11 +398,11 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 113 }, - { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 112 }, - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 } + { txId: txIdMatcher, type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 115 }, + { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 114 }, + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 113 } ]) - expect(info.total).toStrictEqual(5) + expect(info.total).toStrictEqual(11) }) it('should filter and limit transactions at the same time', async () => { @@ -397,10 +410,10 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(2) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 110 } + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 113 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 112 } ]) - expect(info.total).toStrictEqual(3) + expect(info.total).toStrictEqual(6) }) it('should return empty list of transactions for not-found search term', async () => { @@ -413,6 +426,10 @@ describe('getTransactionHistory', () => { it('should not return other transactions from consortium members apart from mints or burns', async () => { const { txid } = await alice.container.fundAddress(accountBob, 11.5) + const height = await alice.container.getBlockCount() + await alice.generate(1) + await waitForIndexedHeight(app, height) + const info = await controller.getTransactionHistory({ search: txid, limit: 20 }) expect(info.transactions.length).toStrictEqual(0) @@ -421,7 +438,6 @@ describe('getTransactionHistory', () => { it('should paginate properly', async () => { const page1 = await controller.getTransactionHistory({ pageIndex: 0, limit: 2 }) - expect(page1).toStrictEqual({ transactions: [ { @@ -430,7 +446,7 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], txId: txIdMatcher, address: accountBob, - block: 113 + block: 115 }, { type: 'Mint', @@ -438,14 +454,13 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], txId: txIdMatcher, address: accountBob, - block: 112 + block: 114 } ], - total: 5 + total: 11 }) const page2 = await controller.getTransactionHistory({ pageIndex: 1, limit: 2 }) - expect(page2).toStrictEqual({ transactions: [ { @@ -454,7 +469,7 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], txId: txIdMatcher, address: accountAlice, - block: 111 + block: 113 }, { type: 'Mint', @@ -462,26 +477,44 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], txId: txIdMatcher, address: accountAlice, - block: 110 + block: 112 } ], - total: 5 + total: 11 }) const page3 = await controller.getTransactionHistory({ pageIndex: 2, limit: 2 }) + expect(page3.transactions[0]).toStrictEqual({ + type: 'Mint', + member: 'alice', + tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], + txId: txIdMatcher, + address: accountAlice, + block: 111 + }) + expect(page3.transactions.length).toStrictEqual(2) + expect(page3.total).toStrictEqual(11) - expect(page3).toStrictEqual({ - transactions: [ - { - type: 'Mint', - member: 'alice', - tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], - txId: txIdMatcher, - address: accountAlice, - block: 109 - } - ], - total: 5 + const page4 = await controller.getTransactionHistory({ pageIndex: 3, limit: 2 }) + expect(page4.transactions.length).toStrictEqual(2) + expect(page4.total).toStrictEqual(11) + + const page5 = await controller.getTransactionHistory({ pageIndex: 4, limit: 2 }) + expect(page5.transactions.length).toStrictEqual(2) + expect(page5.total).toStrictEqual(11) + + const page6 = await controller.getTransactionHistory({ pageIndex: 5, limit: 2 }) + expect(page6.transactions.length).toStrictEqual(1) + expect(page6.total).toStrictEqual(11) + + const txsCombined = [page3.transactions[1]].concat(page4.transactions, page5.transactions, page6.transactions) + + txsCombined.forEach(({ txId }) => { + const index = txIds.indexOf(txId) + expect(index).toBeGreaterThanOrEqual(0) + txIds.splice(index, 1) }) + + expect(txIds.length).toStrictEqual(0) }) }) diff --git a/apps/whale-api/src/module.api/consortium.service.ts b/apps/whale-api/src/module.api/consortium.service.ts index 28bc9bed46..47a4ce7231 100644 --- a/apps/whale-api/src/module.api/consortium.service.ts +++ b/apps/whale-api/src/module.api/consortium.service.ts @@ -78,8 +78,12 @@ export class ConsortiumService { if (searching && !searchFound) { const foundTx = await this.transactionMapper.get(search) if (foundTx !== undefined) { - const transactionsOnBlock = await this.rpcClient.account.listAccountHistory('all', { maxBlockHeight: foundTx.block.height, depth: 0 }) - const transaction = transactionsOnBlock.find(tx => tx.txid === foundTx.txid) + const relevantTxsOnBlock = await this.rpcClient.account.listAccountHistory(members.map(m => m.address), { + maxBlockHeight: foundTx.block.height, + depth: 0, + txtypes: [DfTxType.MINT_TOKEN, DfTxType.BURN_TOKEN] + }) + const transaction = relevantTxsOnBlock.find(tx => tx.txid === foundTx.txid) if (transaction === undefined) { return { diff --git a/packages/whale-api-client/__tests__/api/consortium.test.ts b/packages/whale-api-client/__tests__/api/consortium.test.ts index 6f389f113e..d6620b679c 100644 --- a/packages/whale-api-client/__tests__/api/consortium.test.ts +++ b/packages/whale-api-client/__tests__/api/consortium.test.ts @@ -205,6 +205,7 @@ describe('getTransactionHistory', () => { const client = new StubWhaleApiClient(service) const txIdMatcher = expect.stringMatching(/[0-f]{64}/) const startFlags: StartFlags[] = [{ name: 'regtest-minttoken-simulate-mainnet', value: 1 }] + const txIds: string[] = [] beforeAll(async () => { await tGroup.start({ startFlags }) @@ -271,17 +272,15 @@ describe('getTransactionHistory', () => { }) await alice.generate(1) - await alice.container.fundAddress(accountBob, 10) - await alice.generate(1) idBTC = await alice.token.getTokenId(symbolBTC) idETH = await alice.token.getTokenId(symbolETH) await setGovAttr({ 'v0/params/feature/consortium': 'true', - [`v0/consortium/${idBTC}/mint_limit`]: '10', - [`v0/consortium/${idBTC}/mint_limit_daily`]: '5', - [`v0/consortium/${idETH}/mint_limit`]: '20', - [`v0/consortium/${idETH}/mint_limit_daily`]: '10' + [`v0/consortium/${idBTC}/mint_limit`]: '20', + [`v0/consortium/${idBTC}/mint_limit_daily`]: '10', + [`v0/consortium/${idETH}/mint_limit`]: '40', + [`v0/consortium/${idETH}/mint_limit_daily`]: '20' }) await setMemberInfo(idBTC, [{ @@ -289,15 +288,15 @@ describe('getTransactionHistory', () => { name: 'alice', ownerAddress: accountAlice, backingId: 'abc', - mintLimitDaily: '5', - mintLimit: '10' + mintLimitDaily: '10', + mintLimit: '20' }, { id: '02', name: 'bob', ownerAddress: accountBob, backingId: 'def,hij', - mintLimitDaily: '5', - mintLimit: '10' + mintLimitDaily: '10', + mintLimit: '20' }]) await setMemberInfo(idETH, [{ @@ -305,17 +304,31 @@ describe('getTransactionHistory', () => { name: 'alice', ownerAddress: accountAlice, backingId: '', - mintLimitDaily: '10.00000000', - mintLimit: '20.00000000' + mintLimitDaily: '20.00000000', + mintLimit: '40.00000000' }, { id: '02', name: 'bob', ownerAddress: accountBob, backingId: 'lmn,opq', - mintLimitDaily: '10.00000000', - mintLimit: '20.00000000' + mintLimitDaily: '20.00000000', + mintLimit: '40.00000000' }]) + txIds.push(await alice.rpc.token.mintTokens(`0.5@${symbolBTC}`)) + txIds.push(await bob.rpc.token.mintTokens(`0.5@${symbolBTC}`)) + await alice.generate(1) + await bob.generate(1) + await tGroup.waitForSync() + + txIds.push(await alice.rpc.token.mintTokens(`0.1@${symbolBTC}`)) + txIds.push(await alice.rpc.token.burnTokens(`0.1@${symbolBTC}`, accountAlice)) + txIds.push(await bob.rpc.token.mintTokens(`0.1@${symbolBTC}`)) + txIds.push(await bob.rpc.token.burnTokens(`0.1@${symbolBTC}`, accountBob)) + await alice.generate(1) + await bob.generate(1) + await tGroup.waitForSync() + await alice.rpc.token.mintTokens(`1@${symbolBTC}`) await alice.generate(1) @@ -354,27 +367,27 @@ describe('getTransactionHistory', () => { }) it('should filter transactions with search term (member name)', async () => { - const info = await client.consortium.getTransactionHistory(0, 10, 'alice') + const info = await client.consortium.getTransactionHistory(0, 3, 'alice') expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 110 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 } + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 113 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 112 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 111 } ]) - expect(info.total).toStrictEqual(3) + expect(info.total).toStrictEqual(6) }) it('should filter transactions with search term (owner address)', async () => { - const info = await client.consortium.getTransactionHistory(0, 10, accountAlice) + const info = await client.consortium.getTransactionHistory(0, 3, accountAlice) expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 110 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 109 } + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 113 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 112 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], address: accountAlice, block: 111 } ]) - expect(info.total).toStrictEqual(3) + expect(info.total).toStrictEqual(6) }) it('should filter transactions with search term (transaction id)', async () => { @@ -384,7 +397,7 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(1) expect(info.transactions).toStrictEqual([ - { txId: tx.txid, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 } + { txId: tx.txid, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 113 } ]) expect(info.total).toStrictEqual(1) }) @@ -394,11 +407,11 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 113 }, - { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 112 }, - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 } + { txId: txIdMatcher, type: 'Burn', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], address: accountBob, block: 115 }, + { txId: txIdMatcher, type: 'Mint', member: 'bob', tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], address: accountBob, block: 114 }, + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 113 } ]) - expect(info.total).toStrictEqual(5) + expect(info.total).toStrictEqual(11) }) it('should filter and limit transactions at the same time', async () => { @@ -406,10 +419,10 @@ describe('getTransactionHistory', () => { expect(info.transactions.length).toStrictEqual(2) expect(info.transactions).toStrictEqual([ - { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 111 }, - { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 110 } + { txId: txIdMatcher, type: 'Burn', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], address: accountAlice, block: 113 }, + { txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 112 } ]) - expect(info.total).toStrictEqual(3) + expect(info.total).toStrictEqual(6) }) it('should return empty list of transactions for not-found search term', async () => { @@ -422,6 +435,10 @@ describe('getTransactionHistory', () => { it('should not return other transactions from consortium members apart from mints or burns', async () => { const { txid } = await alice.container.fundAddress(accountBob, 10) + const height = await alice.container.getBlockCount() + await alice.generate(1) + await waitForIndexedHeight(app, height) + const info = await client.consortium.getTransactionHistory(0, 20, txid) expect(info.transactions.length).toStrictEqual(0) @@ -430,7 +447,6 @@ describe('getTransactionHistory', () => { it('should paginate properly', async () => { const page1 = await client.consortium.getTransactionHistory(0, 2) - expect(page1).toStrictEqual({ transactions: [ { @@ -439,7 +455,7 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }], txId: txIdMatcher, address: accountBob, - block: 113 + block: 115 }, { type: 'Mint', @@ -447,14 +463,13 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }], txId: txIdMatcher, address: accountBob, - block: 112 + block: 114 } ], - total: 5 + total: 11 }) const page2 = await client.consortium.getTransactionHistory(1, 2) - expect(page2).toStrictEqual({ transactions: [ { @@ -463,7 +478,7 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }], txId: txIdMatcher, address: accountAlice, - block: 111 + block: 113 }, { type: 'Mint', @@ -471,26 +486,44 @@ describe('getTransactionHistory', () => { tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], txId: txIdMatcher, address: accountAlice, - block: 110 + block: 112 } ], - total: 5 + total: 11 }) const page3 = await client.consortium.getTransactionHistory(2, 2) + expect(page3.transactions[0]).toStrictEqual({ + type: 'Mint', + member: 'alice', + tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], + txId: txIdMatcher, + address: accountAlice, + block: 111 + }) + expect(page3.transactions.length).toStrictEqual(2) + expect(page3.total).toStrictEqual(11) - expect(page3).toStrictEqual({ - transactions: [ - { - type: 'Mint', - member: 'alice', - tokenAmounts: [{ token: 'dBTC', amount: '1.00000000' }], - txId: txIdMatcher, - address: accountAlice, - block: 109 - } - ], - total: 5 + const page4 = await client.consortium.getTransactionHistory(3, 2) + expect(page4.transactions.length).toStrictEqual(2) + expect(page4.total).toStrictEqual(11) + + const page5 = await client.consortium.getTransactionHistory(4, 2) + expect(page5.transactions.length).toStrictEqual(2) + expect(page5.total).toStrictEqual(11) + + const page6 = await client.consortium.getTransactionHistory(5, 2) + expect(page6.transactions.length).toStrictEqual(1) + expect(page6.total).toStrictEqual(11) + + const txsCombined = [page3.transactions[1]].concat(page4.transactions, page5.transactions, page6.transactions) + + txsCombined.forEach(({ txId }) => { + const index = txIds.indexOf(txId) + expect(index).toBeGreaterThanOrEqual(0) + txIds.splice(index, 1) }) + + expect(txIds.length).toStrictEqual(0) }) }) From 9c0dddc2d1dec3662c6668ad9005fece03882c85 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Thu, 5 Jan 2023 13:04:38 +0800 Subject: [PATCH 31/39] Refactor getTransactionHistory func --- .../src/module.api/consortium.service.ts | 88 ++++++++++--------- .../whale-api-client/src/api/consortium.ts | 10 +-- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/apps/whale-api/src/module.api/consortium.service.ts b/apps/whale-api/src/module.api/consortium.service.ts index 47a4ce7231..146f0b5ac0 100644 --- a/apps/whale-api/src/module.api/consortium.service.ts +++ b/apps/whale-api/src/module.api/consortium.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common' import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' import { SemaphoreCache } from '@defichain-apps/libs/caches' import { - ConsortiumMember, ConsortiumTransactionResponse, Transaction, AssetBreakdownInfo, @@ -24,10 +23,10 @@ export class ConsortiumService { protected readonly transactionMapper: TransactionMapper ) {} - private formatTransactionResponse (tx: AccountHistory, members: ConsortiumMember[]): Transaction { + private formatTransactionResponse (tx: AccountHistory, members: MemberDetail[]): Transaction { return { type: tx.type === 'MintToken' ? 'Mint' : 'Burn', - member: members.find(m => m.address === tx.owner)?.name ?? '', + member: members.find(m => m.ownerAddress === tx.owner)?.name ?? '', tokenAmounts: tx.amounts.map((a: any) => { const splits = a.split('@') return { token: splits[1], amount: splits[0] } @@ -38,47 +37,58 @@ export class ConsortiumService { } } - async getTransactionHistory (pageIndex: number, limit: number, search: string): Promise { + async getTransactionHistory (pageIndex: number, limit: number, searchTerm: string): Promise { const attrs = (await this.rpcClient.masternode.getGov('ATTRIBUTES')).ATTRIBUTES - const members: ConsortiumMember[] = [] - const searching: boolean = search !== '' - const keys: string[] = Object.keys(attrs) - const values: object[] = Object.values(attrs) + const searching: boolean = searchTerm !== '' const membersKeyRegex: RegExp = /^v0\/consortium\/\d+\/members$/ + const txIdFormatRegex: RegExp = /^[a-z0-9]{64}$/ + let members: MemberDetail[] = [] let totalTxCount: number = 0 let searchFound: boolean = false - keys.forEach((key: string, i: number) => { - if (membersKeyRegex.exec(key) !== null) { - const membersPerToken = values[i] - const memberIds: string[] = Object.keys(membersPerToken) - let memberDetails: Array<{ ownerAddress: string, name: string }> = Object.values(membersPerToken) + for (const [key, value] of Object.entries(attrs)) { + if (membersKeyRegex.exec(key) === null) { + continue + } - if (searching) { - const foundMembers = memberDetails.filter(m => m.ownerAddress === search || m.name.toLowerCase().includes(search)) - if (foundMembers.length > 0) { - memberDetails = foundMembers - searchFound = true - } - } + const memberIds: string[] = Object.keys(value as object) + let memberDetails: MemberDetail[] = Object.values(value as object) - for (let j = 0; j < memberDetails.length; j++) { - const memberId = memberIds[j] - if (members.find(m => m.id === memberId) === undefined) { - members.push({ - id: memberId, - name: memberDetails[j].name, - address: memberDetails[j].ownerAddress - }) - } + // Filter members list considering the search term is a member address or a name + if (searching) { + const matchingMembers = memberDetails.filter(m => m.ownerAddress === searchTerm || m.name.toLowerCase().includes(searchTerm)) + if (matchingMembers.length > 0) { + memberDetails = matchingMembers + searchFound = true } } - }) + + // Filter unique members + members = memberDetails.reduce((prev, curr, index) => { + const memberId = memberIds[index] + if (prev.find(m => m.id === memberId) === undefined) { + prev.push({ + id: memberId, + name: curr.name, + ownerAddress: curr.ownerAddress + }) + } + return prev + }, []) + } if (searching && !searchFound) { - const foundTx = await this.transactionMapper.get(search) + // Evaluating if the search term is a valid txid format + if (txIdFormatRegex.exec(searchTerm) === null) { + return { + total: 0, + transactions: [] + } + } + + const foundTx = await this.transactionMapper.get(searchTerm) if (foundTx !== undefined) { - const relevantTxsOnBlock = await this.rpcClient.account.listAccountHistory(members.map(m => m.address), { + const relevantTxsOnBlock = await this.rpcClient.account.listAccountHistory(members.map(m => m.ownerAddress), { maxBlockHeight: foundTx.block.height, depth: 0, txtypes: [DfTxType.MINT_TOKEN, DfTxType.BURN_TOKEN] @@ -97,24 +107,20 @@ export class ConsortiumService { transactions: [this.formatTransactionResponse(transaction, members)] } } - - return { - total: 0, - transactions: [] - } } - const transactions: AccountHistory[] = await this.rpcClient.account.listAccountHistory(members.map(m => m.address), { + const transactions: AccountHistory[] = await this.rpcClient.account.listAccountHistory(members.map(m => m.ownerAddress), { txtypes: [DfTxType.MINT_TOKEN, DfTxType.BURN_TOKEN], including_start: true, start: pageIndex * limit, limit }) + // Calculate total transaction counts const promises = [] for (let i = 0; i < members.length; i++) { - promises.push(this.rpcClient.account.historyCount(members[i].address, { txtype: DfTxType.BURN_TOKEN })) - promises.push(this.rpcClient.account.historyCount(members[i].address, { txtype: DfTxType.MINT_TOKEN })) + promises.push(this.rpcClient.account.historyCount(members[i].ownerAddress, { txtype: DfTxType.BURN_TOKEN })) + promises.push(this.rpcClient.account.historyCount(members[i].ownerAddress, { txtype: DfTxType.MINT_TOKEN })) } const counts = await Promise.all(promises) totalTxCount = counts.reduce((prev, curr) => { @@ -153,7 +159,7 @@ export class ConsortiumService { } private pushToAssetBreakdownInfo (assetBreakdownInfo: AssetBreakdownInfo[], memberId: string, memberDetail: MemberDetail, tokenId: string, tokens: TokenInfoWithId[]): void { - const backingAddresses: string[] = memberDetail.backingId.length > 0 ? memberDetail.backingId.split(',').map(a => a.trim()) : [] + const backingAddresses: string[] = memberDetail.backingId !== undefined && memberDetail.backingId.length > 0 ? memberDetail.backingId.split(',').map(a => a.trim()) : [] const member: MemberWithTokenInfo = { id: memberId, diff --git a/packages/whale-api-client/src/api/consortium.ts b/packages/whale-api-client/src/api/consortium.ts index 6fda00b392..c4a9d3de45 100644 --- a/packages/whale-api-client/src/api/consortium.ts +++ b/packages/whale-api-client/src/api/consortium.ts @@ -57,14 +57,10 @@ export interface Transaction { block: number } -export interface ConsortiumMember { - id: string - name: string - address: string -} - export interface MemberDetail { - backingId: string + id: string + backingId?: string + ownerAddress: string name: string } From ee8465c4fd8ff8f58f65ff209160331fe5c46e90 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Thu, 5 Jan 2023 15:40:11 +0800 Subject: [PATCH 32/39] Optimize getTransactionHistory func --- .../src/module.api/consortium.service.ts | 55 ++++++++----------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/apps/whale-api/src/module.api/consortium.service.ts b/apps/whale-api/src/module.api/consortium.service.ts index 146f0b5ac0..0f42d50b5b 100644 --- a/apps/whale-api/src/module.api/consortium.service.ts +++ b/apps/whale-api/src/module.api/consortium.service.ts @@ -40,52 +40,37 @@ export class ConsortiumService { async getTransactionHistory (pageIndex: number, limit: number, searchTerm: string): Promise { const attrs = (await this.rpcClient.masternode.getGov('ATTRIBUTES')).ATTRIBUTES const searching: boolean = searchTerm !== '' - const membersKeyRegex: RegExp = /^v0\/consortium\/\d+\/members$/ const txIdFormatRegex: RegExp = /^[a-z0-9]{64}$/ - let members: MemberDetail[] = [] + const membersKeyRegex: RegExp = /^v0\/consortium\/\d+\/members$/ + const searchForTxId = searching && txIdFormatRegex.exec(searchTerm) !== null + const searchForMemberDetail = searching && txIdFormatRegex.exec(searchTerm) === null let totalTxCount: number = 0 - let searchFound: boolean = false - for (const [key, value] of Object.entries(attrs)) { + const members = (Object.entries(attrs) as [[string, object]]).reduce((prev: MemberDetail[], [key, value]) => { if (membersKeyRegex.exec(key) === null) { - continue + return prev } - const memberIds: string[] = Object.keys(value as object) - let memberDetails: MemberDetail[] = Object.values(value as object) - - // Filter members list considering the search term is a member address or a name - if (searching) { - const matchingMembers = memberDetails.filter(m => m.ownerAddress === searchTerm || m.name.toLowerCase().includes(searchTerm)) - if (matchingMembers.length > 0) { - memberDetails = matchingMembers - searchFound = true + (Object.entries(value) as [[string, MemberDetail]]).forEach(([memberId, memberDetail]) => { + if (searchForMemberDetail) { + if (!(memberDetail.ownerAddress === searchTerm || memberDetail.name.toLowerCase().includes(searchTerm))) { + return prev + } } - } - // Filter unique members - members = memberDetails.reduce((prev, curr, index) => { - const memberId = memberIds[index] - if (prev.find(m => m.id === memberId) === undefined) { + if (!prev.some(m => m.id === memberId)) { prev.push({ id: memberId, - name: curr.name, - ownerAddress: curr.ownerAddress + name: memberDetail.name, + ownerAddress: memberDetail.ownerAddress }) } - return prev - }, []) - } + }) - if (searching && !searchFound) { - // Evaluating if the search term is a valid txid format - if (txIdFormatRegex.exec(searchTerm) === null) { - return { - total: 0, - transactions: [] - } - } + return prev + }, []) + if (searchForTxId) { const foundTx = await this.transactionMapper.get(searchTerm) if (foundTx !== undefined) { const relevantTxsOnBlock = await this.rpcClient.account.listAccountHistory(members.map(m => m.ownerAddress), { @@ -107,6 +92,11 @@ export class ConsortiumService { transactions: [this.formatTransactionResponse(transaction, members)] } } + + return { + total: 0, + transactions: [] + } } const transactions: AccountHistory[] = await this.rpcClient.account.listAccountHistory(members.map(m => m.ownerAddress), { @@ -116,7 +106,6 @@ export class ConsortiumService { limit }) - // Calculate total transaction counts const promises = [] for (let i = 0; i < members.length; i++) { promises.push(this.rpcClient.account.historyCount(members[i].ownerAddress, { txtype: DfTxType.BURN_TOKEN })) From fe753048621b58f14c4ed9e5baba375faeb77838 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Thu, 5 Jan 2023 16:12:14 +0800 Subject: [PATCH 33/39] Remove waitForExpect --- .../category/account/accountHistoryCount.test.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/jellyfish-api-core/__tests__/category/account/accountHistoryCount.test.ts b/packages/jellyfish-api-core/__tests__/category/account/accountHistoryCount.test.ts index 1e36ebeede..8abd85e59c 100644 --- a/packages/jellyfish-api-core/__tests__/category/account/accountHistoryCount.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/account/accountHistoryCount.test.ts @@ -133,20 +133,16 @@ describe('Account', () => { it('should get accountHistoryCount for multiple txtypes at once', async () => { const mintCount = await client.account.historyCount('mine', { txtype: DfTxType.MINT_TOKEN }) const poolSwapCount = await client.account.historyCount('mine', { txtype: DfTxType.POOL_SWAP }) + const combinedCount = await client.account.historyCount('mine', { txtypes: [DfTxType.MINT_TOKEN, DfTxType.POOL_SWAP] }) - await waitForExpect(async () => { - const combinedCount = await client.account.historyCount('mine', { txtypes: [DfTxType.MINT_TOKEN, DfTxType.POOL_SWAP] }) - expect(combinedCount).toStrictEqual(mintCount + poolSwapCount) - }) + expect(combinedCount).toStrictEqual(mintCount + poolSwapCount) }) it('should get accountHistoryCount for multiple addresses at once', async () => { const fromCount = await client.account.historyCount(from) const toCount = await client.account.historyCount(to) + const combinedCount = await client.account.historyCount([from, to]) - await waitForExpect(async () => { - const combinedCount = await client.account.historyCount([from, to]) - expect(combinedCount).toStrictEqual(fromCount + toCount) - }) + expect(combinedCount).toStrictEqual(fromCount + toCount) }) }) From f4d6b5499f106416554be5d50f417a28ea5c9499 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Thu, 5 Jan 2023 17:30:47 +0800 Subject: [PATCH 34/39] Update txId format regex --- apps/whale-api/src/module.api/consortium.controller.spec.ts | 2 +- apps/whale-api/src/module.api/consortium.service.ts | 2 +- packages/whale-api-client/__tests__/api/consortium.test.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/whale-api/src/module.api/consortium.controller.spec.ts b/apps/whale-api/src/module.api/consortium.controller.spec.ts index 313ff3266a..5a5a2ef878 100644 --- a/apps/whale-api/src/module.api/consortium.controller.spec.ts +++ b/apps/whale-api/src/module.api/consortium.controller.spec.ts @@ -198,7 +198,7 @@ describe('getTransactionHistory', () => { let idETH: string let app: NestFastifyApplication let controller: ConsortiumController - const txIdMatcher = expect.stringMatching(/[0-f]{64}/) + const txIdMatcher = expect.stringMatching(/^[0-9a-f]{64}$/) const startFlags: StartFlags[] = [{ name: 'regtest-minttoken-simulate-mainnet', value: 1 }] const txIds: string[] = [] diff --git a/apps/whale-api/src/module.api/consortium.service.ts b/apps/whale-api/src/module.api/consortium.service.ts index 0f42d50b5b..5446eed9bc 100644 --- a/apps/whale-api/src/module.api/consortium.service.ts +++ b/apps/whale-api/src/module.api/consortium.service.ts @@ -40,7 +40,7 @@ export class ConsortiumService { async getTransactionHistory (pageIndex: number, limit: number, searchTerm: string): Promise { const attrs = (await this.rpcClient.masternode.getGov('ATTRIBUTES')).ATTRIBUTES const searching: boolean = searchTerm !== '' - const txIdFormatRegex: RegExp = /^[a-z0-9]{64}$/ + const txIdFormatRegex: RegExp = /^[0-9a-f]{64}$/ const membersKeyRegex: RegExp = /^v0\/consortium\/\d+\/members$/ const searchForTxId = searching && txIdFormatRegex.exec(searchTerm) !== null const searchForMemberDetail = searching && txIdFormatRegex.exec(searchTerm) === null diff --git a/packages/whale-api-client/__tests__/api/consortium.test.ts b/packages/whale-api-client/__tests__/api/consortium.test.ts index d6620b679c..afcccb05ef 100644 --- a/packages/whale-api-client/__tests__/api/consortium.test.ts +++ b/packages/whale-api-client/__tests__/api/consortium.test.ts @@ -203,7 +203,7 @@ describe('getTransactionHistory', () => { let idETH: string const service = new StubService(alice.container) const client = new StubWhaleApiClient(service) - const txIdMatcher = expect.stringMatching(/[0-f]{64}/) + const txIdMatcher = expect.stringMatching(/^[0-9a-f]{64}$/) const startFlags: StartFlags[] = [{ name: 'regtest-minttoken-simulate-mainnet', value: 1 }] const txIds: string[] = [] From 8789df44e480fc65cb65919d6f8eefa00bbe6fe5 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Thu, 5 Jan 2023 18:24:04 +0800 Subject: [PATCH 35/39] Break getTransactionHistory into few functions --- .../src/module.api/consortium.service.ts | 74 +++++++++++-------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/apps/whale-api/src/module.api/consortium.service.ts b/apps/whale-api/src/module.api/consortium.service.ts index 5446eed9bc..ca24b3a4ef 100644 --- a/apps/whale-api/src/module.api/consortium.service.ts +++ b/apps/whale-api/src/module.api/consortium.service.ts @@ -37,16 +37,16 @@ export class ConsortiumService { } } - async getTransactionHistory (pageIndex: number, limit: number, searchTerm: string): Promise { + private isValidTxIdFormat (value: string): boolean { + return /^[0-9a-f]{64}$/.exec(value) !== null + } + + private async getFilteredUniqueMembers (searchTerm: string): Promise { const attrs = (await this.rpcClient.masternode.getGov('ATTRIBUTES')).ATTRIBUTES - const searching: boolean = searchTerm !== '' - const txIdFormatRegex: RegExp = /^[0-9a-f]{64}$/ const membersKeyRegex: RegExp = /^v0\/consortium\/\d+\/members$/ - const searchForTxId = searching && txIdFormatRegex.exec(searchTerm) !== null - const searchForMemberDetail = searching && txIdFormatRegex.exec(searchTerm) === null - let totalTxCount: number = 0 + const searchForMemberDetail = searchTerm !== '' && !this.isValidTxIdFormat(searchTerm) - const members = (Object.entries(attrs) as [[string, object]]).reduce((prev: MemberDetail[], [key, value]) => { + return (Object.entries(attrs) as [[string, object]]).reduce((prev: MemberDetail[], [key, value]) => { if (membersKeyRegex.exec(key) === null) { return prev } @@ -69,36 +69,37 @@ export class ConsortiumService { return prev }, []) + } - if (searchForTxId) { - const foundTx = await this.transactionMapper.get(searchTerm) - if (foundTx !== undefined) { - const relevantTxsOnBlock = await this.rpcClient.account.listAccountHistory(members.map(m => m.ownerAddress), { - maxBlockHeight: foundTx.block.height, - depth: 0, - txtypes: [DfTxType.MINT_TOKEN, DfTxType.BURN_TOKEN] - }) - const transaction = relevantTxsOnBlock.find(tx => tx.txid === foundTx.txid) - - if (transaction === undefined) { - return { - total: 0, - transactions: [] - } - } - - return { - total: 1, - transactions: [this.formatTransactionResponse(transaction, members)] - } + private async getSingleHistoryTransactionResponse (searchTerm: string, members: MemberDetail[]): Promise { + const foundTx = await this.transactionMapper.get(searchTerm) + if (foundTx === undefined) { + return { + transactions: [], + total: 0 } + } + const relevantTxsOnBlock = await this.rpcClient.account.listAccountHistory(members.map(m => m.ownerAddress), { + maxBlockHeight: foundTx.block.height, + depth: 0, + txtypes: [DfTxType.MINT_TOKEN, DfTxType.BURN_TOKEN] + }) + const transaction = relevantTxsOnBlock.find(tx => tx.txid === foundTx.txid) + if (transaction === undefined) { return { - total: 0, - transactions: [] + transactions: [], + total: 0 } } + return { + transactions: [this.formatTransactionResponse(transaction, members)], + total: 1 + } + } + + private async getPaginatedHistoryTransactionsResponse (pageIndex: number, limit: number, members: MemberDetail[]): Promise { const transactions: AccountHistory[] = await this.rpcClient.account.listAccountHistory(members.map(m => m.ownerAddress), { txtypes: [DfTxType.MINT_TOKEN, DfTxType.BURN_TOKEN], including_start: true, @@ -112,7 +113,7 @@ export class ConsortiumService { promises.push(this.rpcClient.account.historyCount(members[i].ownerAddress, { txtype: DfTxType.MINT_TOKEN })) } const counts = await Promise.all(promises) - totalTxCount = counts.reduce((prev, curr) => { + const totalTxCount = counts.reduce((prev, curr) => { return prev + curr }, 0) @@ -124,6 +125,17 @@ export class ConsortiumService { } } + async getTransactionHistory (pageIndex: number, limit: number, searchTerm: string): Promise { + const members = await this.getFilteredUniqueMembers(searchTerm) + + const searchForTxId = searchTerm !== '' && this.isValidTxIdFormat(searchTerm) + if (searchForTxId) { + return await this.getSingleHistoryTransactionResponse(searchTerm, members) + } + + return await this.getPaginatedHistoryTransactionsResponse(pageIndex, limit, members) + } + private updateBurnMintAmounts (assetBreakdownInfo: AssetBreakdownInfo[], tokens: TokenInfoWithId[], key: string, value: string): void { const tokenId = key.split('/')[4] const memberId = key.split('/')[5] From 11c1e502c77f6aab8d11a02f7572bd3aa55ac72b Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Fri, 6 Jan 2023 11:12:52 +0800 Subject: [PATCH 36/39] Make params optional --- .../src/module.api/consortium.controller.spec.ts | 16 ++++++++-------- .../src/module.api/consortium.controller.ts | 10 +++++----- packages/whale-api-client/src/api/consortium.ts | 10 +++++----- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/apps/whale-api/src/module.api/consortium.controller.spec.ts b/apps/whale-api/src/module.api/consortium.controller.spec.ts index 5a5a2ef878..7b6d0f8366 100644 --- a/apps/whale-api/src/module.api/consortium.controller.spec.ts +++ b/apps/whale-api/src/module.api/consortium.controller.spec.ts @@ -349,8 +349,8 @@ describe('getTransactionHistory', () => { }) it('should throw an error if the search term is invalid', async () => { - await expect(controller.getTransactionHistory({ search: 'a', limit: 1 })).rejects.toThrow('InvalidSearchTerm') - await expect(controller.getTransactionHistory({ search: 'a'.repeat(65), limit: 1 })).rejects.toThrow('InvalidSearchTerm') + await expect(controller.getTransactionHistory({ searchTerm: 'a', limit: 1 })).rejects.toThrow('InvalidSearchTerm') + await expect(controller.getTransactionHistory({ searchTerm: 'a'.repeat(65), limit: 1 })).rejects.toThrow('InvalidSearchTerm') }) it('should throw an error if the pageIndex is invalid', async () => { @@ -358,7 +358,7 @@ describe('getTransactionHistory', () => { }) it('should filter transactions with search term (member name)', async () => { - const info = await controller.getTransactionHistory({ search: 'alice', limit: 3 }) + const info = await controller.getTransactionHistory({ searchTerm: 'alice', limit: 3 }) expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ @@ -370,7 +370,7 @@ describe('getTransactionHistory', () => { }) it('should filter transactions with search term (owner address)', async () => { - const info = await controller.getTransactionHistory({ search: accountAlice, limit: 3 }) + const info = await controller.getTransactionHistory({ searchTerm: accountAlice, limit: 3 }) expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ @@ -384,7 +384,7 @@ describe('getTransactionHistory', () => { it('should filter transactions with search term (transaction id)', async () => { const tx = (await alice.rpc.account.listAccountHistory(accountAlice))[0] - const info = await controller.getTransactionHistory({ search: tx.txid, limit: 20 }) + const info = await controller.getTransactionHistory({ searchTerm: tx.txid, limit: 20 }) expect(info.transactions.length).toStrictEqual(1) expect(info.transactions).toStrictEqual([ @@ -406,7 +406,7 @@ describe('getTransactionHistory', () => { }) it('should filter and limit transactions at the same time', async () => { - const info = await controller.getTransactionHistory({ search: accountAlice, limit: 2 }) + const info = await controller.getTransactionHistory({ searchTerm: accountAlice, limit: 2 }) expect(info.transactions.length).toStrictEqual(2) expect(info.transactions).toStrictEqual([ @@ -417,7 +417,7 @@ describe('getTransactionHistory', () => { }) it('should return empty list of transactions for not-found search term', async () => { - const info = await controller.getTransactionHistory({ search: 'not-found-term', limit: 20 }) + const info = await controller.getTransactionHistory({ searchTerm: 'not-found-term', limit: 20 }) expect(info.transactions.length).toStrictEqual(0) expect(info.total).toStrictEqual(0) @@ -430,7 +430,7 @@ describe('getTransactionHistory', () => { await alice.generate(1) await waitForIndexedHeight(app, height) - const info = await controller.getTransactionHistory({ search: txid, limit: 20 }) + const info = await controller.getTransactionHistory({ searchTerm: txid, limit: 20 }) expect(info.transactions.length).toStrictEqual(0) expect(info.total).toStrictEqual(0) diff --git a/apps/whale-api/src/module.api/consortium.controller.ts b/apps/whale-api/src/module.api/consortium.controller.ts index d10ad5ce99..352ef132e4 100644 --- a/apps/whale-api/src/module.api/consortium.controller.ts +++ b/apps/whale-api/src/module.api/consortium.controller.ts @@ -17,15 +17,15 @@ export class ConsortiumController { */ @Get('/transactions') async getTransactionHistory ( - @Query() query: { pageIndex?: number, limit?: number, search?: string } + @Query() query: { pageIndex?: number, limit?: number, searchTerm?: string } ): Promise { - const { pageIndex = 0, limit = 20, search = undefined } = query + const { pageIndex = 0, limit = 20, searchTerm = undefined } = query if (limit > 50 || limit < 1) { throw new ForbiddenException('InvalidLimit') } - if (search !== undefined && (search.length < 3 || search.length > 64)) { + if (searchTerm !== undefined && (searchTerm.length < 3 || searchTerm.length > 64)) { throw new ForbiddenException('InvalidSearchTerm') } @@ -33,8 +33,8 @@ export class ConsortiumController { throw new ForbiddenException('InvalidPageIndex') } - return await this.cache.get(`CONSORTIUM_TRANSACTIONS_${JSON.stringify({ pageIndex, limit, search })}`, async () => { - return await this.consortiumService.getTransactionHistory(+pageIndex, +limit, typeof search === 'string' ? search : '') + return await this.cache.get(`CONSORTIUM_TRANSACTIONS_${JSON.stringify({ pageIndex, limit, searchTerm })}`, async () => { + return await this.consortiumService.getTransactionHistory(+pageIndex, +limit, typeof searchTerm === 'string' ? searchTerm : '') }, { ttl: 600 // 10 mins }) as ConsortiumTransactionResponse diff --git a/packages/whale-api-client/src/api/consortium.ts b/packages/whale-api-client/src/api/consortium.ts index c4a9d3de45..6faaec33fa 100644 --- a/packages/whale-api-client/src/api/consortium.ts +++ b/packages/whale-api-client/src/api/consortium.ts @@ -19,12 +19,12 @@ export class Consortium { /** * Gets the transaction history of consortium members. * - * @param {number} start The starting index for pagination + * @param {number} [pageIndex] The starting index for pagination * @param {number} [limit] How many transactions to fetch - * @param {string} [search] Search term, can be a transaction id, member/owner address or member name + * @param {string} [searchTerm] Search term, can be a transaction id, member/owner address or name * @return {Promise} */ - async getTransactionHistory (pageIndex: number, limit: number, search?: string): Promise { + async getTransactionHistory (pageIndex?: number, limit?: number, searchTerm?: string): Promise { const query = [] if (pageIndex !== undefined) { @@ -35,8 +35,8 @@ export class Consortium { query.push(`limit=${limit}`) } - if (search !== undefined) { - query.push(`search=${search}`) + if (searchTerm !== undefined) { + query.push(`searchTerm=${searchTerm}`) } return await this.client.requestData('GET', `consortium/transactions?${query.join('&')}`) From 23889602f2bf11b34c267afd14bed16068c3bdd0 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Thu, 19 Jan 2023 16:33:52 +0800 Subject: [PATCH 37/39] Use updated accounthistorycount rpc --- .../src/module.api/consortium.controller.spec.ts | 14 +++++++------- .../whale-api/src/module.api/consortium.service.ts | 14 ++++---------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/apps/whale-api/src/module.api/consortium.controller.spec.ts b/apps/whale-api/src/module.api/consortium.controller.spec.ts index 00e30415af..558a113ab3 100644 --- a/apps/whale-api/src/module.api/consortium.controller.spec.ts +++ b/apps/whale-api/src/module.api/consortium.controller.spec.ts @@ -306,31 +306,31 @@ describe('getTransactionHistory', () => { mintLimit: '40.00000000' }]) - txIds.push(await alice.rpc.token.mintTokens(`0.5@${symbolBTC}`)) - txIds.push(await bob.rpc.token.mintTokens(`0.5@${symbolBTC}`)) + txIds.push(await alice.rpc.token.mintTokens({ amounts: [`0.5@${symbolBTC}`] })) + txIds.push(await bob.rpc.token.mintTokens({ amounts: [`0.5@${symbolBTC}`] })) await alice.generate(1) await bob.generate(1) await tGroup.waitForSync() - txIds.push(await alice.rpc.token.mintTokens(`0.1@${symbolBTC}`)) + txIds.push(await alice.rpc.token.mintTokens({ amounts: [`0.1@${symbolBTC}`] })) txIds.push(await alice.rpc.token.burnTokens(`0.1@${symbolBTC}`, accountAlice)) - txIds.push(await bob.rpc.token.mintTokens(`0.1@${symbolBTC}`)) + txIds.push(await bob.rpc.token.mintTokens({ amounts: [`0.1@${symbolBTC}`] })) txIds.push(await bob.rpc.token.burnTokens(`0.1@${symbolBTC}`, accountBob)) await alice.generate(1) await bob.generate(1) await tGroup.waitForSync() - await alice.rpc.token.mintTokens(`1@${symbolBTC}`) + await alice.rpc.token.mintTokens({ amounts: [`1@${symbolBTC}`] }) await alice.generate(1) - await alice.rpc.token.mintTokens(`2@${symbolETH}`) + await alice.rpc.token.mintTokens({ amounts: [`2@${symbolETH}`] }) await alice.generate(1) await alice.rpc.token.burnTokens(`1@${symbolETH}`, accountAlice) await alice.generate(1) await tGroup.waitForSync() - await bob.rpc.token.mintTokens(`4@${symbolBTC}`) + await bob.rpc.token.mintTokens({ amounts: [`4@${symbolBTC}`] }) await bob.generate(1) await bob.rpc.token.burnTokens(`2@${symbolBTC}`, accountBob) diff --git a/apps/whale-api/src/module.api/consortium.service.ts b/apps/whale-api/src/module.api/consortium.service.ts index ca24b3a4ef..8cffe7d9ac 100644 --- a/apps/whale-api/src/module.api/consortium.service.ts +++ b/apps/whale-api/src/module.api/consortium.service.ts @@ -100,22 +100,16 @@ export class ConsortiumService { } private async getPaginatedHistoryTransactionsResponse (pageIndex: number, limit: number, members: MemberDetail[]): Promise { - const transactions: AccountHistory[] = await this.rpcClient.account.listAccountHistory(members.map(m => m.ownerAddress), { + const memberAddresses = members.map(m => m.ownerAddress) + + const transactions: AccountHistory[] = await this.rpcClient.account.listAccountHistory(memberAddresses, { txtypes: [DfTxType.MINT_TOKEN, DfTxType.BURN_TOKEN], including_start: true, start: pageIndex * limit, limit }) - const promises = [] - for (let i = 0; i < members.length; i++) { - promises.push(this.rpcClient.account.historyCount(members[i].ownerAddress, { txtype: DfTxType.BURN_TOKEN })) - promises.push(this.rpcClient.account.historyCount(members[i].ownerAddress, { txtype: DfTxType.MINT_TOKEN })) - } - const counts = await Promise.all(promises) - const totalTxCount = counts.reduce((prev, curr) => { - return prev + curr - }, 0) + const totalTxCount = await this.rpcClient.account.historyCount(memberAddresses, { txtypes: [DfTxType.BURN_TOKEN, DfTxType.MINT_TOKEN] }) return { transactions: transactions.map(tx => { From cc9ab24695380c174bcd4e522bc41c542fdb7e43 Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Thu, 19 Jan 2023 16:52:54 +0800 Subject: [PATCH 38/39] Update mintTokens params --- .../__tests__/api/consortium.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/whale-api-client/__tests__/api/consortium.test.ts b/packages/whale-api-client/__tests__/api/consortium.test.ts index 8f16559768..7906a63099 100644 --- a/packages/whale-api-client/__tests__/api/consortium.test.ts +++ b/packages/whale-api-client/__tests__/api/consortium.test.ts @@ -315,31 +315,31 @@ describe('getTransactionHistory', () => { mintLimit: '40.00000000' }]) - txIds.push(await alice.rpc.token.mintTokens(`0.5@${symbolBTC}`)) - txIds.push(await bob.rpc.token.mintTokens(`0.5@${symbolBTC}`)) + txIds.push(await alice.rpc.token.mintTokens({ amounts: [`0.5@${symbolBTC}`] })) + txIds.push(await bob.rpc.token.mintTokens({ amounts: [`0.5@${symbolBTC}`] })) await alice.generate(1) await bob.generate(1) await tGroup.waitForSync() - txIds.push(await alice.rpc.token.mintTokens(`0.1@${symbolBTC}`)) + txIds.push(await alice.rpc.token.mintTokens({ amounts: [`0.1@${symbolBTC}`] })) txIds.push(await alice.rpc.token.burnTokens(`0.1@${symbolBTC}`, accountAlice)) - txIds.push(await bob.rpc.token.mintTokens(`0.1@${symbolBTC}`)) + txIds.push(await bob.rpc.token.mintTokens({ amounts: [`0.1@${symbolBTC}`] })) txIds.push(await bob.rpc.token.burnTokens(`0.1@${symbolBTC}`, accountBob)) await alice.generate(1) await bob.generate(1) await tGroup.waitForSync() - await alice.rpc.token.mintTokens(`1@${symbolBTC}`) + await alice.rpc.token.mintTokens({ amounts: [`1@${symbolBTC}`] }) await alice.generate(1) - await alice.rpc.token.mintTokens(`2@${symbolETH}`) + await alice.rpc.token.mintTokens({ amounts: [`2@${symbolETH}`] }) await alice.generate(1) await alice.rpc.token.burnTokens(`1@${symbolETH}`, accountAlice) await alice.generate(1) await tGroup.waitForSync() - await bob.rpc.token.mintTokens(`4@${symbolBTC}`) + await bob.rpc.token.mintTokens({ amounts: [`4@${symbolBTC}`] }) await bob.generate(1) await bob.rpc.token.burnTokens(`2@${symbolBTC}`, accountBob) From 9e171c7dc05223871584a2f372804518aad3dfea Mon Sep 17 00:00:00 2001 From: Dilshan Madushanka Date: Wed, 25 Jan 2023 13:34:33 +0800 Subject: [PATCH 39/39] Use PaginationQuery --- .../module.api/consortium.controller.spec.ts | 60 ++++++++++++------- .../src/module.api/consortium.controller.ts | 34 +++++------ .../src/module.api/consortium.service.ts | 1 + .../__tests__/api/consortium.test.ts | 34 +++++++---- .../whale-api-client/src/api/consortium.ts | 10 ++-- 5 files changed, 84 insertions(+), 55 deletions(-) diff --git a/apps/whale-api/src/module.api/consortium.controller.spec.ts b/apps/whale-api/src/module.api/consortium.controller.spec.ts index f7607ea656..f45a165ec4 100644 --- a/apps/whale-api/src/module.api/consortium.controller.spec.ts +++ b/apps/whale-api/src/module.api/consortium.controller.spec.ts @@ -400,22 +400,36 @@ describe('getTransactionHistory', () => { await waitForIndexedHeight(app, height) } - it('should throw an error if the limit is invalid', async () => { - await expect(controller.getTransactionHistory({ limit: 51 })).rejects.toThrow('InvalidLimit') - await expect(controller.getTransactionHistory({ limit: 0 })).rejects.toThrow('InvalidLimit') - }) - it('should throw an error if the search term is invalid', async () => { - await expect(controller.getTransactionHistory({ searchTerm: 'a', limit: 1 })).rejects.toThrow('InvalidSearchTerm') - await expect(controller.getTransactionHistory({ searchTerm: 'a'.repeat(65), limit: 1 })).rejects.toThrow('InvalidSearchTerm') - }) + try { + await controller.getTransactionHistory({ searchTerm: 'a', size: 1 }) + } catch (err: any) { + expect(err.message).toStrictEqual('422 - ValidationError (/v0.0/regtest/consortium/transactions?next=1&size=10&searchTerm=a)') + expect(err.properties).toStrictEqual([{ + constraints: [ + 'searchTerm must be longer than or equal to 3 characters' + ], + property: 'searchTerm', + value: 'a' + }]) + } - it('should throw an error if the pageIndex is invalid', async () => { - await expect(controller.getTransactionHistory({ pageIndex: -1, limit: 1 })).rejects.toThrow('InvalidPageIndex') + try { + await controller.getTransactionHistory({ searchTerm: 'a'.repeat(65), size: 1 }) + } catch (err: any) { + expect(err.message).toStrictEqual(`422 - ValidationError (/v0.0/regtest/consortium/transactions?next=1&size=10&searchTerm=${'a'.repeat(65)})`) + expect(err.properties).toStrictEqual([{ + constraints: [ + 'searchTerm must be shorter than or equal to 64 characters' + ], + property: 'searchTerm', + value: 'a'.repeat(65) + }]) + } }) it('should filter transactions with search term (member name)', async () => { - const info = await controller.getTransactionHistory({ searchTerm: 'alice', limit: 3 }) + const info = await controller.getTransactionHistory({ searchTerm: 'alice', size: 3 }) expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ @@ -427,7 +441,7 @@ describe('getTransactionHistory', () => { }) it('should filter transactions with search term (owner address)', async () => { - const info = await controller.getTransactionHistory({ searchTerm: accountAlice, limit: 3 }) + const info = await controller.getTransactionHistory({ searchTerm: accountAlice, size: 3 }) expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ @@ -441,7 +455,7 @@ describe('getTransactionHistory', () => { it('should filter transactions with search term (transaction id)', async () => { const tx = (await alice.rpc.account.listAccountHistory(accountAlice))[0] - const info = await controller.getTransactionHistory({ searchTerm: tx.txid, limit: 20 }) + const info = await controller.getTransactionHistory({ searchTerm: tx.txid, size: 20 }) expect(info.transactions.length).toStrictEqual(1) expect(info.transactions).toStrictEqual([ @@ -451,7 +465,7 @@ describe('getTransactionHistory', () => { }) it('should limit transactions', async () => { - const info = await controller.getTransactionHistory({ limit: 3 }) + const info = await controller.getTransactionHistory({ size: 3 }) expect(info.transactions.length).toStrictEqual(3) expect(info.transactions).toStrictEqual([ @@ -463,7 +477,7 @@ describe('getTransactionHistory', () => { }) it('should filter and limit transactions at the same time', async () => { - const info = await controller.getTransactionHistory({ searchTerm: accountAlice, limit: 2 }) + const info = await controller.getTransactionHistory({ searchTerm: accountAlice, size: 2 }) expect(info.transactions.length).toStrictEqual(2) expect(info.transactions).toStrictEqual([ @@ -474,7 +488,7 @@ describe('getTransactionHistory', () => { }) it('should return empty list of transactions for not-found search term', async () => { - const info = await controller.getTransactionHistory({ searchTerm: 'not-found-term', limit: 20 }) + const info = await controller.getTransactionHistory({ searchTerm: 'not-found-term', size: 20 }) expect(info.transactions.length).toStrictEqual(0) expect(info.total).toStrictEqual(0) @@ -487,14 +501,14 @@ describe('getTransactionHistory', () => { await alice.generate(1) await waitForIndexedHeight(app, height) - const info = await controller.getTransactionHistory({ searchTerm: txid, limit: 20 }) + const info = await controller.getTransactionHistory({ searchTerm: txid, size: 20 }) expect(info.transactions.length).toStrictEqual(0) expect(info.total).toStrictEqual(0) }) it('should paginate properly', async () => { - const page1 = await controller.getTransactionHistory({ pageIndex: 0, limit: 2 }) + const page1 = await controller.getTransactionHistory({ next: '0', size: 2 }) expect(page1).toStrictEqual({ transactions: [ { @@ -517,7 +531,7 @@ describe('getTransactionHistory', () => { total: 11 }) - const page2 = await controller.getTransactionHistory({ pageIndex: 1, limit: 2 }) + const page2 = await controller.getTransactionHistory({ next: '1', size: 2 }) expect(page2).toStrictEqual({ transactions: [ { @@ -540,7 +554,7 @@ describe('getTransactionHistory', () => { total: 11 }) - const page3 = await controller.getTransactionHistory({ pageIndex: 2, limit: 2 }) + const page3 = await controller.getTransactionHistory({ next: '2', size: 2 }) expect(page3.transactions[0]).toStrictEqual({ type: 'Mint', member: 'alice', @@ -552,15 +566,15 @@ describe('getTransactionHistory', () => { expect(page3.transactions.length).toStrictEqual(2) expect(page3.total).toStrictEqual(11) - const page4 = await controller.getTransactionHistory({ pageIndex: 3, limit: 2 }) + const page4 = await controller.getTransactionHistory({ next: '3', size: 2 }) expect(page4.transactions.length).toStrictEqual(2) expect(page4.total).toStrictEqual(11) - const page5 = await controller.getTransactionHistory({ pageIndex: 4, limit: 2 }) + const page5 = await controller.getTransactionHistory({ next: '4', size: 2 }) expect(page5.transactions.length).toStrictEqual(2) expect(page5.total).toStrictEqual(11) - const page6 = await controller.getTransactionHistory({ pageIndex: 5, limit: 2 }) + const page6 = await controller.getTransactionHistory({ next: '5', size: 2 }) expect(page6.transactions.length).toStrictEqual(1) expect(page6.total).toStrictEqual(11) diff --git a/apps/whale-api/src/module.api/consortium.controller.ts b/apps/whale-api/src/module.api/consortium.controller.ts index c2940623d3..de289ab137 100644 --- a/apps/whale-api/src/module.api/consortium.controller.ts +++ b/apps/whale-api/src/module.api/consortium.controller.ts @@ -1,7 +1,17 @@ -import { Controller, ForbiddenException, Get, Query, Param } from '@nestjs/common' +import { Controller, Get, Query, Param } from '@nestjs/common' import { ConsortiumService } from './consortium.service' import { ConsortiumTransactionResponse, AssetBreakdownInfo, MemberStatsInfo } from '@defichain/whale-api-client/dist/api/consortium' import { SemaphoreCache } from '@defichain-apps/libs/caches' +import { PaginationQuery } from './_core/api.query' +import { IsOptional, IsString, MaxLength, MinLength } from 'class-validator' + +class TransactionHistoryPaginationQuery extends PaginationQuery { + @IsOptional() + @IsString() + @MinLength(3) + @MaxLength(64) + searchTerm?: string +} @Controller('/consortium') export class ConsortiumController { @@ -17,24 +27,14 @@ export class ConsortiumController { */ @Get('/transactions') async getTransactionHistory ( - @Query() query: { pageIndex?: number, limit?: number, searchTerm?: string } + @Query() query: TransactionHistoryPaginationQuery = { size: 50 } ): Promise { - const { pageIndex = 0, limit = 20, searchTerm = undefined } = query - - if (limit > 50 || limit < 1) { - throw new ForbiddenException('InvalidLimit') - } - - if (searchTerm !== undefined && (searchTerm.length < 3 || searchTerm.length > 64)) { - throw new ForbiddenException('InvalidSearchTerm') - } - - if (pageIndex < 0) { - throw new ForbiddenException('InvalidPageIndex') - } + const { searchTerm = '' } = query + const next = query.next !== undefined ? Number(query.next) : 0 + const size = Math.min(query.size, 50) - return await this.cache.get(`CONSORTIUM_TRANSACTIONS_${JSON.stringify({ pageIndex, limit, searchTerm })}`, async () => { - return await this.consortiumService.getTransactionHistory(+pageIndex, +limit, typeof searchTerm === 'string' ? searchTerm : '') + return await this.cache.get(`CONSORTIUM_TRANSACTIONS_${JSON.stringify({ next, size, searchTerm })}`, async () => { + return await this.consortiumService.getTransactionHistory(next, size, searchTerm) }, { ttl: 600 // 10 mins }) as ConsortiumTransactionResponse diff --git a/apps/whale-api/src/module.api/consortium.service.ts b/apps/whale-api/src/module.api/consortium.service.ts index 2621a97eea..1b77d9ea42 100644 --- a/apps/whale-api/src/module.api/consortium.service.ts +++ b/apps/whale-api/src/module.api/consortium.service.ts @@ -106,6 +106,7 @@ export class ConsortiumService { const memberAddresses = members.map(m => m.ownerAddress) const transactions: AccountHistory[] = await this.rpcClient.account.listAccountHistory(memberAddresses, { + no_rewards: true, txtypes: [DfTxType.MINT_TOKEN, DfTxType.BURN_TOKEN], including_start: true, start: pageIndex * limit, diff --git a/packages/whale-api-client/__tests__/api/consortium.test.ts b/packages/whale-api-client/__tests__/api/consortium.test.ts index ef011c8025..f04049a0ab 100644 --- a/packages/whale-api-client/__tests__/api/consortium.test.ts +++ b/packages/whale-api-client/__tests__/api/consortium.test.ts @@ -319,18 +319,32 @@ describe('getTransactionHistory', () => { await waitForIndexedHeight(app, height) } - it('should throw an error if the limit is invalid', async () => { - await expect(client.consortium.getTransactionHistory(0, 51)).rejects.toThrow('InvalidLimit') - await expect(client.consortium.getTransactionHistory(0, 0)).rejects.toThrow('InvalidLimit') - }) - it('should throw an error if the search term is invalid', async () => { - await expect(client.consortium.getTransactionHistory(1, 10, 'a')).rejects.toThrow('InvalidSearchTerm') - await expect(client.consortium.getTransactionHistory(1, 10, 'a'.repeat(65))).rejects.toThrow('InvalidSearchTerm') - }) + try { + await client.consortium.getTransactionHistory(1, 10, 'a') + } catch (err: any) { + expect(err.message).toStrictEqual('422 - ValidationError (/v0.0/regtest/consortium/transactions?next=1&size=10&searchTerm=a)') + expect(err.properties).toStrictEqual([{ + constraints: [ + 'searchTerm must be longer than or equal to 3 characters' + ], + property: 'searchTerm', + value: 'a' + }]) + } - it('should throw an error if the pageIndex is invalid', async () => { - await expect(client.consortium.getTransactionHistory(-1, 10)).rejects.toThrow('InvalidPageIndex') + try { + await client.consortium.getTransactionHistory(1, 10, 'a'.repeat(65)) + } catch (err: any) { + expect(err.message).toStrictEqual(`422 - ValidationError (/v0.0/regtest/consortium/transactions?next=1&size=10&searchTerm=${'a'.repeat(65)})`) + expect(err.properties).toStrictEqual([{ + constraints: [ + 'searchTerm must be shorter than or equal to 64 characters' + ], + property: 'searchTerm', + value: 'a'.repeat(65) + }]) + } }) it('should filter transactions with search term (member name)', async () => { diff --git a/packages/whale-api-client/src/api/consortium.ts b/packages/whale-api-client/src/api/consortium.ts index d7a439e990..afbb8ef29e 100644 --- a/packages/whale-api-client/src/api/consortium.ts +++ b/packages/whale-api-client/src/api/consortium.ts @@ -34,15 +34,15 @@ export class Consortium { * @param {string} [searchTerm] Search term, can be a transaction id, member/owner address or name * @return {Promise} */ - async getTransactionHistory (pageIndex?: number, limit?: number, searchTerm?: string): Promise { + async getTransactionHistory (next?: number, size?: number, searchTerm?: string): Promise { const query = [] - if (pageIndex !== undefined) { - query.push(`pageIndex=${pageIndex}`) + if (next !== undefined) { + query.push(`next=${next}`) } - if (limit !== undefined) { - query.push(`limit=${limit}`) + if (size !== undefined) { + query.push(`size=${size}`) } if (searchTerm !== undefined) {