From da31728feae0b63a6347921a43e2f3afa58bd4b1 Mon Sep 17 00:00:00 2001 From: canonbrother Date: Fri, 23 Jul 2021 14:49:15 +0800 Subject: [PATCH] added poolpair stats `priceRatio` and `totalLiquidityUsd` (#202) * fix * wip * add poolpair.service * added price ratio and totalLiqudityUsd * add poolpair get service test * revert config * revert mapTokenData * stub testPoolSwap on test * remove unuse comment * fix poolpair.test.ts * fix poolpair.e2e, refine PoolPairData * use static address for testpoolswap * get usdtdfi conv price by getpoolpair, remove testpoolswap * remove log * tight logic on checking poolpair token id * added DFI-token poolpair test * prchanges: rename tokenA/tokenB to ab * pr changes: remove mock, use created fixture * refactor poolpair.service.ts to sync instead of live fetching Co-authored-by: Fuxing Loh --- .idea/dictionaries/fuxing.xml | 1 + .../__tests__/api/poolpair.test.ts | 147 +++++++++++------- packages/whale-api-client/src/api/poolpair.ts | 9 +- src/module.api/_module.ts | 4 +- src/module.api/poolpair.controller.e2e.ts | 101 +++++++----- src/module.api/poolpair.controller.spec.ts | 130 +++++++++++----- src/module.api/poolpair.controller.ts | 68 ++++---- src/module.api/poolpair.service.ts | 91 +++++++++++ 8 files changed, 390 insertions(+), 161 deletions(-) create mode 100644 src/module.api/poolpair.service.ts diff --git a/.idea/dictionaries/fuxing.xml b/.idea/dictionaries/fuxing.xml index 07d0233e9..b1ebfb16f 100644 --- a/.idea/dictionaries/fuxing.xml +++ b/.idea/dictionaries/fuxing.xml @@ -124,6 +124,7 @@ paytxfee pooledtx poolpair + poolpairs previousblockhash prevout prevouts diff --git a/packages/whale-api-client/__tests__/api/poolpair.test.ts b/packages/whale-api-client/__tests__/api/poolpair.test.ts index b75295446..e95229cd5 100644 --- a/packages/whale-api-client/__tests__/api/poolpair.test.ts +++ b/packages/whale-api-client/__tests__/api/poolpair.test.ts @@ -1,8 +1,11 @@ import { MasterNodeRegTestContainer } from '@defichain/testcontainers' import { StubWhaleApiClient } from '../stub.client' import { StubService } from '../stub.service' -import { WhaleApiClient, WhaleApiException } from '../../src' -import { createPoolPair, createToken, addPoolLiquidity, getNewAddress, mintTokens } from '@defichain/testing' +import { ApiPagedResponse, WhaleApiClient, WhaleApiException } from '../../src' +import { addPoolLiquidity, createPoolPair, createToken, getNewAddress, mintTokens } from '@defichain/testing' +import { PoolPairService } from '@src/module.api/poolpair.service' +import { PoolPairData } from '@whale-api-client/api/poolpair' +import waitForExpect from 'wait-for-expect' let container: MasterNodeRegTestContainer let service: StubService @@ -18,81 +21,107 @@ beforeAll(async () => { await container.waitForWalletCoinbaseMaturity() await service.start() - const tokens = ['A', 'B', 'C', 'D', 'E', 'F'] + await setup() + + await waitForExpect(() => { + // @ts-expect-error + expect(service.app?.get(PoolPairService).USDT_PER_DFI).toBeDefined() + }, 61000) // 60 seconds interval +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +async function setup (): Promise { + const tokens = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] for (const token of tokens) { await container.waitForWalletBalanceGTE(110) await createToken(container, token) await mintTokens(container, token) } - await createPoolPair(container, 'A', 'B') - await createPoolPair(container, 'A', 'C') - await createPoolPair(container, 'A', 'D') - await createPoolPair(container, 'A', 'E') - await createPoolPair(container, 'A', 'F') - await createPoolPair(container, 'B', 'C') - await createPoolPair(container, 'B', 'D') - await createPoolPair(container, 'B', 'E') - await container.generate(1) + await createPoolPair(container, 'A', 'DFI') + await createPoolPair(container, 'B', 'DFI') + await createPoolPair(container, 'C', 'DFI') + await createPoolPair(container, 'D', 'DFI') + await createPoolPair(container, 'E', 'DFI') + await createPoolPair(container, 'F', 'DFI') + await createPoolPair(container, 'G', 'DFI') + await createPoolPair(container, 'H', 'DFI') await addPoolLiquidity(container, { tokenA: 'A', amountA: 100, - tokenB: 'B', + tokenB: 'DFI', amountB: 200, shareAddress: await getNewAddress(container) }) await addPoolLiquidity(container, { - tokenA: 'A', + tokenA: 'B', amountA: 50, - tokenB: 'C', + tokenB: 'DFI', amountB: 300, shareAddress: await getNewAddress(container) }) await addPoolLiquidity(container, { - tokenA: 'A', + tokenA: 'C', amountA: 90, - tokenB: 'D', + tokenB: 'DFI', amountB: 360, shareAddress: await getNewAddress(container) }) - await container.generate(1) -}) -afterAll(async () => { - try { - await service.stop() - } finally { - await container.stop() - } -}) + // dexUsdtDfi setup + await createToken(container, 'USDT') + await createPoolPair(container, 'USDT', 'DFI') + await mintTokens(container, 'USDT') + await addPoolLiquidity(container, { + tokenA: 'USDT', + amountA: 1000, + tokenB: 'DFI', + amountB: 431.51288, + shareAddress: await getNewAddress(container) + }) +} describe('list', () => { it('should list', async () => { - const response = await client.poolpair.list(30) + const response: ApiPagedResponse = await client.poolpair.list(30) - expect(response.length).toStrictEqual(8) + expect(response.length).toStrictEqual(9) expect(response.hasNext).toStrictEqual(false) expect(response[1]).toStrictEqual({ - id: '8', - symbol: 'A-C', - name: 'A-C', + id: '10', + symbol: 'B-DFI', + name: 'B-Default Defi token', status: true, tokenA: { - id: '1', + id: '2', reserve: '50', blockCommission: '0' }, tokenB: { - id: '3', + id: '0', reserve: '300', blockCommission: '0' }, commission: '0', - totalLiquidity: '122.47448713', + totalLiquidity: { + token: '122.47448713', + usd: '1390.456752' + }, tradeEnabled: true, ownerAddress: expect.any(String), + priceRatio: { + ab: '0.16666666', + ba: '6' + }, rewardPct: '0', creation: { tx: expect.any(String), @@ -102,42 +131,43 @@ describe('list', () => { }) it('should list with pagination', async () => { - const first = await client.poolpair.list(3) - expect(first.length).toStrictEqual(3) + const first = await client.poolpair.list(4) + expect(first.length).toStrictEqual(4) expect(first.hasNext).toStrictEqual(true) - expect(first.nextToken).toStrictEqual('9') + expect(first.nextToken).toStrictEqual('12') - expect(first[0].symbol).toStrictEqual('A-B') - expect(first[1].symbol).toStrictEqual('A-C') - expect(first[2].symbol).toStrictEqual('A-D') + expect(first[0].symbol).toStrictEqual('A-DFI') + expect(first[1].symbol).toStrictEqual('B-DFI') + expect(first[2].symbol).toStrictEqual('C-DFI') + expect(first[3].symbol).toStrictEqual('D-DFI') const next = await client.paginate(first) - expect(next.length).toStrictEqual(3) + expect(next.length).toStrictEqual(4) expect(next.hasNext).toStrictEqual(true) - expect(next.nextToken).toStrictEqual('12') + expect(next.nextToken).toStrictEqual('16') - expect(next[0].symbol).toStrictEqual('A-E') - expect(next[1].symbol).toStrictEqual('A-F') - expect(next[2].symbol).toStrictEqual('B-C') + expect(next[0].symbol).toStrictEqual('E-DFI') + expect(next[1].symbol).toStrictEqual('F-DFI') + expect(next[2].symbol).toStrictEqual('G-DFI') + expect(next[3].symbol).toStrictEqual('H-DFI') const last = await client.paginate(next) - expect(last.length).toStrictEqual(2) + expect(last.length).toStrictEqual(1) expect(last.hasNext).toStrictEqual(false) expect(last.nextToken).toBeUndefined() - expect(last[0].symbol).toStrictEqual('B-D') - expect(last[1].symbol).toStrictEqual('B-E') + expect(last[0].symbol).toStrictEqual('USDT-DFI') }) }) describe('get', () => { it('should get', async () => { - const response = await client.poolpair.get('7') + const response: PoolPairData = await client.poolpair.get('9') expect(response).toStrictEqual({ - id: '7', - symbol: 'A-B', - name: 'A-B', + id: '9', + symbol: 'A-DFI', + name: 'A-Default Defi token', status: true, tokenA: { id: expect.any(String), @@ -150,9 +180,16 @@ describe('get', () => { blockCommission: '0' }, commission: '0', - totalLiquidity: '141.42135623', + totalLiquidity: { + token: '141.42135623', + usd: '926.971168' + }, tradeEnabled: true, ownerAddress: expect.any(String), + priceRatio: { + ab: '0.5', + ba: '2' + }, rewardPct: '0', creation: { tx: expect.any(String), @@ -164,7 +201,7 @@ describe('get', () => { it('should throw error as numeric string is expected', async () => { expect.assertions(2) try { - await client.poolpair.get('A-B') + await client.poolpair.get('A-DFI') } catch (err) { expect(err).toBeInstanceOf(WhaleApiException) expect(err.error).toStrictEqual({ @@ -172,7 +209,7 @@ describe('get', () => { type: 'BadRequest', at: expect.any(Number), message: 'Validation failed (numeric string is expected)', - url: '/v0/regtest/poolpairs/A-B' + url: '/v0/regtest/poolpairs/A-DFI' }) } }) diff --git a/packages/whale-api-client/src/api/poolpair.ts b/packages/whale-api-client/src/api/poolpair.ts index f1fc51c5f..80ad81cb4 100644 --- a/packages/whale-api-client/src/api/poolpair.ts +++ b/packages/whale-api-client/src/api/poolpair.ts @@ -45,8 +45,15 @@ export interface PoolPairData { reserve: string // BigNumber blockCommission: string // BigNumber } + priceRatio: { + ab: string // BigNumber + ba: string // BigNumber + } commission: string // BigNumber - totalLiquidity: string // BigNumber + totalLiquidity: { + token: string // BigNumber + usd?: string // BigNumber + } tradeEnabled: boolean ownerAddress: string rewardPct: string // BigNumber diff --git a/src/module.api/_module.ts b/src/module.api/_module.ts index 12591f247..5e4ded421 100644 --- a/src/module.api/_module.ts +++ b/src/module.api/_module.ts @@ -6,6 +6,7 @@ import { TransactionsController } from '@src/module.api/transaction.controller' import { ApiValidationPipe } from '@src/module.api/pipes/api.validation.pipe' import { AddressController } from '@src/module.api/address.controller' import { PoolPairController } from '@src/module.api/poolpair.controller' +import { PoolPairService } from '@src/module.api/poolpair.service' import { DeFiDCache } from '@src/module.api/cache/defid.cache' import { NetworkGuard } from '@src/module.api/guards/network.guard' import { ExceptionInterceptor } from '@src/module.api/interceptors/exception.interceptor' @@ -33,7 +34,8 @@ import { MasternodesController } from '@src/module.api/masternode.controller' { provide: APP_GUARD, useClass: NetworkGuard }, { provide: APP_INTERCEPTOR, useClass: ResponseInterceptor }, { provide: APP_INTERCEPTOR, useClass: ExceptionInterceptor }, - DeFiDCache + DeFiDCache, + PoolPairService ] }) export class ApiModule { diff --git a/src/module.api/poolpair.controller.e2e.ts b/src/module.api/poolpair.controller.e2e.ts index 2298d1b07..73d5ee0bd 100644 --- a/src/module.api/poolpair.controller.e2e.ts +++ b/src/module.api/poolpair.controller.e2e.ts @@ -2,8 +2,9 @@ import { PoolPairController } from '@src/module.api/poolpair.controller' import { MasterNodeRegTestContainer } from '@defichain/testcontainers' import { NestFastifyApplication } from '@nestjs/platform-fastify' import { createTestingApp, stopTestingApp, waitForIndexedHeight } from '@src/e2e.module' -import { createPoolPair, createToken, addPoolLiquidity, getNewAddress, mintTokens } from '@defichain/testing' +import { addPoolLiquidity, createPoolPair, createToken, getNewAddress, mintTokens } from '@defichain/testing' import { NotFoundException } from '@nestjs/common' +import { PoolPairService } from '@src/module.api/poolpair.service' const container = new MasterNodeRegTestContainer() let app: NestFastifyApplication @@ -20,6 +21,15 @@ beforeAll(async () => { await waitForIndexedHeight(app, 100) + await setup() + await app.get(PoolPairService).syncDfiUsdPair() +}) + +afterAll(async () => { + await stopTestingApp(container, app) +}) + +async function setup (): Promise { const tokens = ['A', 'B', 'C', 'D', 'E', 'F'] for (const token of tokens) { @@ -27,42 +37,47 @@ beforeAll(async () => { await createToken(container, token) await mintTokens(container, token) } - await createPoolPair(container, 'A', 'B') - await createPoolPair(container, 'A', 'C') - await createPoolPair(container, 'A', 'D') - await createPoolPair(container, 'A', 'E') - await createPoolPair(container, 'A', 'F') - await createPoolPair(container, 'B', 'C') - await createPoolPair(container, 'B', 'D') - await createPoolPair(container, 'B', 'E') - await container.generate(1) + await createPoolPair(container, 'A', 'DFI') + await createPoolPair(container, 'B', 'DFI') + await createPoolPair(container, 'C', 'DFI') + await createPoolPair(container, 'D', 'DFI') + await createPoolPair(container, 'E', 'DFI') + await createPoolPair(container, 'F', 'DFI') await addPoolLiquidity(container, { tokenA: 'A', amountA: 100, - tokenB: 'B', + tokenB: 'DFI', amountB: 200, shareAddress: await getNewAddress(container) }) await addPoolLiquidity(container, { - tokenA: 'A', + tokenA: 'B', amountA: 50, - tokenB: 'C', + tokenB: 'DFI', amountB: 300, shareAddress: await getNewAddress(container) }) await addPoolLiquidity(container, { - tokenA: 'A', + tokenA: 'C', amountA: 90, - tokenB: 'D', + tokenB: 'DFI', amountB: 360, shareAddress: await getNewAddress(container) }) -}) -afterAll(async () => { - await stopTestingApp(container, app) -}) + // dexUsdtDfi setup + await createToken(container, 'USDT') + await createPoolPair(container, 'USDT', 'DFI') + await mintTokens(container, 'USDT') + await addPoolLiquidity(container, { + tokenA: 'USDT', + amountA: 1000, + tokenB: 'DFI', + amountB: 431.51288, + shareAddress: await getNewAddress(container) + }) +} describe('list', () => { it('should list', async () => { @@ -70,28 +85,35 @@ describe('list', () => { size: 30 }) - expect(response.data.length).toStrictEqual(8) + expect(response.data.length).toStrictEqual(7) expect(response.page).toBeUndefined() expect(response.data[1]).toStrictEqual({ id: '8', - symbol: 'A-C', - name: 'A-C', + symbol: 'B-DFI', + name: 'B-Default Defi token', status: true, tokenA: { - id: '1', + id: '2', reserve: '50', blockCommission: '0' }, tokenB: { - id: '3', + id: '0', reserve: '300', blockCommission: '0' }, commission: '0', - totalLiquidity: '122.47448713', + totalLiquidity: { + token: '122.47448713', + usd: '1390.456752' + }, tradeEnabled: true, ownerAddress: expect.any(String), + priceRatio: { + ab: '0.16666666', + ba: '6' + }, rewardPct: '0', customRewards: undefined, creation: { @@ -107,22 +129,20 @@ describe('list', () => { }) expect(first.data.length).toStrictEqual(2) expect(first.page?.next).toStrictEqual('8') - expect(first.data[0].symbol).toStrictEqual('A-B') - expect(first.data[1].symbol).toStrictEqual('A-C') + expect(first.data[0].symbol).toStrictEqual('A-DFI') + expect(first.data[1].symbol).toStrictEqual('B-DFI') const next = await controller.list({ size: 10, next: first.page?.next }) - expect(next.data.length).toStrictEqual(6) + expect(next.data.length).toStrictEqual(5) expect(next.page?.next).toBeUndefined() - expect(next.data[0].symbol).toStrictEqual('A-D') - expect(next.data[1].symbol).toStrictEqual('A-E') - expect(next.data[2].symbol).toStrictEqual('A-F') - expect(next.data[3].symbol).toStrictEqual('B-C') - expect(next.data[4].symbol).toStrictEqual('B-D') - expect(next.data[5].symbol).toStrictEqual('B-E') + expect(next.data[0].symbol).toStrictEqual('C-DFI') + expect(next.data[1].symbol).toStrictEqual('D-DFI') + expect(next.data[2].symbol).toStrictEqual('E-DFI') + expect(next.data[3].symbol).toStrictEqual('F-DFI') }) it('should list with undefined next pagination', async () => { @@ -142,8 +162,8 @@ describe('get', () => { expect(response).toStrictEqual({ id: '7', - symbol: 'A-B', - name: 'A-B', + symbol: 'A-DFI', + name: 'A-Default Defi token', status: true, tokenA: { id: expect.any(String), @@ -156,9 +176,16 @@ describe('get', () => { blockCommission: '0' }, commission: '0', - totalLiquidity: '141.42135623', + totalLiquidity: { + token: '141.42135623', + usd: '926.971168' + }, tradeEnabled: true, ownerAddress: expect.any(String), + priceRatio: { + ab: '0.5', + ba: '2' + }, rewardPct: '0', customRewards: undefined, creation: { diff --git a/src/module.api/poolpair.controller.spec.ts b/src/module.api/poolpair.controller.spec.ts index ff24a9c04..cb0627618 100644 --- a/src/module.api/poolpair.controller.spec.ts +++ b/src/module.api/poolpair.controller.spec.ts @@ -2,9 +2,11 @@ import { Test, TestingModule } from '@nestjs/testing' import { MasterNodeRegTestContainer } from '@defichain/testcontainers' import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' import { PoolPairController } from '@src/module.api/poolpair.controller' -import { createPoolPair, createToken, addPoolLiquidity, getNewAddress, mintTokens } from '@defichain/testing' +import { PoolPairService } from '@src/module.api/poolpair.service' +import { addPoolLiquidity, createPoolPair, createToken, getNewAddress, mintTokens } from '@defichain/testing' import { CacheModule, NotFoundException } from '@nestjs/common' import { DeFiDCache } from './cache/defid.cache' +import { ConfigService } from '@nestjs/config' const container = new MasterNodeRegTestContainer() let controller: PoolPairController @@ -22,54 +24,92 @@ beforeAll(async () => { controllers: [PoolPairController], providers: [ { provide: JsonRpcClient, useValue: client }, - DeFiDCache + DeFiDCache, + PoolPairService, + ConfigService ] }).compile() controller = app.get(PoolPairController) - const tokens = ['A', 'B', 'C', 'D', 'E', 'F'] + await setup() + await app.get(PoolPairService).syncDfiUsdPair() +}) + +afterAll(async () => { + await container.stop() +}) + +async function setup (): Promise { + const tokens = ['USDT', 'B', 'C', 'D', 'E', 'F'] for (const token of tokens) { await container.waitForWalletBalanceGTE(110) await createToken(container, token) await mintTokens(container, token) } - await createPoolPair(container, 'A', 'B') - await createPoolPair(container, 'A', 'C') - await createPoolPair(container, 'A', 'D') - await createPoolPair(container, 'A', 'E') - await createPoolPair(container, 'A', 'F') - await createPoolPair(container, 'B', 'C') - await createPoolPair(container, 'B', 'D') - await createPoolPair(container, 'B', 'E') - await container.generate(1) + + for (const token of tokens) { + await createPoolPair(container, token, 'DFI') + } await addPoolLiquidity(container, { - tokenA: 'A', + tokenA: 'USDT', amountA: 100, - tokenB: 'B', + tokenB: 'DFI', amountB: 200, shareAddress: await getNewAddress(container) }) await addPoolLiquidity(container, { - tokenA: 'A', + tokenA: 'B', amountA: 50, - tokenB: 'C', + tokenB: 'DFI', amountB: 300, shareAddress: await getNewAddress(container) }) await addPoolLiquidity(container, { - tokenA: 'A', + tokenA: 'C', amountA: 90, - tokenB: 'D', + tokenB: 'DFI', amountB: 360, shareAddress: await getNewAddress(container) }) -}) + await addPoolLiquidity(container, { + tokenA: 'D', + amountA: 51.1, + tokenB: 'DFI', + amountB: 144.54134, + shareAddress: await getNewAddress(container) + }) +} -afterAll(async () => { - await container.stop() +it('should resolve tvl for all poolpair', async () => { + const response = await controller.list({ size: 5 }) + + expect(response.data[0].totalLiquidity).toStrictEqual({ + token: '141.42135623', + usd: '200' + }) + + expect(response.data[1].totalLiquidity).toStrictEqual({ + token: '122.47448713', + usd: '300' + }) + + expect(response.data[2].totalLiquidity).toStrictEqual({ + token: '180', + usd: '360' + }) + + expect(response.data[3].totalLiquidity).toStrictEqual({ + token: '85.94220426', + usd: '144.54134' + }) + + expect(response.data[4].totalLiquidity).toStrictEqual({ + token: '0', + usd: '0' + }) }) describe('list', () => { @@ -78,28 +118,35 @@ describe('list', () => { size: 30 }) - expect(response.data.length).toStrictEqual(8) + expect(response.data.length).toStrictEqual(6) expect(response.page).toBeUndefined() expect(response.data[1]).toStrictEqual({ id: '8', - symbol: 'A-C', - name: 'A-C', + symbol: 'B-DFI', + name: 'B-Default Defi token', status: true, tokenA: { - id: '1', + id: '2', reserve: '50', blockCommission: '0' }, tokenB: { - id: '3', + id: '0', reserve: '300', blockCommission: '0' }, commission: '0', - totalLiquidity: '122.47448713', + totalLiquidity: { + token: '122.47448713', + usd: '300' + }, tradeEnabled: true, ownerAddress: expect.any(String), + priceRatio: { + ab: '0.16666666', + ba: '6' + }, rewardPct: '0', customRewards: undefined, creation: { @@ -115,22 +162,20 @@ describe('list', () => { }) expect(first.data.length).toStrictEqual(2) expect(first.page?.next).toStrictEqual('8') - expect(first.data[0].symbol).toStrictEqual('A-B') - expect(first.data[1].symbol).toStrictEqual('A-C') + expect(first.data[0].symbol).toStrictEqual('USDT-DFI') + expect(first.data[1].symbol).toStrictEqual('B-DFI') const next = await controller.list({ size: 10, next: first.page?.next }) - expect(next.data.length).toStrictEqual(6) + expect(next.data.length).toStrictEqual(4) expect(next.page?.next).toBeUndefined() - expect(next.data[0].symbol).toStrictEqual('A-D') - expect(next.data[1].symbol).toStrictEqual('A-E') - expect(next.data[2].symbol).toStrictEqual('A-F') - expect(next.data[3].symbol).toStrictEqual('B-C') - expect(next.data[4].symbol).toStrictEqual('B-D') - expect(next.data[5].symbol).toStrictEqual('B-E') + expect(next.data[0].symbol).toStrictEqual('C-DFI') + expect(next.data[1].symbol).toStrictEqual('D-DFI') + expect(next.data[2].symbol).toStrictEqual('E-DFI') + expect(next.data[3].symbol).toStrictEqual('F-DFI') }) it('should list with undefined next pagination', async () => { @@ -150,8 +195,8 @@ describe('get', () => { expect(response).toStrictEqual({ id: '7', - symbol: 'A-B', - name: 'A-B', + symbol: 'USDT-DFI', + name: 'USDT-Default Defi token', status: true, tokenA: { id: expect.any(String), @@ -164,9 +209,16 @@ describe('get', () => { blockCommission: '0' }, commission: '0', - totalLiquidity: '141.42135623', + totalLiquidity: { + token: '141.42135623', + usd: '200' + }, tradeEnabled: true, ownerAddress: expect.any(String), + priceRatio: { + ab: '0.5', + ba: '2' + }, rewardPct: '0', customRewards: undefined, creation: { diff --git a/src/module.api/poolpair.controller.ts b/src/module.api/poolpair.controller.ts index 5af86512a..b586d4bf0 100644 --- a/src/module.api/poolpair.controller.ts +++ b/src/module.api/poolpair.controller.ts @@ -1,16 +1,19 @@ -import { NotFoundException, Controller, Get, Query, Param, ParseIntPipe } from '@nestjs/common' +import { Controller, Get, NotFoundException, Param, ParseIntPipe, Query } from '@nestjs/common' import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' import { ApiPagedResponse } from '@src/module.api/_core/api.paged.response' import { DeFiDCache } from '@src/module.api/cache/defid.cache' import { PoolPairData } from '@whale-api-client/api/poolpair' import { PaginationQuery } from '@src/module.api/_core/api.query' +import { PoolPairService } from './poolpair.service' +import BigNumber from 'bignumber.js' import { PoolPairInfo } from '@defichain/jellyfish-api-core/dist/category/poolpair' @Controller('/v0/:network/poolpairs') export class PoolPairController { constructor ( protected readonly rpcClient: JsonRpcClient, - protected readonly deFiDCache: DeFiDCache + protected readonly deFiDCache: DeFiDCache, + private readonly poolPairService: PoolPairService ) { } @@ -24,17 +27,17 @@ export class PoolPairController { async list ( @Query() query: PaginationQuery ): Promise> { - const poolPairResult = await this.rpcClient.poolpair.listPoolPairs({ + const result = await this.rpcClient.poolpair.listPoolPairs({ start: query.next !== undefined ? Number(query.next) : 0, including_start: query.next === undefined, // TODO(fuxingloh): open issue at DeFiCh/ain, rpc_accounts.cpp#388 limit: query.size }, true) - const poolPairInfosDto = Object.entries(poolPairResult).map(([id, value]) => { - return mapPoolPair(id, value) - }).sort(a => Number.parseInt(a.id)) - - return ApiPagedResponse.of(poolPairInfosDto, query.size, item => { + const items = Object.entries(result).map(([id, info]) => { + const totalLiquidityUsd = this.poolPairService.getTotalLiquidityUsd(info) + return mapPoolPair(id, info, totalLiquidityUsd) + }) + return ApiPagedResponse.of(items, query.size, item => { return item.id }) } @@ -49,35 +52,44 @@ export class PoolPairController { if (info === undefined) { throw new NotFoundException('Unable to find poolpair') } - return mapPoolPair(String(id), info) + + const totalLiquidityUsd = this.poolPairService.getTotalLiquidityUsd(info) + return mapPoolPair(String(id), info, totalLiquidityUsd) } } -function mapPoolPair (id: string, poolPairInfo: PoolPairInfo): PoolPairData { +export function mapPoolPair (id: string, info: PoolPairInfo, totalLiquidityUsd?: BigNumber): PoolPairData { return { - id, - symbol: poolPairInfo.symbol, - name: poolPairInfo.name, - status: poolPairInfo.status, + id: id, + symbol: info.symbol, + name: info.name, + status: info.status, tokenA: { - id: poolPairInfo.idTokenA, - reserve: poolPairInfo.reserveA.toFixed(), - blockCommission: poolPairInfo.blockCommissionA.toFixed() + id: info.idTokenA, + reserve: info.reserveA.toFixed(), + blockCommission: info.blockCommissionA.toFixed() }, tokenB: { - id: poolPairInfo.idTokenB, - reserve: poolPairInfo.reserveB.toFixed(), - blockCommission: poolPairInfo.blockCommissionB.toFixed() + id: info.idTokenB, + reserve: info.reserveB.toFixed(), + blockCommission: info.blockCommissionB.toFixed() + }, + priceRatio: { + ab: info['reserveA/reserveB'] instanceof BigNumber ? info['reserveA/reserveB'].toFixed() : info['reserveA/reserveB'], + ba: info['reserveB/reserveA'] instanceof BigNumber ? info['reserveB/reserveA'].toFixed() : info['reserveB/reserveA'] + }, + commission: info.commission.toFixed(), + totalLiquidity: { + token: info.totalLiquidity.toFixed(), + usd: totalLiquidityUsd?.toFixed() }, - commission: poolPairInfo.commission.toFixed(), - totalLiquidity: poolPairInfo.totalLiquidity.toFixed(), - tradeEnabled: poolPairInfo.tradeEnabled, - ownerAddress: poolPairInfo.ownerAddress, - rewardPct: poolPairInfo.rewardPct.toFixed(), - customRewards: poolPairInfo.customRewards, + tradeEnabled: info.tradeEnabled, + ownerAddress: info.ownerAddress, + rewardPct: info.rewardPct.toFixed(), + customRewards: info.customRewards, creation: { - tx: poolPairInfo.creationTx, - height: poolPairInfo.creationHeight.toNumber() + tx: info.creationTx, + height: info.creationHeight.toNumber() } } } diff --git a/src/module.api/poolpair.service.ts b/src/module.api/poolpair.service.ts new file mode 100644 index 000000000..ffba4e8cd --- /dev/null +++ b/src/module.api/poolpair.service.ts @@ -0,0 +1,91 @@ +import { Injectable, Logger } from '@nestjs/common' +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' +import BigNumber from 'bignumber.js' +import { PoolPairInfo } from '@defichain/jellyfish-api-core/dist/category/poolpair' +import { Interval } from '@nestjs/schedule' + +@Injectable() +export class PoolPairService { + private readonly logger = new Logger(PoolPairService.name) + private USDT_PER_DFI: BigNumber | undefined + private syncing: boolean = false + + constructor ( + protected readonly rpcClient: JsonRpcClient + ) { + } + + @Interval(60000) + private async sync (): Promise { + if (this.syncing) return + + try { + this.syncing = true + await this.syncDfiUsdPair() + } catch (err) { + this.logger.error('sync error', err) + } finally { + this.syncing = false + } + } + + async syncDfiUsdPair (): Promise { + const pair = await this.getPoolPair('DFI', 'USDT') + if (pair === undefined) { + return + } + + if (pair.idTokenA === '0') { + this.USDT_PER_DFI = new BigNumber(pair['reserveB/reserveA']) + } else if (pair.idTokenB === '0') { + this.USDT_PER_DFI = new BigNumber(pair['reserveA/reserveB']) + } + } + + /** + * Get PoolPair where the order of token doesn't matter + */ + private async getPoolPair (a: string, b: string): Promise { + try { + const result = await this.rpcClient.poolpair.getPoolPair(`${a}-${b}`, true) + if (Object.values(result).length > 0) { + return Object.values(result)[0] + } + } catch (err) { + if (err?.payload?.message !== 'Pool not found') { + throw err + } + } + + try { + const result = await this.rpcClient.poolpair.getPoolPair(`${b}-${a}`, true) + if (Object.values(result).length > 0) { + return Object.values(result)[0] + } + } catch (err) { + if (err?.payload?.message !== 'Pool not found') { + throw err + } + } + } + + /** + * TODO(fuxingloh): graph based matrix resolution + * Currently implemented with fix pair derivation + * Ideally should use vertex directed graph where we can always find total liquidity if it can be resolved. + */ + getTotalLiquidityUsd (info: PoolPairInfo): BigNumber | undefined { + if (this.USDT_PER_DFI === undefined) { + return + } + + const [a, b] = info.symbol.split('-') + if (a === 'DFI') { + return info.reserveA.multipliedBy(2).multipliedBy(this.USDT_PER_DFI) + } + + if (b === 'DFI') { + return info.reserveB.multipliedBy(2).multipliedBy(this.USDT_PER_DFI) + } + } +}