From 252fa2bd7a7ca1e301c3f29c48d713a1138e71ba Mon Sep 17 00:00:00 2001 From: Eli <31790206+eli-lim@users.noreply.github.com> Date: Fri, 20 May 2022 15:44:17 +0800 Subject: [PATCH] chore(whale-migration): whale sync (#1449) * chore(whale-migration): whale sync - Fix imports - Set source code to be at parity with whale repo as of 19/05/2022 16:35 GMT+8 * chore(whale-migration): revert minor changes --- apps/whale/src/module.api/_module.ts | 5 +- .../src/module.api/address.controller.e2e.ts | 152 ++++++++ .../src/module.api/address.controller.ts | 50 ++- .../whale/src/module.api/cache/defid.cache.ts | 52 ++- .../src/module.api/cache/global.cache.ts | 3 +- .../loan.collateral.controller.e2e.ts | 2 +- .../module.api/loan.scheme.controller.e2e.ts | 6 +- .../module.api/loan.token.controller.e2e.ts | 2 +- .../module.api/loan.vault.controller.e2e.ts | 4 +- .../module.api/masternode.controller.e2e.ts | 2 +- .../src/module.api/masternode.service.ts | 2 +- .../src/module.api/poolpair.controller.e2e.ts | 326 ++++++++++++++++-- .../src/module.api/poolpair.controller.ts | 33 +- .../src/module.api/poolpair.prices.service.ts | 118 +++++++ apps/whale/src/module.api/poolpair.service.ts | 227 +----------- .../poolswap.pathfinding.service.ts | 281 +++++++++++++++ .../src/module.api/rawtx.controller.e2e.ts | 48 ++- apps/whale/src/module.api/rpc.controller.ts | 4 +- apps/whale/src/module.api/stats.controller.ts | 42 ++- .../module.api/transaction.controller.e2e.ts | 2 +- .../provider.level/level.database.spec.ts | 2 +- .../provider.memory/memory.database.spec.ts | 2 +- .../module.indexer/model/script.activity.ts | 2 +- .../module.indexer/model/script.unspent.ts | 2 +- .../src/module.indexer/model/transaction.ts | 2 +- .../module.indexer/model/transaction.vin.ts | 2 +- .../module.indexer/model/transaction.vout.ts | 2 +- 27 files changed, 1044 insertions(+), 331 deletions(-) create mode 100644 apps/whale/src/module.api/poolpair.prices.service.ts create mode 100644 apps/whale/src/module.api/poolswap.pathfinding.service.ts diff --git a/apps/whale/src/module.api/_module.ts b/apps/whale/src/module.api/_module.ts index 2d4c7a5a18..59a45ace0b 100644 --- a/apps/whale/src/module.api/_module.ts +++ b/apps/whale/src/module.api/_module.ts @@ -6,7 +6,7 @@ import { TransactionController } from './transaction.controller' import { ApiValidationPipe } from './pipes/api.validation.pipe' import { AddressController } from './address.controller' import { PoolPairController } from './poolpair.controller' -import { PoolPairService, PoolSwapPathFindingService } from './poolpair.service' +import { PoolPairService } from './poolpair.service' import { MasternodeService } from './masternode.service' import { DeFiDCache } from './cache/defid.cache' import { SemaphoreCache } from './cache/semaphore.cache' @@ -29,6 +29,8 @@ import { FeeController } from './fee.controller' import { RawtxController } from './rawtx.controller' import { LoanController } from './loan.controller' import { LoanVaultService } from './loan.vault.service' +import { PoolSwapPathFindingService } from './poolswap.pathfinding.service' +import { PoolPairPricesService } from './poolpair.prices.service' /** * Exposed ApiModule for public interfacing @@ -67,6 +69,7 @@ import { LoanVaultService } from './loan.vault.service' SemaphoreCache, PoolPairService, PoolSwapPathFindingService, + PoolPairPricesService, MasternodeService, LoanVaultService, { diff --git a/apps/whale/src/module.api/address.controller.e2e.ts b/apps/whale/src/module.api/address.controller.e2e.ts index d66461c5ef..fe8f5b1624 100644 --- a/apps/whale/src/module.api/address.controller.e2e.ts +++ b/apps/whale/src/module.api/address.controller.e2e.ts @@ -8,6 +8,7 @@ import { RpcApiError } from '@defichain/jellyfish-api-core' import { Testing } from '@defichain/jellyfish-testing' import { ForbiddenException } from '@nestjs/common' import BigNumber from 'bignumber.js' +import { RegTestFoundationKeys } from '@defichain/jellyfish-network' const container = new MasterNodeRegTestContainer() let app: NestFastifyApplication @@ -209,6 +210,157 @@ describe('listAccountHistory', () => { }) }) +describe('getAccount', () => { + beforeAll(async () => { + await container.start() + await container.waitForWalletCoinbaseMaturity() + + colAddr = await testing.generateAddress() + usdcAddr = await testing.generateAddress() + poolAddr = await testing.generateAddress() + emptyAddr = await testing.generateAddress() + + await testing.token.dfi({ address: colAddr, amount: 20000 }) + await testing.generate(1) + + await testing.token.create({ symbol: 'USDC', collateralAddress: colAddr }) + await testing.generate(1) + + await testing.token.mint({ symbol: 'USDC', amount: 10000 }) + await testing.generate(1) + + await testing.rpc.account.accountToAccount(colAddr, { [usdcAddr]: '10000@USDC' }) + await testing.generate(1) + + await testing.rpc.poolpair.createPoolPair({ + tokenA: 'DFI', + tokenB: 'USDC', + commission: 0, + status: true, + ownerAddress: poolAddr + }) + await testing.generate(1) + + const poolPairsKeys = Object.keys(await testing.rpc.poolpair.listPoolPairs()) + expect(poolPairsKeys.length).toStrictEqual(1) + dfiUsdc = poolPairsKeys[0] + + // set LP_SPLIT, make LM gain rewards, MANDATORY + // ensure `no_rewards` flag turned on + // ensure do not get response without txid + await testing.container.call('setgov', [{ LP_SPLITS: { [dfiUsdc]: 1.0 } }]) + await container.generate(1) + + await testing.rpc.poolpair.addPoolLiquidity({ + [colAddr]: '5000@DFI', + [usdcAddr]: '5000@USDC' + }, poolAddr) + await testing.generate(1) + + await testing.rpc.poolpair.poolSwap({ + from: colAddr, + tokenFrom: 'DFI', + amountFrom: 555, + to: usdcAddr, + tokenTo: 'USDC' + }) + await testing.generate(1) + + await testing.rpc.poolpair.removePoolLiquidity(poolAddr, '2@DFI-USDC') + await testing.generate(1) + + // for testing same block pagination + await testing.token.create({ symbol: 'APE', collateralAddress: colAddr }) + await testing.generate(1) + + await testing.token.create({ symbol: 'CAT', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'DOG', collateralAddress: colAddr }) + await testing.generate(1) + + await testing.token.create({ symbol: 'ELF', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'FOX', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'RAT', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'BEE', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'COW', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'OWL', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'ELK', collateralAddress: colAddr }) + await testing.generate(1) + + await testing.token.create({ symbol: 'PIG', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'KOI', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'FLY', collateralAddress: colAddr }) + await testing.generate(1) + + app = await createTestingApp(container) + controller = app.get(AddressController) + + await testing.generate(1) + }) + + afterAll(async () => { + await stopTestingApp(container, app) + }) + + it('should getAccount', async () => { + const history = await controller.listAccountHistory(colAddr, { size: 30 }) + for (const h of history.data) { + if (['sent', 'receive'].includes(h.type)) { + continue + } + const acc = await controller.getAccountHistory(colAddr, h.block.height, h.txn) + expect(acc?.owner).toStrictEqual(h.owner) + expect(acc?.txid).toStrictEqual(h.txid) + expect(acc?.txn).toStrictEqual(h.txn) + } + + const poolHistory = await controller.listAccountHistory(poolAddr, { size: 30 }) + for (const h of poolHistory.data) { + if (['sent', 'receive'].includes(h.type)) { + continue + } + const acc = await controller.getAccountHistory(poolAddr, h.block.height, h.txn) + expect(acc?.owner).toStrictEqual(h.owner) + expect(acc?.txid).toStrictEqual(h.txid) + expect(acc?.txn).toStrictEqual(h.txn) + } + }) + + it('should be failed for non-existence data', async () => { + const promise = controller.getAccountHistory(await container.getNewAddress(), Number(`${'0'.repeat(64)}`), 1) + await expect(promise).rejects.toThrow('Record not found') + }) + + it('should be failed as invalid height', async () => { + { // NaN + const promise = controller.getAccountHistory(await container.getNewAddress(), Number('NotANumber'), 1) + await expect(promise).rejects.toThrow('JSON value is not an integer as expected') + } + + { // negative height + const promise = controller.getAccountHistory(await container.getNewAddress(), -1, 1) + await expect(promise).rejects.toThrow('Record not found') + } + }) + + it('should be failed as getting unsupport tx type - sent, received, blockReward', async () => { + const history = await controller.listAccountHistory(colAddr, { size: 30 }) + for (const h of history.data) { + if (['sent', 'receive'].includes(h.type)) { + const promise = controller.getAccountHistory(colAddr, h.block.height, h.txn) + await expect(promise).rejects.toThrow('Record not found') + } + } + + const operatorAccHistory = await container.call('listaccounthistory', [RegTestFoundationKeys[0].operator.address]) + for (const h of operatorAccHistory) { + if (['blockReward'].includes(h.type)) { + const promise = controller.getAccountHistory(RegTestFoundationKeys[0].operator.address, h.blockHeight, h.txn) + await expect(promise).rejects.toThrow('Record not found') + } + } + }) +}) + describe('getBalance', () => { beforeAll(async () => { await container.start() diff --git a/apps/whale/src/module.api/address.controller.ts b/apps/whale/src/module.api/address.controller.ts index 13a2cd1b17..09bf064885 100644 --- a/apps/whale/src/module.api/address.controller.ts +++ b/apps/whale/src/module.api/address.controller.ts @@ -1,5 +1,5 @@ import BigNumber from 'bignumber.js' -import { ConflictException, Controller, ForbiddenException, Get, Inject, Param, Query } from '@nestjs/common' +import { BadRequestException, ConflictException, Controller, ForbiddenException, Get, Inject, NotFoundException, Param, ParseIntPipe, Query } from '@nestjs/common' import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' import { ApiPagedResponse } from './_core/api.paged.response' import { DeFiDCache } from './cache/defid.cache' @@ -31,6 +31,26 @@ export class AddressController { ) { } + @Get('/history/:height/:txno') + async getAccountHistory ( + @Param('address') address: string, + @Param('height', ParseIntPipe) height: number, + @Param('txno', ParseIntPipe) txno: number + ): Promise { + try { + const accountHistory = await this.rpcClient.account.getAccountHistory(address, height, txno) + if (Object.keys(accountHistory).length === 0) { + throw new NotFoundException('Record not found') + } + return mapAddressHistory(accountHistory) + } catch (err) { + if (err instanceof NotFoundException) { + throw err + } + throw new BadRequestException(err) + } + } + /** * @param {string} address to list participate account history * @param {PaginationQuery} query @@ -81,7 +101,7 @@ export class AddressController { }) } - const history = mapAddressHistory(list) + const history = list.map(each => mapAddressHistory(each)) return ApiPagedResponse.of(history, query.size, item => { return `${item.txid}-${item.type}-${item.block.height}` @@ -195,19 +215,17 @@ function mapAddressToken (id: string, tokenInfo: TokenInfo, value: BigNumber): A } } -function mapAddressHistory (list: AccountHistory[]): AddressHistory[] { - return list.map(each => { - return { - owner: each.owner, - txid: each.txid, - txn: each.txn, - type: each.type, - amounts: each.amounts, - block: { - height: each.blockHeight, - hash: each.blockHash, - time: each.blockTime - } +function mapAddressHistory (history: AccountHistory): AddressHistory { + return { + owner: history.owner, + txid: history.txid, + txn: history.txn, + type: history.type, + amounts: history.amounts, + block: { + height: history.blockHeight, + hash: history.blockHash, + time: history.blockTime } - }) + } } diff --git a/apps/whale/src/module.api/cache/defid.cache.ts b/apps/whale/src/module.api/cache/defid.cache.ts index 43c045b144..f3fcbfc1a7 100644 --- a/apps/whale/src/module.api/cache/defid.cache.ts +++ b/apps/whale/src/module.api/cache/defid.cache.ts @@ -3,7 +3,7 @@ import { Cache } from 'cache-manager' import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' import { TokenInfo, TokenResult } from '@defichain/jellyfish-api-core/dist/category/token' import { CachePrefix, GlobalCache } from './global.cache' -import { PoolPairInfo } from '@defichain/jellyfish-api-core/dist/category/poolpair' +import { PoolPairInfo, PoolPairsResult } from '@defichain/jellyfish-api-core/dist/category/poolpair' import { GetLoanSchemeResult } from '@defichain/jellyfish-api-core/dist/category/loan' @Injectable() @@ -61,6 +61,27 @@ export class DeFiDCache extends GlobalCache { return await this.get(CachePrefix.POOL_PAIR_INFO, id, this.fetchPoolPairInfo.bind(this)) } + /** + * Retrieve poolPair info from cached list of poolPairs as getPoolPair rpc + * tends to be more expensive + * @param {string} id - id of the poolPair + */ + async getPoolPairInfoFromPoolPairs (id: string): Promise { + const poolPairsById = await this.listPoolPairs(60) + if (poolPairsById === undefined) { + return undefined + } + return poolPairsById[id] + } + + async listPoolPairs (ttlSeconds: number): Promise { + return await this.get(CachePrefix.POOL_PAIRS, '*', this.fetchPoolPairs.bind(this), + { + ttl: ttlSeconds + } + ) + } + private async fetchPoolPairInfo (id: string): Promise { try { const result = await this.rpcClient.poolpair.getPoolPair(id) @@ -76,4 +97,33 @@ export class DeFiDCache extends GlobalCache { throw err } } + + private async fetchPoolPairs (): Promise { + const result: PoolPairsResult = {} + let next: number | null = 0 + + // Follow pagination chain until the end + while (true) { + const poolPairs: PoolPairsResult = await this.rpcClient.poolpair.listPoolPairs({ + start: next, + including_start: next === 0, // only for the first + limit: 1000 + }, true) + + const poolPairIds: string[] = Object.keys(poolPairs) + + // At the end of pagination chain - no more data to fetch + if (poolPairIds.length === 0) { + break + } + + // Add to results + for (const poolPairId of poolPairIds) { + result[poolPairId] = poolPairs[poolPairId] + } + next = Number(poolPairIds[poolPairIds.length - 1]) + } + + return result + } } diff --git a/apps/whale/src/module.api/cache/global.cache.ts b/apps/whale/src/module.api/cache/global.cache.ts index d7dd1487df..5065cfcb74 100644 --- a/apps/whale/src/module.api/cache/global.cache.ts +++ b/apps/whale/src/module.api/cache/global.cache.ts @@ -9,7 +9,8 @@ export enum CachePrefix { TOKEN_INFO = 0, POOL_PAIR_INFO = 1, TOKEN_INFO_SYMBOL = 2, - LOAN_SCHEME_INFO = 3 + LOAN_SCHEME_INFO = 3, + POOL_PAIRS = 4, } export class GlobalCache { diff --git a/apps/whale/src/module.api/loan.collateral.controller.e2e.ts b/apps/whale/src/module.api/loan.collateral.controller.e2e.ts index e625015fc4..7908353fe6 100644 --- a/apps/whale/src/module.api/loan.collateral.controller.e2e.ts +++ b/apps/whale/src/module.api/loan.collateral.controller.e2e.ts @@ -215,7 +215,7 @@ describe('get', () => { await controller.getCollateral('999') } catch (err) { expect(err).toBeInstanceOf(NotFoundException) - expect((err as NotFoundException).getResponse()).toStrictEqual({ + expect(err.response).toStrictEqual({ statusCode: 404, message: 'Unable to find collateral token', error: 'Not Found' diff --git a/apps/whale/src/module.api/loan.scheme.controller.e2e.ts b/apps/whale/src/module.api/loan.scheme.controller.e2e.ts index b721c4d5d4..a933cfab9d 100644 --- a/apps/whale/src/module.api/loan.scheme.controller.e2e.ts +++ b/apps/whale/src/module.api/loan.scheme.controller.e2e.ts @@ -135,10 +135,10 @@ describe('get', () => { await controller.getScheme('999') } catch (err) { expect(err).toBeInstanceOf(NotFoundException) - expect((err as NotFoundException).getResponse()).toStrictEqual({ - error: 'Not Found', + expect(err.response).toStrictEqual({ statusCode: 404, - message: 'Unable to find scheme' + message: 'Unable to find scheme', + error: 'Not Found' }) } }) diff --git a/apps/whale/src/module.api/loan.token.controller.e2e.ts b/apps/whale/src/module.api/loan.token.controller.e2e.ts index 99675339f8..b4d7b2f7e5 100644 --- a/apps/whale/src/module.api/loan.token.controller.e2e.ts +++ b/apps/whale/src/module.api/loan.token.controller.e2e.ts @@ -184,7 +184,7 @@ describe('get', () => { await controller.getLoanToken('999') } catch (err) { expect(err).toBeInstanceOf(NotFoundException) - expect((err as NotFoundException).getResponse()).toStrictEqual({ + expect(err.response).toStrictEqual({ statusCode: 404, message: 'Unable to find loan token', error: 'Not Found' diff --git a/apps/whale/src/module.api/loan.vault.controller.e2e.ts b/apps/whale/src/module.api/loan.vault.controller.e2e.ts index 730e477258..1f2b2ad155 100644 --- a/apps/whale/src/module.api/loan.vault.controller.e2e.ts +++ b/apps/whale/src/module.api/loan.vault.controller.e2e.ts @@ -127,7 +127,7 @@ describe('get', () => { await controller.getVault('0530ab29a9f09416a014a4219f186f1d5d530e9a270a9f941275b3972b43ebb7') } catch (err) { expect(err).toBeInstanceOf(NotFoundException) - expect((err as NotFoundException).getResponse()).toStrictEqual({ + expect(err.response).toStrictEqual({ statusCode: 404, message: 'Unable to find vault', error: 'Not Found' @@ -138,7 +138,7 @@ describe('get', () => { await controller.getVault('999') } catch (err) { expect(err).toBeInstanceOf(NotFoundException) - expect((err as NotFoundException).getResponse()).toStrictEqual({ + expect(err.response).toStrictEqual({ statusCode: 404, message: 'Unable to find vault', error: 'Not Found' diff --git a/apps/whale/src/module.api/masternode.controller.e2e.ts b/apps/whale/src/module.api/masternode.controller.e2e.ts index 4a1e0ac224..cf26ae63cb 100644 --- a/apps/whale/src/module.api/masternode.controller.e2e.ts +++ b/apps/whale/src/module.api/masternode.controller.e2e.ts @@ -94,7 +94,7 @@ describe('get', () => { await controller.get('8d4d987dee688e400a0cdc899386f243250d3656d802231755ab4d28178c9816') } catch (err) { expect(err).toBeInstanceOf(NotFoundException) - expect((err as NotFoundException).getResponse()).toStrictEqual({ + expect(err.response).toStrictEqual({ statusCode: 404, message: 'Unable to find masternode', error: 'Not Found' diff --git a/apps/whale/src/module.api/masternode.service.ts b/apps/whale/src/module.api/masternode.service.ts index a58d9c404f..661387630a 100644 --- a/apps/whale/src/module.api/masternode.service.ts +++ b/apps/whale/src/module.api/masternode.service.ts @@ -61,7 +61,7 @@ export class MasternodeService { } // !TODO: Alter retrospective behaviour based on EunosPaya height - // See: https://github.com/DeFiCh/ain/blob/master/dist/masternodes/masternodes.cpp#L116 + // See: https://github.com/DeFiCh/ain/blob/master/src/masternodes/masternodes.cpp#L116 async getMasternodeState (masternode: Masternode, height: number): Promise { if (masternode.resignHeight === -1) { // enabled or pre-enabled // Special case for genesis block diff --git a/apps/whale/src/module.api/poolpair.controller.e2e.ts b/apps/whale/src/module.api/poolpair.controller.e2e.ts index 2df52dfc19..d436d56a97 100644 --- a/apps/whale/src/module.api/poolpair.controller.e2e.ts +++ b/apps/whale/src/module.api/poolpair.controller.e2e.ts @@ -4,6 +4,7 @@ import { NestFastifyApplication } from '@nestjs/platform-fastify' import { createTestingApp, stopTestingApp, waitForIndexedHeight } from '../e2e.module' import { addPoolLiquidity, createPoolPair, createToken, getNewAddress, mintTokens } from '@defichain/testing' import { NotFoundException } from '@nestjs/common' +import { BigNumber } from 'bignumber.js' const container = new MasterNodeRegTestContainer() let app: NestFastifyApplication @@ -41,6 +42,20 @@ async function setup (): Promise { await createToken(container, token) await mintTokens(container, token) } + + // Create non-DAT token - direct RPC call required as createToken() will + // rpc call 'gettoken' with symbol, but will fail for non-DAT tokens + await container.waitForWalletBalanceGTE(110) + await container.call('createtoken', [{ + symbol: 'M', + name: 'M', + isDAT: false, + mintable: true, + tradeable: true, + collateralAddress: await getNewAddress(container) + }]) + await container.generate(1) + await createPoolPair(container, 'A', 'DFI') await createPoolPair(container, 'B', 'DFI') await createPoolPair(container, 'C', 'DFI') @@ -76,6 +91,15 @@ async function setup (): Promise { shareAddress: await getNewAddress(container) }) + // 1 G = 5 A = 10 DFI + await addPoolLiquidity(container, { + tokenA: 'G', + amountA: 10, + tokenB: 'A', + amountB: 50, + shareAddress: await getNewAddress(container) + }) + // 1 J = 7 K await addPoolLiquidity(container, { tokenA: 'J', @@ -101,6 +125,18 @@ async function setup (): Promise { shareAddress: await getNewAddress(container) }) + // BURN should not be listed as swappable + await createToken(container, 'BURN') + await createPoolPair(container, 'BURN', 'DFI', { status: false }) + await mintTokens(container, 'BURN', { mintAmount: 1 }) + await addPoolLiquidity(container, { + tokenA: 'BURN', + amountA: 1, + tokenB: 'DFI', + amountB: 1, + shareAddress: await getNewAddress(container) + }) + // dexUsdtDfi setup await createToken(container, 'USDT') await createPoolPair(container, 'USDT', 'DFI') @@ -115,6 +151,15 @@ async function setup (): Promise { await container.call('setgov', [{ LP_SPLITS: { 14: 1.0 } }]) await container.generate(1) + + // dex fee set up + await container.call('setgov', [{ + ATTRIBUTES: { + 'v0/poolpairs/14/token_a_fee_pct': '0.05', + 'v0/poolpairs/14/token_b_fee_pct': '0.08' + } + }]) + await container.generate(1) } describe('list', () => { @@ -137,14 +182,24 @@ describe('list', () => { symbol: 'B', reserve: '50', blockCommission: '0', - displaySymbol: 'dB' + displaySymbol: 'dB', + fee: { + pct: '0.05', + inPct: '0.05', + outPct: '0.05' + } }, tokenB: { id: '0', symbol: 'DFI', reserve: '300', blockCommission: '0', - displaySymbol: 'DFI' + displaySymbol: 'DFI', + fee: { + pct: '0.08', + inPct: '0.08', + outPct: '0.08' + } }, apr: { reward: 2229.42, @@ -185,7 +240,7 @@ describe('list', () => { expect(first.data[1].symbol).toStrictEqual('B-DFI') const next = await controller.list({ - size: 11, + size: 12, next: first.page?.next }) @@ -223,14 +278,16 @@ describe('get', () => { symbol: 'A', reserve: '100', blockCommission: '0', - displaySymbol: 'dA' + displaySymbol: 'dA', + fee: undefined }, tokenB: { id: '0', symbol: 'DFI', reserve: '200', blockCommission: '0', - displaySymbol: 'DFI' + displaySymbol: 'DFI', + fee: undefined }, apr: { reward: 0, @@ -265,9 +322,9 @@ describe('get', () => { expect.assertions(2) try { await controller.get('999') - } catch (err) { + } catch (err: any) { expect(err).toBeInstanceOf(NotFoundException) - expect((err as NotFoundException).getResponse()).toStrictEqual({ + expect(err.response).toStrictEqual({ statusCode: 404, message: 'Unable to find poolpair', error: 'Not Found' @@ -355,7 +412,7 @@ describe('get best path', () => { { symbol: 'G-A', poolPairId: '19', - priceRatio: { ab: '0.00000000', ba: '0.00000000' }, + priceRatio: { ab: '0.20000000', ba: '5.00000000' }, tokenA: { id: '7', symbol: 'G', displaySymbol: 'dG' }, tokenB: { id: '1', symbol: 'A', displaySymbol: 'dA' } }, @@ -374,11 +431,11 @@ describe('get best path', () => { tokenB: { id: '0', symbol: 'DFI', displaySymbol: 'DFI' } } ], - estimatedReturn: '0.00000000' + estimatedReturn: '2.50000000' }) }) - it('should get best of two possible swap paths', async () => { + it('should return direct path even if composite swap paths has greater return', async () => { // 1 J = 7 K // 1 J = 2 L = 8 K const response = await controller.getBestPath('10', '11') @@ -395,21 +452,14 @@ describe('get best path', () => { }, bestPath: [ { - symbol: 'J-L', - poolPairId: '22', - priceRatio: { ab: '0.50000000', ba: '2.00000000' }, + symbol: 'J-K', + poolPairId: '21', + priceRatio: { ab: '0.14285714', ba: '7.00000000' }, tokenA: { id: '10', symbol: 'J', displaySymbol: 'dJ' }, - tokenB: { id: '12', symbol: 'L', displaySymbol: 'dL' } - }, - { - symbol: 'L-K', - poolPairId: '23', - priceRatio: { ab: '0.25000000', ba: '4.00000000' }, - tokenA: { id: '12', symbol: 'L', displaySymbol: 'dL' }, tokenB: { id: '11', symbol: 'K', displaySymbol: 'dK' } } ], - estimatedReturn: '8.00000000' + estimatedReturn: '7.00000000' }) }) @@ -522,7 +572,7 @@ describe('get all paths', () => { { symbol: 'G-A', poolPairId: '19', - priceRatio: { ab: '0.00000000', ba: '0.00000000' }, + priceRatio: { ab: '0.20000000', ba: '5.00000000' }, tokenA: { id: '7', symbol: 'G', displaySymbol: 'dG' }, tokenB: { id: '1', symbol: 'A', displaySymbol: 'dA' } }, @@ -563,7 +613,7 @@ describe('get all paths', () => { { symbol: 'I-J', poolPairId: '20', - priceRatio: { ab: '0.00000000', ba: '0.00000000' }, + priceRatio: { ab: '0', ba: '0' }, tokenA: { id: '9', symbol: 'I', displaySymbol: 'dI' }, tokenB: { id: '10', symbol: 'J', displaySymbol: 'dJ' } }, @@ -586,7 +636,7 @@ describe('get all paths', () => { { symbol: 'I-J', poolPairId: '20', - priceRatio: { ab: '0.00000000', ba: '0.00000000' }, + priceRatio: { ab: '0', ba: '0' }, tokenA: { id: '9', symbol: 'I', displaySymbol: 'dI' }, tokenB: { id: '10', symbol: 'J', displaySymbol: 'dJ' } }, @@ -662,6 +712,14 @@ describe('get all paths', () => { }) }) + it('should throw error when fromToken === toToken', async () => { + // DFI to DFI - forbid technically correct but redundant results, + // e.g. [DFI -> A -> DFI], [DFI -> B -> DFI], etc. + await expect(controller.listPaths('0', '0')) + .rejects + .toThrowError('Invalid tokens: fromToken must be different from toToken') + }) + it('should throw error for invalid tokenId', async () => { await expect(controller.listPaths('-1', '1')).rejects.toThrowError('Unable to find token -1') await expect(controller.listPaths('1', '-1')).rejects.toThrowError('Unable to find token -1') @@ -679,7 +737,7 @@ describe('get list swappable tokens', () => { swappableTokens: [ { id: '7', symbol: 'G', displaySymbol: 'dG' }, { id: '0', symbol: 'DFI', displaySymbol: 'DFI' }, - { id: '24', symbol: 'USDT', displaySymbol: 'dUSDT' }, + { id: '26', symbol: 'USDT', displaySymbol: 'dUSDT' }, { id: '6', symbol: 'F', displaySymbol: 'dF' }, { id: '5', symbol: 'E', displaySymbol: 'dE' }, { id: '4', symbol: 'D', displaySymbol: 'dD' }, @@ -689,6 +747,12 @@ describe('get list swappable tokens', () => { }) }) + it('should not show status:false tokens', async () => { + const result = await controller.listSwappableTokens('1') // A + expect(result.swappableTokens.map(token => token.symbol)) + .not.toContain('BURN') + }) + it('should list no tokens for token that is not swappable with any', async () => { const result = await controller.listSwappableTokens('8') // H expect(result).toStrictEqual({ @@ -703,3 +767,215 @@ describe('get list swappable tokens', () => { await expect(controller.listSwappableTokens('a')).rejects.toThrowError('Unable to find token a') }) }) + +describe('latest dex prices', () => { + it('should get latest dex prices - denomination: DFI', async () => { + const result = await controller.listDexPrices('DFI') + expect(result).toStrictEqual({ + denomination: { displaySymbol: 'DFI', id: '0', symbol: 'DFI' }, + dexPrices: { + USDT: { + token: { displaySymbol: 'dUSDT', id: '26', symbol: 'USDT' }, + denominationPrice: '0.43151288' + }, + L: { + token: { displaySymbol: 'dL', id: '12', symbol: 'L' }, + denominationPrice: '0' + }, + K: { + token: { displaySymbol: 'dK', id: '11', symbol: 'K' }, + denominationPrice: '0' + }, + J: { + token: { displaySymbol: 'dJ', id: '10', symbol: 'J' }, + denominationPrice: '0' + }, + I: { + token: { displaySymbol: 'dI', id: '9', symbol: 'I' }, + denominationPrice: '0' + }, + H: { + token: { displaySymbol: 'dH', id: '8', symbol: 'H' }, + denominationPrice: '0' + }, + G: { + token: { displaySymbol: 'dG', id: '7', symbol: 'G' }, + denominationPrice: '10.00000000' + }, + F: { + token: { displaySymbol: 'dF', id: '6', symbol: 'F' }, + denominationPrice: '0' + }, + E: { + token: { displaySymbol: 'dE', id: '5', symbol: 'E' }, + denominationPrice: '0' + }, + D: { + token: { displaySymbol: 'dD', id: '4', symbol: 'D' }, + denominationPrice: '0' + }, + C: { + token: { displaySymbol: 'dC', id: '3', symbol: 'C' }, + denominationPrice: '4.00000000' + }, + B: { + token: { displaySymbol: 'dB', id: '2', symbol: 'B' }, + denominationPrice: '6.00000000' + }, + A: { + token: { displaySymbol: 'dA', id: '1', symbol: 'A' }, + denominationPrice: '2.00000000' + } + } + }) + }) + + it('should get latest dex prices - denomination: USDT', async () => { + const result = await controller.listDexPrices('USDT') + expect(result).toStrictEqual({ + denomination: { displaySymbol: 'dUSDT', id: '26', symbol: 'USDT' }, + dexPrices: { + DFI: { + token: { displaySymbol: 'DFI', id: '0', symbol: 'DFI' }, + denominationPrice: '2.31742792' // 1 DFI = 2.31 USDT + }, + A: { + token: { displaySymbol: 'dA', id: '1', symbol: 'A' }, + denominationPrice: '4.63485584' // 1 A = 4.63 USDT + }, + G: { + token: { displaySymbol: 'dG', id: '7', symbol: 'G' }, + denominationPrice: '23.17427920' // 1 G = 5 A = 10 DFI = 23 USDT + }, + B: { + token: { displaySymbol: 'dB', id: '2', symbol: 'B' }, + denominationPrice: '13.90456752' + }, + C: { + token: { displaySymbol: 'dC', id: '3', symbol: 'C' }, + denominationPrice: '9.26971168' + }, + L: { + token: { displaySymbol: 'dL', id: '12', symbol: 'L' }, + denominationPrice: '0' + }, + K: { + token: { displaySymbol: 'dK', id: '11', symbol: 'K' }, + denominationPrice: '0' + }, + J: { + token: { displaySymbol: 'dJ', id: '10', symbol: 'J' }, + denominationPrice: '0' + }, + I: { + token: { displaySymbol: 'dI', id: '9', symbol: 'I' }, + denominationPrice: '0' + }, + H: { + token: { displaySymbol: 'dH', id: '8', symbol: 'H' }, + denominationPrice: '0' + }, + F: { + token: { displaySymbol: 'dF', id: '6', symbol: 'F' }, + denominationPrice: '0' + }, + E: { + token: { displaySymbol: 'dE', id: '5', symbol: 'E' }, + denominationPrice: '0' + }, + D: { + token: { displaySymbol: 'dD', id: '4', symbol: 'D' }, + denominationPrice: '0' + } + } + }) + }) + + it('should get consistent, mathematically sound dex prices - USDT and DFI', async () => { + const pricesInUSDT = await controller.listDexPrices('USDT') + const pricesInDFI = await controller.listDexPrices('DFI') + + // 1 DFI === x USDT + // 1 USDT === 1/x DFI + expect(new BigNumber(pricesInDFI.dexPrices.USDT.denominationPrice).toFixed(8)) + .toStrictEqual( + new BigNumber(pricesInUSDT.dexPrices.DFI.denominationPrice) + .pow(-1) + .toFixed(8) + ) + expect(pricesInDFI.dexPrices.USDT.denominationPrice).toStrictEqual('0.43151288') + expect(pricesInUSDT.dexPrices.DFI.denominationPrice).toStrictEqual('2.31742792') + }) + + it('should get consistent, mathematically sound dex prices - A and B', async () => { + // 1 A = n DFI + // 1 B = m DFI + // 1 DFI = 1/m B + // hence 1 A = n DFI = n/m B + const pricesInDFI = await controller.listDexPrices('DFI') + const pricesInA = await controller.listDexPrices('A') + const pricesInB = await controller.listDexPrices('B') + + // 1 A = n DFI + const AInDfi = new BigNumber(pricesInDFI.dexPrices.A.denominationPrice) // n + // 1 DFI = 1/n A + const DFIInA = new BigNumber(pricesInA.dexPrices.DFI.denominationPrice) + + // Verify that B/DFI and DFI/B values are consistent between listPrices('DFI') and listPrices('A') + expect(AInDfi.toFixed(8)).toStrictEqual(DFIInA.pow(-1).toFixed(8)) + expect(AInDfi.toFixed(8)).toStrictEqual('2.00000000') + expect(DFIInA.toFixed(8)).toStrictEqual('0.50000000') + + // 1 B = m DFI + const BInDfi = new BigNumber(pricesInDFI.dexPrices.B.denominationPrice) // m + // 1 DFI = 1/m B + const DFIInB = new BigNumber(pricesInB.dexPrices.DFI.denominationPrice) + + // Verify that B/DFI and DFI/B values are consistent between listPrices('DFI') and listPrices('B') + expect(BInDfi.toFixed(6)).toStrictEqual( + DFIInB.pow(-1).toFixed(6) // precision - 2 due to floating point imprecision + ) + expect(BInDfi.toFixed(8)).toStrictEqual('6.00000000') + expect(DFIInB.toFixed(8)).toStrictEqual('0.16666666') + + // Verify that the value of token A denoted in B (1 A = n/m B) is also returned by the endpoint + expect(new BigNumber(pricesInB.dexPrices.A.denominationPrice).toFixed(7)) + .toStrictEqual( + AInDfi.div(BInDfi).toFixed(7) // precision - 1 due to floating point imprecision + ) + expect(AInDfi.div(BInDfi).toFixed(8)).toStrictEqual('0.33333333') + expect(pricesInB.dexPrices.A.denominationPrice).toStrictEqual('0.33333332') + }) + + it('should list DAT tokens only - M (non-DAT token) is not included in result', async () => { + // M not included in any denominated dex prices + const result = await controller.listDexPrices('DFI') + expect(result.dexPrices.M).toBeUndefined() + + // M is not a valid 'denomination' token + await expect(controller.listDexPrices('M')) + .rejects + .toThrowError('Could not find token with symbol \'M\'') + }) + + it('should list DAT tokens only - status:false tokens are excluded', async () => { + // BURN not included in any denominated dex prices + const result = await controller.listDexPrices('DFI') + expect(result.dexPrices.BURN).toBeUndefined() + + // BURN is not a valid 'denomination' token + await expect(controller.listDexPrices('BURN')) + .rejects + .toThrowError('Unexpected error: could not find token with symbol \'BURN\'') + }) + + describe('param validation - denomination', () => { + it('should throw error for invalid denomination', async () => { + await expect(controller.listDexPrices('aaaaa')).rejects.toThrowError('Could not find token with symbol \'aaaaa\'') + await expect(controller.listDexPrices('-1')).rejects.toThrowError('Could not find token with symbol \'-1\'') + + // endpoint is case-sensitive + await expect(controller.listDexPrices('dfi')).rejects.toThrowError('Could not find token with symbol \'dfi\'') + }) + }) +}) diff --git a/apps/whale/src/module.api/poolpair.controller.ts b/apps/whale/src/module.api/poolpair.controller.ts index 62c8c54d87..9d488d446d 100644 --- a/apps/whale/src/module.api/poolpair.controller.ts +++ b/apps/whale/src/module.api/poolpair.controller.ts @@ -4,20 +4,22 @@ import { ApiPagedResponse } from './_core/api.paged.response' import { DeFiDCache } from './cache/defid.cache' import { AllSwappableTokensResult, - BestSwapPathResult, + BestSwapPathResult, DexPricesResult, PoolPairData, PoolSwapAggregatedData, PoolSwapData, SwapPathsResult } from '@defichain/whale-api-client/dist/api/PoolPairs' import { PaginationQuery } from './_core/api.query' -import { PoolPairService, PoolSwapPathFindingService } from './poolpair.service' +import { PoolPairService } from './poolpair.service' +import { PoolSwapPathFindingService } from './poolswap.pathfinding.service' import BigNumber from 'bignumber.js' import { PoolPairInfo } from '@defichain/jellyfish-api-core/dist/category/poolpair' import { parseDATSymbol } from './token.controller' import { PoolSwapMapper } from '../module.model/pool.swap' import { PoolSwapAggregatedMapper } from '../module.model/pool.swap.aggregated' import { StringIsIntegerPipe } from './pipes/api.validation.pipe' +import { PoolPairPricesService } from './poolpair.prices.service' @Controller('/poolpairs') export class PoolPairController { @@ -26,6 +28,7 @@ export class PoolPairController { protected readonly deFiDCache: DeFiDCache, private readonly poolPairService: PoolPairService, private readonly poolSwapPathService: PoolSwapPathFindingService, + private readonly poolPairPricesService: PoolPairPricesService, private readonly poolSwapMapper: PoolSwapMapper, private readonly poolSwapAggregatedMapper: PoolSwapAggregatedMapper ) { @@ -118,6 +121,7 @@ export class PoolPairController { const fromTo = await this.poolPairService.findSwapFromTo(swap.block.height, swap.txid, swap.txno) swap.from = fromTo?.from swap.to = fromTo?.to + swap.type = await this.poolPairService.checkSwapType(swap) } return ApiPagedResponse.of(items, query.size, item => { @@ -182,6 +186,13 @@ export class PoolPairController { ): Promise { return await this.poolSwapPathService.getBestPath(fromTokenId, toTokenId) } + + @Get('/dexprices') + async listDexPrices ( + @Query('denomination') denomination: string + ): Promise { + return await this.poolPairPricesService.listDexPrices(denomination) + } } function mapPoolPair (id: string, info: PoolPairInfo, totalLiquidityUsd?: BigNumber, apr?: PoolPairData['apr'], volume?: PoolPairData['volume']): PoolPairData { @@ -198,14 +209,28 @@ function mapPoolPair (id: string, info: PoolPairInfo, totalLiquidityUsd?: BigNum displaySymbol: parseDATSymbol(symbolA), id: info.idTokenA, reserve: info.reserveA.toFixed(), - blockCommission: info.blockCommissionA.toFixed() + blockCommission: info.blockCommissionA.toFixed(), + fee: info.dexFeePctTokenA !== undefined + ? { + pct: info.dexFeePctTokenA?.toFixed(), + inPct: info.dexFeeInPctTokenA?.toFixed(), + outPct: info.dexFeeOutPctTokenA?.toFixed() + } + : undefined }, tokenB: { symbol: symbolB, displaySymbol: parseDATSymbol(symbolB), id: info.idTokenB, reserve: info.reserveB.toFixed(), - blockCommission: info.blockCommissionB.toFixed() + blockCommission: info.blockCommissionB.toFixed(), + fee: info.dexFeePctTokenB !== undefined + ? { + pct: info.dexFeePctTokenB?.toFixed(), + inPct: info.dexFeeInPctTokenB?.toFixed(), + outPct: info.dexFeeOutPctTokenB?.toFixed() + } + : undefined }, priceRatio: { ab: info['reserveA/reserveB'] instanceof BigNumber ? info['reserveA/reserveB'].toFixed() : info['reserveA/reserveB'], diff --git a/apps/whale/src/module.api/poolpair.prices.service.ts b/apps/whale/src/module.api/poolpair.prices.service.ts new file mode 100644 index 0000000000..99bd29ad0b --- /dev/null +++ b/apps/whale/src/module.api/poolpair.prices.service.ts @@ -0,0 +1,118 @@ +import { Injectable, Logger } from '@nestjs/common' +import { PoolSwapPathFindingService } from './poolswap.pathfinding.service' +import { TokenMapper } from '../module.model/token' +import { DeFiDCache } from './cache/defid.cache' +import { TokenInfo } from '@defichain/jellyfish-api-core/dist/category/token' +import { DexPricesResult, TokenIdentifier } from '@defichain/whale-api-client/dist/api/PoolPairs' +import { parseDisplaySymbol } from './token.controller' +import { SemaphoreCache } from './cache/semaphore.cache' + +@Injectable() +export class PoolPairPricesService { + private readonly logger: Logger = new Logger(PoolSwapPathFindingService.name) + + constructor ( + private readonly poolSwapPathfindingService: PoolSwapPathFindingService, + private readonly tokenMapper: TokenMapper, + private readonly defidCache: DeFiDCache, + protected readonly cache: SemaphoreCache + ) { + } + + async listDexPrices (denominationSymbol: string): Promise { + const cached = await this.cache.get( + `LATEST_DEX_PRICES_${denominationSymbol}`, + async () => await this._listDexPrices(denominationSymbol), + { + ttl: 30 // 30s + } + ) + if (cached !== undefined) { + return cached + } + return await this._listDexPrices(denominationSymbol) + } + + private async _listDexPrices (denominationSymbol: string): Promise { + const dexPrices: DexPricesResult['dexPrices'] = {} + + // Do a check first to ensure the symbol provided is valid, to save on calling getAllTokens + // for non-existent tokens + try { + await this.defidCache.getTokenInfoBySymbol(denominationSymbol) + } catch (err) { + throw new Error(`Could not find token with symbol '${denominationSymbol}'`) + } + + // Get all tokens and their info + const allTokensBySymbol: TokensBySymbol = await this.getAllTokens() + const allTokens: TokenInfoWithId[] = Object.values(allTokensBySymbol) + + // Get denomination token info + const denominationToken = allTokensBySymbol[denominationSymbol] + if (denominationToken === undefined) { + throw new Error(`Unexpected error: could not find token with symbol '${denominationSymbol}'`) + } + + // For every token available, compute estimated return in denomination token + for (const token of allTokens) { + if (token.id === denominationToken.id) { + continue + } + const bestPath = await this.poolSwapPathfindingService.getBestPath(token.id, denominationToken.id) + dexPrices[token.symbol] = { + token: mapToTokenIdentifier(token), + denominationPrice: bestPath.estimatedReturn + } + } + + return { + denomination: mapToTokenIdentifier(denominationToken), + dexPrices + } + } + + /** + * Helper to get all tokens in a map indexed by their symbol for quick look-ups + * @private + * @return {Promise} + */ + private async getAllTokens (): Promise { + const tokens = await this.tokenMapper.query(Number.MAX_SAFE_INTEGER) + const allTokenInfo: TokensBySymbol = {} + + for (const token of tokens) { + // Skip LP tokens and non-DAT tokens + if (token.isLPS || !token.isDAT || token.symbol === 'BURN') { + continue + } + + const tokenInfo = await this.defidCache.getTokenInfo(token.tokenId.toString()) + if (tokenInfo === undefined) { + this.logger.error(`Could not find token info for id: ${token.tokenId}`) + continue + } + + allTokenInfo[token.symbol] = { + id: token.tokenId.toString(), + ...tokenInfo + } + } + return allTokenInfo + } +} + +function mapToTokenIdentifier (token: TokenInfoWithId): TokenIdentifier { + return { + id: token.id, + symbol: token.symbol, + displaySymbol: parseDisplaySymbol(token) + } +} + +// To remove if/when jellyfish-api-core supports IDs on tokenInfo, since it's commonly required +interface TokenInfoWithId extends TokenInfo { + id: string +} + +type TokensBySymbol = Record diff --git a/apps/whale/src/module.api/poolpair.service.ts b/apps/whale/src/module.api/poolpair.service.ts index 16e7619582..bc153f456f 100644 --- a/apps/whale/src/module.api/poolpair.service.ts +++ b/apps/whale/src/module.api/poolpair.service.ts @@ -4,13 +4,10 @@ import BigNumber from 'bignumber.js' import { PoolPairInfo } from '@defichain/jellyfish-api-core/dist/category/poolpair' import { SemaphoreCache } from './cache/semaphore.cache' import { - AllSwappableTokensResult, - BestSwapPathResult, PoolPairData, PoolSwapData, PoolSwapFromToData, - SwapPathPoolPair, - SwapPathsResult, SwapType, TokenIdentifier + SwapType } from '@defichain/whale-api-client/dist/api/PoolPairs' import { getBlockSubsidy } from './subsidy' import { BlockMapper } from '../module.model/block' @@ -23,21 +20,16 @@ import { CCompositeSwap, CompositeSwap, CPoolSwap, - DfTx, OP_DEFI_TX, PoolSwap as PoolSwapDfTx, - toOPCodes + toOPCodes, + DfTx } from '@defichain/jellyfish-transaction' import { fromScript } from '@defichain/jellyfish-address' import { NetworkName } from '@defichain/jellyfish-network' import { AccountHistory } from '@defichain/jellyfish-api-core/dist/category/account' import { DeFiDCache } from './cache/defid.cache' import { parseDisplaySymbol } from './token.controller' -import { UndirectedGraph } from 'graphology' -import { PoolPairToken, PoolPairTokenMapper } from '../module.model/pool.pair.token' -import { Interval } from '@nestjs/schedule' -import { allSimplePaths } from 'graphology-simple-path' -import { connectedComponents } from 'graphology-components' import { RpcApiError } from '@defichain/jellyfish-api-core' @Injectable() @@ -489,216 +481,3 @@ function findPoolSwapFromTo (history: AccountHistory | undefined, from: boolean, return undefined } - -@Injectable() -export class PoolSwapPathFindingService { - tokenGraph: UndirectedGraph = new UndirectedGraph() - tokensToSwappableTokens = new Map() - - constructor ( - protected readonly poolPairTokenMapper: PoolPairTokenMapper, - protected readonly deFiDCache: DeFiDCache - ) { - } - - @Interval(120_000) // 120s - async syncTokenGraph (): Promise { - const poolPairTokens = await this.poolPairTokenMapper.list(200) - await this.addTokensAndConnectionsToGraph(poolPairTokens) - await this.updateTokensToSwappableTokens() - } - - async getAllSwappableTokens (tokenId: string): Promise { - await this.syncTokenGraphIfEmpty() - - return { - fromToken: await this.getTokenIdentifier(tokenId), - swappableTokens: this.tokensToSwappableTokens.get(tokenId) ?? [] - } - } - - async getBestPath (fromTokenId: string, toTokenId: string): Promise { - const { fromToken, toToken, paths } = await this.getAllSwapPaths(fromTokenId, toTokenId) - - let bestPath: SwapPathPoolPair[] = [] - let bestReturn = new BigNumber(-1) - - for (const path of paths) { - const totalReturn = computeReturnInDestinationToken(path, fromTokenId) - if (totalReturn.isGreaterThan(bestReturn)) { - bestReturn = totalReturn - bestPath = path - } - } - return { - fromToken: fromToken, - toToken: toToken, - bestPath: bestPath, - estimatedReturn: bestReturn.eq(-1) - ? '0' - : bestReturn.toFixed(8) // denoted in toToken - } - } - - /** - * Get all poolPairs that can support a direct swap or composite swaps from one token to another. - * @param {number} fromTokenId - * @param {number} toTokenId - */ - async getAllSwapPaths (fromTokenId: string, toTokenId: string): Promise { - await this.syncTokenGraphIfEmpty() - - const result: SwapPathsResult = { - fromToken: await this.getTokenIdentifier(fromTokenId), - toToken: await this.getTokenIdentifier(toTokenId), - paths: [] - } - - if ( - !this.tokenGraph.hasNode(fromTokenId) || - !this.tokenGraph.hasNode(toTokenId) - ) { - return result - } - - result.paths = await this.computePathsBetweenTokens(fromTokenId, toTokenId) - return result - } - - /** - * Performs graph traversal to compute all possible paths between two tokens. - * Must be able to handle cycles. - * @param {number} fromTokenId - * @param {number} toTokenId - * @return {Promise} - * @private - */ - private async computePathsBetweenTokens ( - fromTokenId: string, - toTokenId: string - ): Promise { - const poolPairPaths: SwapPathPoolPair[][] = [] - - for (const path of allSimplePaths(this.tokenGraph, fromTokenId, toTokenId)) { - const poolPairs: SwapPathPoolPair[] = [] - - // Iterate over the path pairwise; ( tokenA )---< poolPairId >---( tokenB ) - // to collect poolPair info into the final result - for (let i = 1; i < path.length; i++) { - const tokenA = path[i - 1] - const tokenB = path[i] - - const poolPairId = this.tokenGraph.edge(tokenA, tokenB) - if (poolPairId === undefined) { - throw new Error( - 'Unexpected error encountered during path finding - ' + - `could not find edge between ${tokenA} and ${tokenB}` - ) - } - - const poolPair = await this.getPoolPairInfo(poolPairId) - poolPairs.push({ - poolPairId: poolPairId, - symbol: poolPair.symbol, - tokenA: await this.getTokenIdentifier(poolPair.idTokenA), - tokenB: await this.getTokenIdentifier(poolPair.idTokenB), - priceRatio: { - ab: new BigNumber(poolPair['reserveA/reserveB']).toFixed(8), - ba: new BigNumber(poolPair['reserveB/reserveA']).toFixed(8) - } - }) - } - - poolPairPaths.push(poolPairs) - } - - return poolPairPaths - } - - /** - * Derives from PoolPairToken to construct an undirected graph. - * Each node represents a token, each edge represents a poolPair that bridges a pair of tokens. - * For example, [[A-DFI], [B-DFI]] creates an undirected graph with 3 nodes and 2 edges: - * ( A )--- A-DFI ---( DFI )--- B-DFI ---( B ) - * @param {PoolPairToken[]} poolPairTokens - poolPairTokens to derive tokens and poolPairs added to the graph - * @private - */ - private async addTokensAndConnectionsToGraph (poolPairTokens: PoolPairToken[]): Promise { - for (const poolPairToken of poolPairTokens) { - const [a, b] = poolPairToken.id.split('-') - if (!this.tokenGraph.hasNode(a)) { - this.tokenGraph.addNode(a) - } - if (!this.tokenGraph.hasNode(b)) { - this.tokenGraph.addNode(b) - } - if (!this.tokenGraph.hasEdge(a, b)) { - this.tokenGraph.addUndirectedEdgeWithKey(poolPairToken.poolPairId, a, b) - } - } - } - - private async getTokenIdentifier (tokenId: string): Promise { - const tokenInfo = await this.deFiDCache.getTokenInfo(tokenId) - if (tokenInfo === undefined) { - throw new NotFoundException(`Unable to find token ${tokenId}`) - } - return { - id: tokenId, - symbol: tokenInfo.symbol, - displaySymbol: parseDisplaySymbol(tokenInfo) - } - } - - private async getPoolPairInfo (poolPairId: string): Promise { - const poolPair = await this.deFiDCache.getPoolPairInfo(poolPairId) - if (poolPair === undefined) { - throw new NotFoundException(`Unable to find token ${poolPairId}`) - } - return poolPair - } - - /** - * Indexes each token to their graph 'component', allowing quick queries for - * all the swappable tokens for a given token. - * @private - */ - private async updateTokensToSwappableTokens (): Promise { - const components = connectedComponents(this.tokenGraph) - for (const component of components) { - // enrich with symbol - const tokens: TokenIdentifier[] = [] - for (const tokenId of component) { - tokens.push(await this.getTokenIdentifier(tokenId)) - } - - // index each token to their swappable tokens - for (const token of tokens) { - this.tokensToSwappableTokens.set( - token.id, - tokens.filter(tk => tk.id !== token.id) // exclude tokens from their own 'swappables' list - ) - } - } - } - - private async syncTokenGraphIfEmpty (): Promise { - if (this.tokenGraph.size === 0) { - await this.syncTokenGraph() - } - } -} - -function computeReturnInDestinationToken (path: SwapPathPoolPair[], fromTokenId: string): BigNumber { - let total = new BigNumber(1) - for (const poolPair of path) { - if (fromTokenId === poolPair.tokenA.id) { - total = total.multipliedBy(poolPair.priceRatio.ba) - fromTokenId = poolPair.tokenB.id - } else { - total = total.multipliedBy(poolPair.priceRatio.ab) - fromTokenId = poolPair.tokenA.id - } - } - return total -} diff --git a/apps/whale/src/module.api/poolswap.pathfinding.service.ts b/apps/whale/src/module.api/poolswap.pathfinding.service.ts new file mode 100644 index 0000000000..bd255ba1b6 --- /dev/null +++ b/apps/whale/src/module.api/poolswap.pathfinding.service.ts @@ -0,0 +1,281 @@ +import { Inject, Injectable, NotFoundException } from '@nestjs/common' +import { UndirectedGraph } from 'graphology' +import { + AllSwappableTokensResult, + BestSwapPathResult, + SwapPathPoolPair, + SwapPathsResult, + TokenIdentifier +} from '@defichain/whale-api-client/dist/api/PoolPairs' +import { PoolPairToken, PoolPairTokenMapper } from '../module.model/pool.pair.token' +import { DeFiDCache } from './cache/defid.cache' +import { Interval } from '@nestjs/schedule' +import BigNumber from 'bignumber.js' +import { allSimplePaths } from 'graphology-simple-path' +import { parseDisplaySymbol } from './token.controller' +import { PoolPairInfo } from '@defichain/jellyfish-api-core/dist/category/poolpair' +import { connectedComponents } from 'graphology-components' +import { NetworkName } from '@defichain/jellyfish-network' + +@Injectable() +export class PoolSwapPathFindingService { + tokenGraph: UndirectedGraph = new UndirectedGraph() + tokensToSwappableTokens = new Map() + + constructor ( + protected readonly poolPairTokenMapper: PoolPairTokenMapper, + protected readonly deFiDCache: DeFiDCache, + @Inject('NETWORK') protected readonly network: NetworkName + ) { + } + + @Interval(120_000) // 120s + async syncTokenGraph (): Promise { + const poolPairTokens = await this.poolPairTokenMapper.list(200) + await this.addTokensAndConnectionsToGraph(poolPairTokens) + await this.updateTokensToSwappableTokens() + } + + async getAllSwappableTokens (tokenId: string): Promise { + await this.syncTokenGraphIfEmpty() + + return { + fromToken: await this.getTokenIdentifier(tokenId), + swappableTokens: this.tokensToSwappableTokens.get(tokenId) ?? [] + } + } + + async getBestPath (fromTokenId: string, toTokenId: string): Promise { + const { + fromToken, + toToken, + paths + } = await this.getAllSwapPaths(fromTokenId, toTokenId) + + // always use direct path if available + for (const path of paths) { + if (path.length === 1) { + return { + fromToken: fromToken, + toToken: toToken, + bestPath: path, + estimatedReturn: formatNumber( + computeReturnInDestinationToken(path, fromTokenId) + ) // denoted in toToken + } + } + } + + // otherwise, search for the best path based on return + let bestPath: SwapPathPoolPair[] = [] + let bestReturn = new BigNumber(0) + + for (const path of paths) { + const totalReturn = computeReturnInDestinationToken(path, fromTokenId) + if (totalReturn.isGreaterThan(bestReturn)) { + bestReturn = totalReturn + bestPath = path + } + } + + return { + fromToken: fromToken, + toToken: toToken, + bestPath: bestPath, + estimatedReturn: formatNumber(bestReturn) // denoted in toToken + } + } + + /** + * Get all poolPairs that can support a direct swap or composite swaps from one token to another. + * @param {number} fromTokenId + * @param {number} toTokenId + */ + async getAllSwapPaths (fromTokenId: string, toTokenId: string): Promise { + if (fromTokenId === toTokenId) { + throw new Error('Invalid tokens: fromToken must be different from toToken') + } + + await this.syncTokenGraphIfEmpty() + + const result: SwapPathsResult = { + fromToken: await this.getTokenIdentifier(fromTokenId), + toToken: await this.getTokenIdentifier(toTokenId), + paths: [] + } + + if ( + !this.tokenGraph.hasNode(fromTokenId) || + !this.tokenGraph.hasNode(toTokenId) + ) { + return result + } + + result.paths = await this.computePathsBetweenTokens(fromTokenId, toTokenId) + return result + } + + /** + * Performs graph traversal to compute all possible paths between two tokens. + * Must be able to handle cycles. + * @param {number} fromTokenId + * @param {number} toTokenId + * @return {Promise} + * @private + */ + private async computePathsBetweenTokens ( + fromTokenId: string, + toTokenId: string + ): Promise { + const poolPairPaths: SwapPathPoolPair[][] = [] + + for (const path of allSimplePaths(this.tokenGraph, fromTokenId, toTokenId)) { + const poolPairs: SwapPathPoolPair[] = [] + + // Iterate over the path pairwise; ( tokenA )---< poolPairId >---( tokenB ) + // to collect poolPair info into the final result + for (let i = 1; i < path.length; i++) { + const tokenA = path[i - 1] + const tokenB = path[i] + + const poolPairId = this.tokenGraph.edge(tokenA, tokenB) + if (poolPairId === undefined) { + throw new Error( + 'Unexpected error encountered during path finding - ' + + `could not find edge between ${tokenA} and ${tokenB}` + ) + } + + const poolPair = await this.getPoolPairInfo(poolPairId) + poolPairs.push({ + poolPairId: poolPairId, + symbol: poolPair.symbol, + tokenA: await this.getTokenIdentifier(poolPair.idTokenA), + tokenB: await this.getTokenIdentifier(poolPair.idTokenB), + priceRatio: { + ab: formatNumber(new BigNumber(poolPair['reserveA/reserveB'])), + ba: formatNumber(new BigNumber(poolPair['reserveB/reserveA'])) + } + }) + } + + poolPairPaths.push(poolPairs) + } + + return poolPairPaths + } + + /** + * Derives from PoolPairToken to construct an undirected graph. + * Each node represents a token, each edge represents a poolPair that bridges a pair of tokens. + * For example, [[A-DFI], [B-DFI]] creates an undirected graph with 3 nodes and 2 edges: + * ( A )--- A-DFI ---( DFI )--- B-DFI ---( B ) + * @param {PoolPairToken[]} poolPairTokens - poolPairTokens to derive tokens and poolPairs added to the graph + * @private + */ + private async addTokensAndConnectionsToGraph (poolPairTokens: PoolPairToken[]): Promise { + for (const poolPairToken of poolPairTokens) { + if (await this.isPoolPairIgnored(poolPairToken)) { + continue + } + + const [a, b] = poolPairToken.id.split('-') + + if (!this.tokenGraph.hasNode(a)) { + this.tokenGraph.addNode(a) + } + if (!this.tokenGraph.hasNode(b)) { + this.tokenGraph.addNode(b) + } + if (!this.tokenGraph.hasEdge(a, b)) { + this.tokenGraph.addUndirectedEdgeWithKey(poolPairToken.poolPairId, a, b) + } + } + } + + private async isPoolPairIgnored (pair: PoolPairToken): Promise { + // Hot Fix for MainNet due to cost of running getPoolPairInfo during boot up. + if (this.network === 'mainnet') { + if (pair.poolPairId === 48) { + return true + } + } else { + const poolPair = await this.getPoolPairInfo(`${pair.poolPairId}`) + if (!poolPair.status) { + return true + } + } + + return false + } + + private async getTokenIdentifier (tokenId: string): Promise { + const tokenInfo = await this.deFiDCache.getTokenInfo(tokenId) + if (tokenInfo === undefined) { + throw new NotFoundException(`Unable to find token ${tokenId}`) + } + return { + id: tokenId, + symbol: tokenInfo.symbol, + displaySymbol: parseDisplaySymbol(tokenInfo) + } + } + + private async getPoolPairInfo (poolPairId: string): Promise { + const poolPair = await this.deFiDCache.getPoolPairInfoFromPoolPairs(poolPairId) + if (poolPair === undefined) { + throw new NotFoundException(`Unable to find token ${poolPairId}`) + } + return poolPair + } + + /** + * Indexes each token to their graph 'component', allowing quick queries for + * all the swappable tokens for a given token. + * @private + */ + private async updateTokensToSwappableTokens (): Promise { + const components = connectedComponents(this.tokenGraph) + for (const component of components) { + // enrich with symbol + const tokens: TokenIdentifier[] = [] + for (const tokenId of component) { + tokens.push(await this.getTokenIdentifier(tokenId)) + } + + // index each token to their swappable tokens + for (const token of tokens) { + this.tokensToSwappableTokens.set( + token.id, + tokens.filter(tk => tk.id !== token.id) // exclude tokens from their own 'swappables' list + ) + } + } + } + + private async syncTokenGraphIfEmpty (): Promise { + if (this.tokenGraph.size === 0) { + await this.syncTokenGraph() + } + } +} + +function computeReturnInDestinationToken (path: SwapPathPoolPair[], fromTokenId: string): BigNumber { + let total = new BigNumber(1) + for (const poolPair of path) { + if (fromTokenId === poolPair.tokenA.id) { + total = total.multipliedBy(poolPair.priceRatio.ba) + fromTokenId = poolPair.tokenB.id + } else { + total = total.multipliedBy(poolPair.priceRatio.ab) + fromTokenId = poolPair.tokenA.id + } + } + return total +} + +function formatNumber (number: BigNumber): string { + return number.eq(0) + ? '0' + : number.toFixed(8) +} diff --git a/apps/whale/src/module.api/rawtx.controller.e2e.ts b/apps/whale/src/module.api/rawtx.controller.e2e.ts index c014778227..447413455f 100644 --- a/apps/whale/src/module.api/rawtx.controller.e2e.ts +++ b/apps/whale/src/module.api/rawtx.controller.e2e.ts @@ -55,13 +55,11 @@ describe('test', () => { await controller.test({ hex: '0400000100881133bb11aa00cc' }) } catch (err) { expect(err).toBeInstanceOf(BadRequestApiException) - expect((err as BadRequestApiException).getResponse()).toStrictEqual({ - error: { - code: 400, - type: 'BadRequest', - message: 'Transaction decode failed', - at: expect.any(Number) - } + expect(err.response.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + message: 'Transaction decode failed', + at: expect.any(Number) }) } }) @@ -75,13 +73,11 @@ describe('test', () => { }) } catch (err) { expect(err).toBeInstanceOf(BadRequestApiException) - expect((err as BadRequestApiException).getResponse()).toStrictEqual({ - error: { - code: 400, - type: 'BadRequest', - at: expect.any(Number), - message: 'Transaction is not allowed to be inserted' - } + expect(err.response.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + message: 'Transaction is not allowed to be inserted' }) } }) @@ -123,13 +119,11 @@ describe('send', () => { }) } catch (err) { expect(err).toBeInstanceOf(BadRequestApiException) - expect((err as BadRequestApiException).getResponse()).toStrictEqual({ - error: { - code: 400, - type: 'BadRequest', - at: expect.any(Number), - message: 'Transaction decode failed' - } + expect(err.response.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + message: 'Transaction decode failed' }) } }) @@ -143,13 +137,11 @@ describe('send', () => { }) } catch (err) { expect(err).toBeInstanceOf(BadRequestApiException) - expect((err as BadRequestApiException).getResponse()).toStrictEqual({ - error: { - code: 400, - type: 'BadRequest', - at: expect.any(Number), - message: 'Absurdly high fee' - } + expect(err.response.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + message: 'Absurdly high fee' }) } }) diff --git a/apps/whale/src/module.api/rpc.controller.ts b/apps/whale/src/module.api/rpc.controller.ts index d725ce50b3..b4a56b129b 100644 --- a/apps/whale/src/module.api/rpc.controller.ts +++ b/apps/whale/src/module.api/rpc.controller.ts @@ -35,7 +35,9 @@ export class MethodWhitelist implements PipeTransform { 'getgov', 'validateaddress', 'listcommunitybalances', - 'getaccounthistory' + 'getaccounthistory', + 'getfutureswapblock', + 'getpendingfutureswaps' ] transform (value: string, metadata: ArgumentMetadata): string { diff --git a/apps/whale/src/module.api/stats.controller.ts b/apps/whale/src/module.api/stats.controller.ts index ea5240ab1e..1fb9ef7706 100644 --- a/apps/whale/src/module.api/stats.controller.ts +++ b/apps/whale/src/module.api/stats.controller.ts @@ -11,6 +11,7 @@ import { BlockchainInfo } from '@defichain/jellyfish-api-core/dist/category/bloc import { getBlockSubsidy } from './subsidy' import { BlockSubsidy } from '@defichain/jellyfish-network' import { BurnInfo } from '@defichain/jellyfish-api-core/dist/category/account' +import { GetLoanInfoResult } from '@defichain/jellyfish-api-core/dist/category/loan' @Controller('/stats') export class StatsController { @@ -28,17 +29,21 @@ export class StatsController { @Get() async get (): Promise { const block = requireValue(await this.blockMapper.getHighest(), 'block') - + const burned = await this.cachedGet('burned', this.getBurned.bind(this), 1806) + const burnedTotal = await this.getCachedBurnTotal() return { count: { ...await this.cachedGet('count', this.getCount.bind(this), 1801), blocks: block.height }, - burned: await this.cachedGet('burned', this.getBurned.bind(this), 1700), + burned: { + ...burned, + total: burnedTotal.toNumber() + }, tvl: await this.cachedGet('tvl', this.getTVL.bind(this), 310), price: await this.cachedGet('price', this.getPrice.bind(this), 220), masternodes: await this.cachedGet('masternodes', this.getMasternodes.bind(this), 325), - loan: await this.cachedGet('loan', this.getLoan.bind(this), 299), + loan: await this.getLoan(), emission: await this.cachedGet('emission', this.getEmission.bind(this), 1750), net: await this.cachedGet('net', this.getNet.bind(this), 1830), blockchain: { @@ -53,22 +58,32 @@ export class StatsController { const max = 1200000000 const total = this.blockSubsidy.getSupply(height).div(100000000) - const burned = await this.cachedGet('Controller.supply.getBurnedTotal', this.getBurnedTotal.bind(this), 1806) - const circulating = total.minus(burned) + const burnedTotal = await this.getCachedBurnTotal() + const circulating = total.minus(burnedTotal) return { max: max, total: total.gt(max) ? max : total.toNumber(), // as emission burn is taken into the 1.2b calculation post eunos - burned: burned.toNumber(), + burned: burnedTotal.toNumber(), circulating: circulating.toNumber() } } @Get('/burn') - async getBurn (): Promise { - return await this.cachedGet('Controller.burn.getBurnInfo', async () => { + async getBurnInfo (): Promise { + return await this.cachedGet('Controller.stats.getBurnInfo', async () => { return await this.rpcClient.account.getBurnInfo() - }, 123) + }, 666) + } + + private async getLoanInfo (): Promise { + return await this.cachedGet('Controller.stats.getLoanInfo', async () => { + return await this.rpcClient.loan.getLoanInfo() + }, 299) + } + + private async getCachedBurnTotal (): Promise { + return await this.cachedGet('Controller.supply.getBurnedTotal', this.getBurnedTotal.bind(this), 1899) } private async cachedGet (field: string, fetch: () => Promise, ttl: number): Promise { @@ -124,7 +139,7 @@ export class StatsController { } private async getBurned (): Promise { - const burnInfo = await this.rpcClient.account.getBurnInfo() + const burnInfo = await this.getBurnInfo() const utxo = burnInfo.amount const account = findTokenBalance(burnInfo.tokens, 'DFI') @@ -152,12 +167,13 @@ export class StatsController { private async getBurnedTotal (): Promise { const address = '76a914f7874e8821097615ec345f74c7e5bcf61b12e2ee88ac' const tokens = await this.rpcClient.account.getAccount(address) - const burnInfo = await this.rpcClient.account.getBurnInfo() + const burnInfo = await this.getBurnInfo() const utxo = burnInfo.amount const account = findTokenBalance(tokens, 'DFI') const emission = burnInfo.emissionburn - return utxo.plus(account).plus(emission) + const fee = burnInfo.feeburn + return utxo.plus(account).plus(emission).plus(fee) } private async getPrice (): Promise { @@ -195,7 +211,7 @@ export class StatsController { } private async getLoan (): Promise { - const info = await this.rpcClient.loan.getLoanInfo() + const info = await this.getLoanInfo() return { count: { diff --git a/apps/whale/src/module.api/transaction.controller.e2e.ts b/apps/whale/src/module.api/transaction.controller.e2e.ts index 434982d392..e0526d6db7 100644 --- a/apps/whale/src/module.api/transaction.controller.e2e.ts +++ b/apps/whale/src/module.api/transaction.controller.e2e.ts @@ -85,7 +85,7 @@ describe('get', () => { await controller.get('invalidtransactionid') } catch (err) { expect(err).toBeInstanceOf(NotFoundException) - expect((err as NotFoundException).getResponse()).toStrictEqual({ + expect(err.response).toStrictEqual({ statusCode: 404, message: 'transaction not found', error: 'Not Found' diff --git a/apps/whale/src/module.database/provider.level/level.database.spec.ts b/apps/whale/src/module.database/provider.level/level.database.spec.ts index 950cbf1d24..1c1e4c11dc 100644 --- a/apps/whale/src/module.database/provider.level/level.database.spec.ts +++ b/apps/whale/src/module.database/provider.level/level.database.spec.ts @@ -1,7 +1,7 @@ import { Test } from '@nestjs/testing' import { ConfigModule } from '@nestjs/config' import { LevelDatabaseModule } from './module' -import * as spec from '../../module.database/database.spec/specifications' +import * as spec from '../database.spec/specifications' import { Database } from '../database' import { LevelDatabase } from './level.database' diff --git a/apps/whale/src/module.database/provider.memory/memory.database.spec.ts b/apps/whale/src/module.database/provider.memory/memory.database.spec.ts index ea0fe0d686..975e3f83bc 100644 --- a/apps/whale/src/module.database/provider.memory/memory.database.spec.ts +++ b/apps/whale/src/module.database/provider.memory/memory.database.spec.ts @@ -1,6 +1,6 @@ import { Test } from '@nestjs/testing' import { MemoryDatabaseModule } from './module' -import * as spec from '../../module.database/database.spec/specifications' +import * as spec from '../database.spec/specifications' import { Database } from '../database' import { LevelDatabase } from '../provider.level/level.database' diff --git a/apps/whale/src/module.indexer/model/script.activity.ts b/apps/whale/src/module.indexer/model/script.activity.ts index 5a9094ea15..255ab9568c 100644 --- a/apps/whale/src/module.indexer/model/script.activity.ts +++ b/apps/whale/src/module.indexer/model/script.activity.ts @@ -1,4 +1,4 @@ -import { defid, Indexer, RawBlock } from '../../module.indexer/model/_abstract' +import { defid, Indexer, RawBlock } from './_abstract' import { ScriptActivity, ScriptActivityMapper } from '../../module.model/script.activity' import { Injectable } from '@nestjs/common' import { HexEncoder } from '../../module.model/_hex.encoder' diff --git a/apps/whale/src/module.indexer/model/script.unspent.ts b/apps/whale/src/module.indexer/model/script.unspent.ts index 0daae9958c..36c2d3911c 100644 --- a/apps/whale/src/module.indexer/model/script.unspent.ts +++ b/apps/whale/src/module.indexer/model/script.unspent.ts @@ -1,5 +1,5 @@ import { ScriptUnspent, ScriptUnspentMapper } from '../../module.model/script.unspent' -import { defid, Indexer, RawBlock } from '../../module.indexer/model/_abstract' +import { defid, Indexer, RawBlock } from './_abstract' import { Injectable } from '@nestjs/common' import { HexEncoder } from '../../module.model/_hex.encoder' import { TransactionVout, TransactionVoutMapper } from '../../module.model/transaction.vout' diff --git a/apps/whale/src/module.indexer/model/transaction.ts b/apps/whale/src/module.indexer/model/transaction.ts index 1ecb965729..b6b254fbd2 100644 --- a/apps/whale/src/module.indexer/model/transaction.ts +++ b/apps/whale/src/module.indexer/model/transaction.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common' -import { defid, Indexer, RawBlock } from '../../module.indexer/model/_abstract' +import { defid, Indexer, RawBlock } from './_abstract' import { Transaction, TransactionMapper } from '../../module.model/transaction' import BigNumber from 'bignumber.js' diff --git a/apps/whale/src/module.indexer/model/transaction.vin.ts b/apps/whale/src/module.indexer/model/transaction.vin.ts index 16ebba6269..20a3840f08 100644 --- a/apps/whale/src/module.indexer/model/transaction.vin.ts +++ b/apps/whale/src/module.indexer/model/transaction.vin.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common' -import { defid, Indexer, RawBlock } from '../../module.indexer/model/_abstract' +import { defid, Indexer, RawBlock } from './_abstract' import { TransactionVin, TransactionVinMapper } from '../../module.model/transaction.vin' import { TransactionVout } from '../../module.model/transaction.vout' import { HexEncoder } from '../../module.model/_hex.encoder' diff --git a/apps/whale/src/module.indexer/model/transaction.vout.ts b/apps/whale/src/module.indexer/model/transaction.vout.ts index ecb6287014..996cf555eb 100644 --- a/apps/whale/src/module.indexer/model/transaction.vout.ts +++ b/apps/whale/src/module.indexer/model/transaction.vout.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common' -import { Indexer, defid, RawBlock } from '../../module.indexer/model/_abstract' +import { Indexer, defid, RawBlock } from './_abstract' import { TransactionVout, TransactionVoutMapper } from '../../module.model/transaction.vout' import { HexEncoder } from '../../module.model/_hex.encoder'