Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(apps/whale-api): Add consortium transaction history endpoint #1868

Open
wants to merge 65 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
d446ed5
Add burntokens rpc
helloscoopa Nov 11, 2022
5d20bef
Add minttokens tests
helloscoopa Nov 11, 2022
5526572
Merge branch 'main' into dilshan/consortium-rpcs
helloscoopa Nov 11, 2022
28aebb9
Add setgov tests
helloscoopa Nov 13, 2022
ced9ac1
Add listprices tests for pagination
helloscoopa Nov 13, 2022
a2d3c7a
Fix types in jsDocs
helloscoopa Nov 13, 2022
bdfa982
Merge branch 'main' into dilshan/consortium-rpcs
helloscoopa Nov 14, 2022
abdeb7f
Use regtest-minttoken-simulate-mainnet flag
helloscoopa Nov 15, 2022
c936b97
Add tests for unlimited global mint limits
helloscoopa Nov 16, 2022
379d77b
Resolve test failiure on PoolPairController.test.ts
helloscoopa Nov 16, 2022
1e7755a
Add new consortium attribute tests, bump docker image tag
shohamc1 Nov 17, 2022
bf10e84
Allow minting to zero
shohamc1 Nov 17, 2022
ca092bf
Remove waiting for grandcentral height
helloscoopa Nov 17, 2022
5b5b28f
Add GET consortium/transactions endpoint
helloscoopa Nov 22, 2022
7934b46
Fix change requests
helloscoopa Nov 23, 2022
f2d0234
Add test to validate pagination
helloscoopa Nov 24, 2022
64f8b99
Merge branch 'main' into epic/consortium-apis
helloscoopa Nov 28, 2022
4f1f5ea
Merge branch 'epic/consortium-apis' into dilshan/consortium-tx-histor…
Nov 30, 2022
f34be2e
Merge branch 'main' into epic/consortium-apis
helloscoopa Dec 12, 2022
4ed95e3
Fix merge conflicts
helloscoopa Dec 12, 2022
c0e8a6e
Update to support pagination
helloscoopa Dec 12, 2022
653c61d
Remove .only
helloscoopa Dec 12, 2022
b6968b8
Merge branch 'dilshan/listaccounthistory-pagination' into dilshan/con…
helloscoopa Dec 12, 2022
8f7f401
Merge branch 'main' into dilshan/listaccounthistory-pagination
helloscoopa Dec 12, 2022
9b23ca1
Merge branch 'dilshan/listaccounthistory-pagination' into dilshan/con…
helloscoopa Dec 12, 2022
ba49405
Merge branch 'main' into dilshan/listaccounthistory-pagination
helloscoopa Dec 13, 2022
9328d18
Update tests
helloscoopa Dec 13, 2022
fe89127
Add multi-address support
helloscoopa Dec 22, 2022
0c37bbc
Merge branch 'main' into dilshan/listaccounthistory-pagination
helloscoopa Dec 22, 2022
0ae9af9
Merge branch 'dilshan/listaccounthistory-pagination' into dilshan/con…
helloscoopa Dec 22, 2022
1d72f28
Use multi-address support
helloscoopa Dec 22, 2022
52064f3
Revert missing tests while fixing MC
helloscoopa Dec 22, 2022
6b12ea0
Refactor: start -> pageIndex
helloscoopa Dec 23, 2022
9feacb4
Update to support multi-txtype multi-address filter
helloscoopa Dec 23, 2022
65c88e3
Merge branch 'main' into dilshan/listaccounthistory-pagination
helloscoopa Jan 3, 2023
8f9c661
Merge branch 'main' into dilshan/add-more-filter-options-to-accounthi…
helloscoopa Jan 3, 2023
6792708
Bump image to 1929da31d
helloscoopa Jan 3, 2023
1d88862
Bump image to fe4ccb39d
helloscoopa Jan 3, 2023
c3acc51
Update docs
helloscoopa Jan 3, 2023
e6b2319
Update docs
helloscoopa Jan 3, 2023
8eb155b
Remove extra line
helloscoopa Jan 3, 2023
7433ea4
Update docs
helloscoopa Jan 3, 2023
081995d
Update packages/jellyfish-api-core/__tests__/category/account/listAcc…
helloscoopa Jan 3, 2023
c37c4b8
Add tests to validate pagination of txs in the same block
helloscoopa Jan 4, 2023
ddbbe98
Merge branch 'dilshan/listaccounthistory-pagination' into dilshan/con…
helloscoopa Jan 4, 2023
9c0dddc
Refactor getTransactionHistory func
helloscoopa Jan 5, 2023
ee8465c
Optimize getTransactionHistory func
helloscoopa Jan 5, 2023
fe75304
Remove waitForExpect
helloscoopa Jan 5, 2023
3db0c8f
Merge branch 'main' into dilshan/add-more-filter-options-to-accounthi…
helloscoopa Jan 5, 2023
f4d6b54
Update txId format regex
helloscoopa Jan 5, 2023
8789df4
Break getTransactionHistory into few functions
helloscoopa Jan 5, 2023
11c1e50
Make params optional
helloscoopa Jan 6, 2023
0a7aeb4
Merge branch 'main' into dilshan/add-more-filter-options-to-accounthi…
helloscoopa Jan 6, 2023
892283a
Merge branch 'main' into dilshan/add-more-filter-options-to-accounthi…
helloscoopa Jan 17, 2023
c16fb55
Merge branch 'main' into dilshan/add-more-filter-options-to-accounthi…
helloscoopa Jan 19, 2023
b45912d
Merge branch 'main' into dilshan/add-more-filter-options-to-accounthi…
helloscoopa Jan 19, 2023
277ccef
Merge branch 'dilshan/add-more-filter-options-to-accounthistorycount'…
helloscoopa Jan 19, 2023
2388960
Use updated accounthistorycount rpc
helloscoopa Jan 19, 2023
a47a0cf
Merge branch 'main' into dilshan/consortium-tx-history-api
helloscoopa Jan 19, 2023
cc9ab24
Update mintTokens params
helloscoopa Jan 19, 2023
19ee8f0
Merge branch 'main' into dilshan/consortium-tx-history-api
helloscoopa Jan 20, 2023
9277ab9
Merge branch 'main' into dilshan/consortium-tx-history-api
helloscoopa Jan 20, 2023
88bd9fe
Fix merge conflicts
helloscoopa Jan 22, 2023
9e171c7
Use PaginationQuery
helloscoopa Jan 25, 2023
b340fbf
Merge branch 'main' into dilshan/consortium-tx-history-api
helloscoopa Jan 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 333 additions & 1 deletion apps/whale-api/src/module.api/consortium.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ConsortiumController } from './consortium.controller'
import { TestingGroup } from '@defichain/jellyfish-testing'
import { createTestingApp, stopTestingApp } from '../e2e.module'
import { createTestingApp, stopTestingApp, waitForIndexedHeight } from '../e2e.module'
import { NestFastifyApplication } from '@nestjs/platform-fastify'
import { StartFlags } from '@defichain/testcontainers'

Expand Down Expand Up @@ -186,3 +186,335 @@ describe('getAssetBreakdown', () => {
}])
})
})

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
const txIdMatcher = expect.stringMatching(/^[0-9a-f]{64}$/)
const startFlags: StartFlags[] = [{ name: 'regtest-minttoken-simulate-mainnet', value: 1 }]
const txIds: string[] = []

beforeAll(async () => {
await tGroup.start({ startFlags })
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<void> {
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, mintLimitDaily: string }>): Promise<void> {
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<void> {
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)

idBTC = await alice.token.getTokenId(symbolBTC)
idETH = await alice.token.getTokenId(symbolETH)

await setGovAttr({
'v0/params/feature/consortium': 'true',
[`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, [{
id: '01',
name: 'alice',
ownerAddress: accountAlice,
backingId: 'abc',
mintLimitDaily: '10',
mintLimit: '20'
}, {
id: '02',
name: 'bob',
ownerAddress: accountBob,
backingId: 'def,hij',
mintLimitDaily: '10',
mintLimit: '20'
}])

await setMemberInfo(idETH, [{
id: '01',
name: 'alice',
ownerAddress: accountAlice,
backingId: '',
mintLimitDaily: '20.00000000',
mintLimit: '40.00000000'
}, {
id: '02',
name: 'bob',
ownerAddress: accountBob,
backingId: 'lmn,opq',
mintLimitDaily: '20.00000000',
mintLimit: '40.00000000'
}])

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({ amounts: [`0.1@${symbolBTC}`] }))
txIds.push(await alice.rpc.token.burnTokens(`0.1@${symbolBTC}`, accountAlice))
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({ amounts: [`1@${symbolBTC}`] })
await alice.generate(1)

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({ amounts: [`4@${symbolBTC}`] })
await bob.generate(1)

await bob.rpc.token.burnTokens(`2@${symbolBTC}`, accountBob)
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(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')
})

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 () => {
const info = await controller.getTransactionHistory({ searchTerm: '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: 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(6)
})

it('should filter transactions with search term (owner address)', async () => {
const info = await controller.getTransactionHistory({ searchTerm: 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: 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(6)
})

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 })

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: 113 }
])
expect(info.total).toStrictEqual(1)
})

it('should limit transactions', async () => {
const info = await controller.getTransactionHistory({ limit: 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: 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(11)
})

it('should filter and limit transactions at the same time', async () => {
const info = await controller.getTransactionHistory({ searchTerm: accountAlice, limit: 2 })

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: 113 },
{ txId: txIdMatcher, type: 'Mint', member: 'alice', tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }], address: accountAlice, block: 112 }
])
expect(info.total).toStrictEqual(6)
})

it('should return empty list of transactions for not-found search term', async () => {
const info = await controller.getTransactionHistory({ searchTerm: 'not-found-term', limit: 20 })

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 height = await alice.container.getBlockCount()
await alice.generate(1)
await waitForIndexedHeight(app, height)

const info = await controller.getTransactionHistory({ searchTerm: txid, limit: 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 })
expect(page1).toStrictEqual({
transactions: [
{
type: 'Burn',
member: 'bob',
tokenAmounts: [{ token: 'dBTC', amount: '-2.00000000' }],
txId: txIdMatcher,
address: accountBob,
block: 115
},
{
type: 'Mint',
member: 'bob',
tokenAmounts: [{ token: 'dBTC', amount: '4.00000000' }],
txId: txIdMatcher,
address: accountBob,
block: 114
}
],
total: 11
})

const page2 = await controller.getTransactionHistory({ pageIndex: 1, limit: 2 })
expect(page2).toStrictEqual({
transactions: [
{
type: 'Burn',
member: 'alice',
tokenAmounts: [{ token: 'dETH', amount: '-1.00000000' }],
txId: txIdMatcher,
address: accountAlice,
block: 113
},
{
type: 'Mint',
member: 'alice',
tokenAmounts: [{ token: 'dETH', amount: '2.00000000' }],
txId: txIdMatcher,
address: accountAlice,
block: 112
}
],
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)

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)
})
})
34 changes: 32 additions & 2 deletions apps/whale-api/src/module.api/consortium.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Controller, Get } from '@nestjs/common'
import { Controller, ForbiddenException, Get, Query } from '@nestjs/common'
import { ConsortiumService } from './consortium.service'
import { ConsortiumTransactionResponse, AssetBreakdownInfo } from '@defichain/whale-api-client/dist/api/consortium'
import { SemaphoreCache } from '@defichain-apps/libs/caches'
import { AssetBreakdownInfo } from '@defichain/whale-api-client/dist/api/consortium'

@Controller('/consortium')
export class ConsortiumController {
Expand All @@ -10,6 +10,36 @@ export class ConsortiumController {
protected readonly cache: SemaphoreCache
) {}

/**
* Gets the transaction history of consortium members.
*
* @return {Promise<ConsortiumTransactionResponse>}
*/
@Get('/transactions')
async getTransactionHistory (
@Query() query: { pageIndex?: number, limit?: number, searchTerm?: string }
): Promise<ConsortiumTransactionResponse> {
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')
}
helloscoopa marked this conversation as resolved.
Show resolved Hide resolved

return await this.cache.get<ConsortiumTransactionResponse>(`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
}

/**
* Gets the asset breakdown information of consortium members.
*
Expand Down
Loading