diff --git a/src/module.api/cache/defid.cache.spec.ts b/src/module.api/cache/defid.cache.spec.ts index 774f4bdbb..6f3a59ba0 100644 --- a/src/module.api/cache/defid.cache.spec.ts +++ b/src/module.api/cache/defid.cache.spec.ts @@ -8,6 +8,8 @@ import { PoolPairInfo } from '@defichain/jellyfish-api-core/dist/category/poolpa import { TokenInfo } from '@defichain/jellyfish-api-core/dist/category/token' import { Cache } from 'cache-manager' import { CachePrefix } from '@src/module.api/cache/global.cache' +import { Testing } from '@defichain/jellyfish-testing' +import BigNumber from 'bignumber.js' const container = new MasterNodeRegTestContainer() let client: JsonRpcClient @@ -178,3 +180,80 @@ describe('getTokenInfo', () => { expect(dfi).toBeUndefined() }) }) + +describe('getStockLpRewardPct', () => { + beforeAll(async () => { + await container.start() + await container.waitForReady() + await container.waitForWalletCoinbaseMaturity() + client = new JsonRpcClient(await container.getCachedRpcUrl()) + + testingModule = await Test.createTestingModule({ + imports: [CacheModule.register()], + providers: [ + { provide: JsonRpcClient, useValue: client }, + DeFiDCache + ] + }).compile() + + cache = testingModule.get(CACHE_MANAGER) + defiCache = testingModule.get(DeFiDCache) + + await container.waitForWalletBalanceGTE(110) + + await createToken(container, 'BAT') // 1 + await container.generate(1) + + await createPoolPair(container, 'BAT', 'DFI') // 2 + await container.generate(1) + + // loan pool pair setup + const testing = Testing.create(container) + const oracleId = await container.call('appointoracle', [await testing.generateAddress(), [ + { token: 'lA', currency: 'USD' }, // 3 + { token: 'lB', currency: 'USD' } // 4 + ], 1]) + await testing.generate(1) + + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [ + { tokenAmount: '1@lA', currency: 'USD' }, // 5 + { tokenAmount: '2@lB', currency: 'USD' } // 6 + ] + }) + await testing.generate(1) + + const loanTokens = ['lA', 'lB'] + for (const lt of loanTokens) { + await testing.container.call('setloantoken', [{ + symbol: lt, + fixedIntervalPriceId: `${lt}/USD`, + mintable: false, + interest: new BigNumber(0.02) + }]) + await testing.generate(1) + } + + for (const lt of loanTokens) { + await testing.poolpair.create({ tokenA: lt, tokenB: 'DFI' }) + await testing.generate(1) + } + + await container.call('setgov', [{ LP_LOAN_TOKEN_SPLITS: { 5: 1 } }]) + await testing.generate(1) + + // calling with non loan token pp returned with assumed zero % + const nonLoanLpPct = await defiCache.getStockLpRewardPct('3') + expect(nonLoanLpPct.toFixed()).toStrictEqual('0') + + await container.stop() + }) + + it('should get from cache via get as container RPC is killed', async () => { + const lA = await defiCache.getStockLpRewardPct('5') + expect(lA.toFixed()).toStrictEqual('1') + + const lB = await defiCache.getStockLpRewardPct('6') + expect(lB.toFixed()).toStrictEqual('0') // naturally zero when govvar have no value for this id + }) +}) diff --git a/src/module.api/cache/defid.cache.ts b/src/module.api/cache/defid.cache.ts index 994793268..5282629ba 100644 --- a/src/module.api/cache/defid.cache.ts +++ b/src/module.api/cache/defid.cache.ts @@ -5,6 +5,7 @@ import { TokenInfo, TokenResult } from '@defichain/jellyfish-api-core/dist/categ import { CachePrefix, GlobalCache } from '@src/module.api/cache/global.cache' import { PoolPairInfo } from '@defichain/jellyfish-api-core/dist/category/poolpair' import { GetLoanSchemeResult } from '@defichain/jellyfish-api-core/dist/category/loan' +import BigNumber from 'bignumber.js' @Injectable() export class DeFiDCache extends GlobalCache { @@ -76,4 +77,31 @@ export class DeFiDCache extends GlobalCache { throw err } } + + async getStockLpRewardPct (poolId: string): Promise { + const all = (await this.get>( + CachePrefix.GOVERNANCE, + 'LP_LOAN_TOKEN_SPLITS', + this.fetchAllStockLpRewardPct.bind(this)) + ) as Record + + const thisPool = all[poolId] + return thisPool === undefined ? new BigNumber(0) : new BigNumber(thisPool) + } + + private async fetchAllStockLpRewardPct (): Promise> { + const { LP_LOAN_TOKEN_SPLITS: rewardPct } = await this.rpcClient.masternode.getGov('LP_LOAN_TOKEN_SPLITS') + if (rewardPct === undefined) { + // unexpected (absolutely existed in prod) + throw new Error('LP_LOAN_TOKEN_SPLITS govvar missing') + } + + const tokenIds = Object.keys(rewardPct) + const result: Record = {} + tokenIds.forEach(t => { + result[t] = new BigNumber(rewardPct[t]) + }) + + return result + } } diff --git a/src/module.api/cache/global.cache.ts b/src/module.api/cache/global.cache.ts index d7dd1487d..0c3c4eef6 100644 --- a/src/module.api/cache/global.cache.ts +++ b/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, + GOVERNANCE = 4 } export class GlobalCache { diff --git a/src/module.api/poolpair.controller.e2e.ts b/src/module.api/poolpair.controller.e2e.ts index a7e8f5a94..34c3a7050 100644 --- a/src/module.api/poolpair.controller.e2e.ts +++ b/src/module.api/poolpair.controller.e2e.ts @@ -4,6 +4,8 @@ import { NestFastifyApplication } from '@nestjs/platform-fastify' import { createTestingApp, stopTestingApp, waitForIndexedHeight } from '@src/e2e.module' import { addPoolLiquidity, createPoolPair, createToken, getNewAddress, mintTokens } from '@defichain/testing' import { NotFoundException } from '@nestjs/common' +import { Testing } from '@defichain/jellyfish-testing' +import BigNumber from 'bignumber.js' const container = new MasterNodeRegTestContainer() let app: NestFastifyApplication @@ -115,6 +117,49 @@ async function setup (): Promise { await container.call('setgov', [{ LP_SPLITS: { 14: 1.0 } }]) await container.generate(1) + + // loan token LP setup + const testing = Testing.create(container) + const loanTokens = ['lA', 'lB', 'lC'] + const oracleId = await container.call('appointoracle', [await testing.generateAddress(), [ + { token: 'lA', currency: 'USD' }, + { token: 'lB', currency: 'USD' }, + { token: 'lC', currency: 'USD' } + ], 1]) + await testing.generate(1) + + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [ + { tokenAmount: '1@lA', currency: 'USD' }, + { tokenAmount: '2@lB', currency: 'USD' }, + { tokenAmount: '3@lC', currency: 'USD' } + ] + }) + await testing.generate(1) + + for (const lt of loanTokens) { + await testing.container.call('setloantoken', [{ + symbol: lt, + fixedIntervalPriceId: `${lt}/USD`, + mintable: false, + interest: new BigNumber(0.02) + }]) + await testing.generate(1) + } + + for (const lt of loanTokens) { + await testing.poolpair.create({ tokenA: lt, tokenB: 'DFI' }) + await testing.generate(1) + } + + await container.call('setgov', [{ + LP_LOAN_TOKEN_SPLITS: { + // 29: 0, + 30: 0.4, + 31: 0.6 + } + }]) + await testing.generate(1) } describe('list', () => { @@ -123,7 +168,7 @@ describe('list', () => { size: 30 }) - expect(response.data.length).toStrictEqual(12) + expect(response.data.length).toStrictEqual(15) expect(response.page).toBeUndefined() expect(response.data[1]).toStrictEqual({ @@ -173,6 +218,13 @@ describe('list', () => { h24: 0 } }) + + expect(response.data[12].symbol).toStrictEqual('lA-DFI') + expect(response.data[12].rewardPct).toStrictEqual('0') + expect(response.data[13].symbol).toStrictEqual('lB-DFI') + expect(response.data[13].rewardPct).toStrictEqual('0.4') + expect(response.data[14].symbol).toStrictEqual('lC-DFI') + expect(response.data[14].rewardPct).toStrictEqual('0.6') }) it('should list with pagination', async () => { @@ -185,11 +237,11 @@ describe('list', () => { expect(first.data[1].symbol).toStrictEqual('B-DFI') const next = await controller.list({ - size: 11, + size: 14, next: first.page?.next }) - expect(next.data.length).toStrictEqual(10) + expect(next.data.length).toStrictEqual(13) expect(next.page?.next).toBeUndefined() expect(next.data[0].symbol).toStrictEqual('C-DFI') expect(next.data[1].symbol).toStrictEqual('D-DFI') @@ -261,11 +313,64 @@ describe('get', () => { }) }) + it('loan token pool "rewardPct" should use gov value', async () => { + const response = await controller.get('31') + + expect(response).toStrictEqual({ + id: '31', + symbol: 'lC-DFI', + displaySymbol: 'dlC-DFI', + name: '-Default Defi token', + status: true, + tokenA: { + id: expect.any(String), + symbol: 'lC', + reserve: '0', + blockCommission: '0', + displaySymbol: 'dlC' + }, + tokenB: { + id: '0', + symbol: 'DFI', + reserve: '0', + blockCommission: '0', + displaySymbol: 'DFI' + }, + apr: { + // legit empty pool value + reward: Infinity, + total: NaN, + commission: NaN + }, + commission: '0', + totalLiquidity: { + token: '0', + usd: '0' + }, + tradeEnabled: false, + ownerAddress: expect.any(String), + priceRatio: { + ab: '0', + ba: '0' + }, + rewardPct: '0.6', + customRewards: undefined, + creation: { + tx: expect.any(String), + height: expect.any(Number) + }, + volume: { + d30: 0, + h24: 0 + } + }) + }) + it('should throw error while getting non-existent poolpair', async () => { expect.assertions(2) try { await controller.get('999') - } catch (err) { + } catch (err: any) { expect(err).toBeInstanceOf(NotFoundException) expect(err.response).toStrictEqual({ statusCode: 404, @@ -679,6 +784,8 @@ describe('get list swappable tokens', () => { swappableTokens: [ { id: '7', symbol: 'G', displaySymbol: 'dG' }, { id: '0', symbol: 'DFI', displaySymbol: 'DFI' }, + { id: '27', symbol: 'lB', displaySymbol: 'dlB' }, + { id: '26', symbol: 'lA', displaySymbol: 'dlA' }, { id: '24', symbol: 'USDT', displaySymbol: 'dUSDT' }, { id: '6', symbol: 'F', displaySymbol: 'dF' }, { id: '5', symbol: 'E', displaySymbol: 'dE' }, diff --git a/src/module.api/poolpair.controller.ts b/src/module.api/poolpair.controller.ts index 7536b619b..8af606647 100644 --- a/src/module.api/poolpair.controller.ts +++ b/src/module.api/poolpair.controller.ts @@ -52,7 +52,8 @@ export class PoolPairController { const totalLiquidityUsd = await this.poolPairService.getTotalLiquidityUsd(info) const apr = await this.poolPairService.getAPR(id, info) const volume = await this.poolPairService.getUSDVolume(id) - items.push(mapPoolPair(id, info, totalLiquidityUsd, apr, volume)) + const rewardPct = await this.poolPairService.getRewardPct(id, info) + items.push(mapPoolPair(id, info, totalLiquidityUsd, apr, volume, rewardPct)) } const response = ApiPagedResponse.of(items, query.size, item => { @@ -78,7 +79,8 @@ export class PoolPairController { const totalLiquidityUsd = await this.poolPairService.getTotalLiquidityUsd(info) const apr = await this.poolPairService.getAPR(id, info) const volume = await this.poolPairService.getUSDVolume(id) - return mapPoolPair(String(id), info, totalLiquidityUsd, apr, volume) + const rewardPct = await this.poolPairService.getRewardPct(id, info) + return mapPoolPair(String(id), info, totalLiquidityUsd, apr, volume, rewardPct) } /** @@ -184,7 +186,7 @@ export class PoolPairController { } } -function mapPoolPair (id: string, info: PoolPairInfo, totalLiquidityUsd?: BigNumber, apr?: PoolPairData['apr'], volume?: PoolPairData['volume']): PoolPairData { +function mapPoolPair (id: string, info: PoolPairInfo, totalLiquidityUsd?: BigNumber, apr?: PoolPairData['apr'], volume?: PoolPairData['volume'], rewardPct?: BigNumber): PoolPairData { const [symbolA, symbolB] = info.symbol.split('-') return { @@ -218,7 +220,7 @@ function mapPoolPair (id: string, info: PoolPairInfo, totalLiquidityUsd?: BigNum }, tradeEnabled: info.tradeEnabled, ownerAddress: info.ownerAddress, - rewardPct: info.rewardPct.toFixed(), + rewardPct: rewardPct?.toFixed() ?? info.rewardPct.toFixed(), customRewards: info.customRewards, creation: { tx: info.creationTx, diff --git a/src/module.api/poolpair.service.ts b/src/module.api/poolpair.service.ts index be49cfd9e..33abe3bff 100644 --- a/src/module.api/poolpair.service.ts +++ b/src/module.api/poolpair.service.ts @@ -60,7 +60,7 @@ export class PoolPairService { if (Object.values(result).length > 0) { return Object.values(result)[0] } - } catch (err) { + } catch (err: any) { if (err?.payload?.message !== 'Pool not found') { throw err } @@ -71,7 +71,7 @@ export class PoolPairService { if (Object.values(result).length > 0) { return Object.values(result)[0] } - } catch (err) { + } catch (err: any) { if (err?.payload?.message !== 'Pool not found') { throw err } @@ -387,6 +387,23 @@ export class PoolPairService { total: reward.plus(commission).toNumber() } } + + async getRewardPct (id: string, info: PoolPairInfo): Promise { + if (!info.rewardPct.isZero()) { + return info.rewardPct + } + + const token = await this.deFiDCache.getTokenInfo(info.idTokenA) + if (token === undefined) { + throw new Error(`Pool ${id} not found`) + } + + if (!token.isLoanToken) { + return new BigNumber(0) + } + + return await this.deFiDCache.getStockLpRewardPct(id) + } } function findPoolSwapDfTx (vouts: TransactionVout[]): PoolSwapDfTx | undefined {