From c5e051c863013dffcadaedf4ac53717a5010321f Mon Sep 17 00:00:00 2001 From: jingyi2811 Date: Wed, 27 Oct 2021 15:43:46 +0800 Subject: [PATCH] Add loanToken get and list endpoints (#435) * Add loanToken endpoint * Fix test * Apply changes as per code review of another PR (loan scheme) * Fix wrong docker image * Quick push * Quick push * Quick push switch branch * Stable version * Fix type * Fix type * Remove unused id * Don't use any * Improve code * Change string or number to Bignumber * Fix failed test * Fix failed test * Revert "Change string or number to Bignumber" This reverts commit 97e931e960813f5deed5b30093fa65e2d75fc379. * Don't use BigNumber. Use string * Round number => number. Decimals => string * added `token: TokenData` to LoanToken and CollateralToken Co-authored-by: Fuxing Loh --- .../__tests__/api/loan.collateral.test.ts | 66 +++++- .../__tests__/api/loan.token.test.ts | 220 ++++++++++++++++++ packages/whale-api-client/src/api/loan.ts | 37 ++- src/module.api/cache/defid.cache.ts | 17 +- src/module.api/cache/global.cache.ts | 3 +- .../loan.collateral.controller.e2e.ts | 85 ++++--- src/module.api/loan.controller.ts | 98 ++++++-- src/module.api/loan.token.controller.e2e.ts | 190 +++++++++++++++ src/module.api/token.controller.ts | 2 +- 9 files changed, 657 insertions(+), 61 deletions(-) create mode 100644 packages/whale-api-client/__tests__/api/loan.token.test.ts create mode 100644 src/module.api/loan.token.controller.e2e.ts diff --git a/packages/whale-api-client/__tests__/api/loan.collateral.test.ts b/packages/whale-api-client/__tests__/api/loan.collateral.test.ts index 99b27f4c8..c4b4c421d 100644 --- a/packages/whale-api-client/__tests__/api/loan.collateral.test.ts +++ b/packages/whale-api-client/__tests__/api/loan.collateral.test.ts @@ -108,21 +108,44 @@ afterAll(async () => { describe('list', () => { it('should listCollateralTokens', async () => { - const result = await client.loan.listCollateral() + const result = await client.loan.listCollateralToken() expect(result.length).toStrictEqual(4) // Not deterministic ordering due to use of id expect(result[0]).toStrictEqual({ - token: expect.any(String), tokenId: expect.any(String), priceFeedId: expect.any(String), factor: expect.any(String), - activateAfterBlock: expect.any(Number) + activateAfterBlock: expect.any(Number), + token: { + collateralAddress: expect.any(String), + creation: { + height: expect.any(Number), + tx: expect.any(String) + }, + decimal: 8, + destruction: { + height: -1, + tx: expect.any(String) + }, + displaySymbol: expect.any(String), + finalized: false, + id: expect.any(String), + isDAT: true, + isLPS: false, + limit: '0', + mintable: true, + minted: '0', + name: expect.any(String), + symbol: expect.any(String), + symbolKey: expect.any(String), + tradeable: true + } }) }) - it('should listLoanSchemes with pagination', async () => { - const first = await client.loan.listCollateral(2) + it('should listCollateral with pagination', async () => { + const first = await client.loan.listCollateralToken(2) expect(first.length).toStrictEqual(2) expect(first.hasNext).toStrictEqual(true) @@ -144,20 +167,43 @@ describe('list', () => { describe('get', () => { it('should get collateral token by symbol', async () => { - const data = await client.loan.getCollateral('AAPL') + const data = await client.loan.getCollateralToken('AAPL') expect(data).toStrictEqual({ - token: 'AAPL', tokenId: collateralTokenId1, factor: '0.1', priceFeedId: 'AAPL/USD', - activateAfterBlock: 108 + activateAfterBlock: 108, + token: { + collateralAddress: expect.any(String), + creation: { + height: expect.any(Number), + tx: expect.any(String) + }, + decimal: 8, + destruction: { + height: -1, + tx: expect.any(String) + }, + displaySymbol: 'dAAPL', + finalized: false, + id: expect.any(String), + isDAT: true, + isLPS: false, + limit: '0', + mintable: true, + minted: '0', + name: 'AAPL', + symbol: 'AAPL', + symbolKey: expect.any(String), + tradeable: true + } }) }) it('should fail due to getting non-existent or malformed collateral token id', async () => { expect.assertions(4) try { - await client.loan.getCollateral('999') + await client.loan.getCollateralToken('999') } catch (err) { expect(err).toBeInstanceOf(WhaleApiException) expect(err.error).toStrictEqual({ @@ -170,7 +216,7 @@ describe('get', () => { } try { - await client.loan.getCollateral('$*@') + await client.loan.getCollateralToken('$*@') } catch (err) { expect(err).toBeInstanceOf(WhaleApiException) expect(err.error).toStrictEqual({ diff --git a/packages/whale-api-client/__tests__/api/loan.token.test.ts b/packages/whale-api-client/__tests__/api/loan.token.test.ts new file mode 100644 index 000000000..2f8420603 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/loan.token.test.ts @@ -0,0 +1,220 @@ +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiClient, WhaleApiException } from '../../src' +import BigNumber from 'bignumber.js' +import { Testing } from '@defichain/jellyfish-testing' +import { LoanMasterNodeRegTestContainer } from '@defichain/testcontainers' + +let container: LoanMasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient + +beforeAll(async () => { + container = new LoanMasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + const testing = Testing.create(container) + + const oracleId = await testing.container.call('appointoracle', [await testing.generateAddress(), [ + { token: 'AAPL', currency: 'USD' }, + { token: 'TSLA', currency: 'USD' }, + { token: 'MSFT', currency: 'USD' }, + { token: 'FB', currency: 'USD' } + ], 1]) + await testing.generate(1) + + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '1.5@AAPL', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '2.5@TSLA', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '3.5@MSFT', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '4.5@FB', + currency: 'USD' + }] + }) + await testing.generate(1) + + await testing.container.call('setloantoken', [{ + symbol: 'AAPL', + fixedIntervalPriceId: 'AAPL/USD', + mintable: false, + interest: new BigNumber(0.01) + }]) + await testing.generate(1) + + await testing.container.call('setloantoken', [{ + symbol: 'TSLA', + fixedIntervalPriceId: 'TSLA/USD', + mintable: false, + interest: new BigNumber(0.02) + }]) + await testing.generate(1) + + await testing.container.call('setloantoken', [{ + symbol: 'MSFT', + fixedIntervalPriceId: 'MSFT/USD', + mintable: false, + interest: new BigNumber(0.03) + }]) + await testing.generate(1) + + await testing.container.call('setloantoken', [{ + symbol: 'FB', + fixedIntervalPriceId: 'FB/USD', + mintable: false, + interest: new BigNumber(0.04) + }]) + await testing.generate(1) +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +describe('list', () => { + it('should listLoanTokens', async () => { + const result = await client.loan.listLoanToken() + expect(result.length).toStrictEqual(4) + expect(result[0]).toStrictEqual({ + tokenId: expect.any(String), + interest: expect.any(String), + fixedIntervalPriceId: expect.any(String), + token: { + collateralAddress: expect.any(String), + creation: { + height: expect.any(Number), + tx: expect.any(String) + }, + decimal: 8, + destruction: { + height: -1, + tx: expect.any(String) + }, + displaySymbol: expect.any(String), + finalized: false, + id: expect.any(String), + isDAT: true, + isLPS: false, + limit: '0', + mintable: false, + minted: '0', + name: '', + symbol: expect.any(String), + symbolKey: expect.any(String), + tradeable: true + } + }) + + expect(result[1].tokenId.length).toStrictEqual(64) + expect(result[2].tokenId.length).toStrictEqual(64) + expect(result[3].tokenId.length).toStrictEqual(64) + }) + + it('should listLoanTokens with pagination', async () => { + const first = await client.loan.listLoanToken(2) + + expect(first.length).toStrictEqual(2) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken?.length).toStrictEqual(64) + + const next = await client.paginate(first) + + expect(next.length).toStrictEqual(2) + expect(next.hasNext).toStrictEqual(true) + expect(next.nextToken?.length).toStrictEqual(64) + + const last = await client.paginate(next) + + expect(last.length).toStrictEqual(0) + expect(last.hasNext).toStrictEqual(false) + expect(last.nextToken).toBeUndefined() + }) +}) + +describe('get', () => { + it('should get loan token by symbol', async () => { + const data = await client.loan.getLoanToken('AAPL') + expect(data).toStrictEqual({ + tokenId: expect.any(String), + fixedIntervalPriceId: 'AAPL/USD', + interest: '0.01', + token: { + collateralAddress: expect.any(String), + creation: { + height: expect.any(Number), + tx: expect.any(String) + }, + decimal: 8, + destruction: { + height: -1, + tx: expect.any(String) + }, + displaySymbol: 'dAAPL', + finalized: false, + id: '1', + isDAT: true, + isLPS: false, + limit: '0', + mintable: false, + minted: '0', + name: '', + symbol: 'AAPL', + symbolKey: 'AAPL', + tradeable: true + } + }) + }) + + it('should fail due to getting non-existent or malformed loan token id', async () => { + expect.assertions(4) + try { + await client.loan.getLoanToken('999') + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find loan token', + url: '/v0.0/regtest/loans/tokens/999' + }) + } + + try { + await client.loan.getLoanToken('$*@') + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find loan token', + url: '/v0.0/regtest/loans/tokens/$*@' + }) + } + }) +}) diff --git a/packages/whale-api-client/src/api/loan.ts b/packages/whale-api-client/src/api/loan.ts index 0603ab7ab..6a9c4de71 100644 --- a/packages/whale-api-client/src/api/loan.ts +++ b/packages/whale-api-client/src/api/loan.ts @@ -1,5 +1,6 @@ import { WhaleApiClient } from '../whale.api.client' import { ApiPagedResponse } from '../whale.api.response' +import { TokenData } from './tokens' export class Loan { constructor (private readonly client: WhaleApiClient) { @@ -33,19 +34,40 @@ export class Loan { * @param {string} next set of collateral tokens * @return {Promise>} */ - async listCollateral (size: number = 30, next?: string): Promise> { + async listCollateralToken (size: number = 30, next?: string): Promise> { return await this.client.requestList('GET', 'loans/collaterals', size, next) } /** - * Get information about a collateral token with given collateralToken id. + * Get information about a collateral token with given collateral token id. * * @param {string} id collateralToken id to get * @return {Promise} */ - async getCollateral (id: string): Promise { + async getCollateralToken (id: string): Promise { return await this.client.requestData('GET', `loans/collaterals/${id}`) } + + /** + * Paginate query loan tokens. + * + * @param {number} size of loan token to query + * @param {string} next set of loan tokens + * @return {Promise>} + */ + async listLoanToken (size: number = 30, next?: string): Promise> { + return await this.client.requestList('GET', 'loans/tokens', size, next) + } + + /** + * Get information about a loan token with given loan token id. + * + * @param {string} id loanToken id to get + * @return {Promise} + */ + async getLoanToken (id: string): Promise { + return await this.client.requestData('GET', `loans/tokens/${id}`) + } } export interface LoanScheme { @@ -55,9 +77,16 @@ export interface LoanScheme { } export interface CollateralToken { - token: string tokenId: string + token: TokenData factor: string priceFeedId: string activateAfterBlock: number } + +export interface LoanToken { + tokenId: string + token: TokenData + interest: string + fixedIntervalPriceId: string +} diff --git a/src/module.api/cache/defid.cache.ts b/src/module.api/cache/defid.cache.ts index e304f2a66..39e4608e4 100644 --- a/src/module.api/cache/defid.cache.ts +++ b/src/module.api/cache/defid.cache.ts @@ -1,7 +1,7 @@ import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common' import { Cache } from 'cache-manager' import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' -import { TokenInfo } from '@defichain/jellyfish-api-core/dist/category/token' +import { TokenInfo, TokenResult } from '@defichain/jellyfish-api-core/dist/category/token' import { CachePrefix, GlobalCache } from '@src/module.api/cache/global.cache' import { PoolPairInfo } from '@defichain/jellyfish-api-core/dist/category/poolpair' @@ -18,6 +18,9 @@ export class DeFiDCache extends GlobalCache { return await this.batch(CachePrefix.TOKEN_INFO, ids, this.fetchTokenInfo.bind(this)) } + /** + * @param {string} id numeric id of token + */ async getTokenInfo (id: string): Promise { return await this.get(CachePrefix.TOKEN_INFO, id, this.fetchTokenInfo.bind(this)) } @@ -33,6 +36,18 @@ export class DeFiDCache extends GlobalCache { return result[id] } + async batchTokenInfoBySymbol (symbols: string[]): Promise> { + return await this.batch(CachePrefix.TOKEN_INFO_SYMBOL, symbols, this.fetchTokenInfoBySymbol.bind(this)) + } + + async getTokenInfoBySymbol (symbol: string): Promise { + return await this.get(CachePrefix.TOKEN_INFO_SYMBOL, symbol, this.fetchTokenInfoBySymbol.bind(this)) + } + + private async fetchTokenInfoBySymbol (symbol: string): Promise { + return await this.rpcClient.token.getToken(symbol) + } + async getPoolPairInfo (id: string): Promise { return await this.get(CachePrefix.POOL_PAIR_INFO, id, this.fetchPoolPairInfo.bind(this)) } diff --git a/src/module.api/cache/global.cache.ts b/src/module.api/cache/global.cache.ts index 1e0d63d91..dea4018ed 100644 --- a/src/module.api/cache/global.cache.ts +++ b/src/module.api/cache/global.cache.ts @@ -7,7 +7,8 @@ export interface CacheOption { export enum CachePrefix { SEMAPHORE = -1, TOKEN_INFO = 0, - POOL_PAIR_INFO = 1 + POOL_PAIR_INFO = 1, + TOKEN_INFO_SYMBOL = 2 } export class GlobalCache { diff --git a/src/module.api/loan.collateral.controller.e2e.ts b/src/module.api/loan.collateral.controller.e2e.ts index 10c76fbfa..267e72570 100644 --- a/src/module.api/loan.collateral.controller.e2e.ts +++ b/src/module.api/loan.collateral.controller.e2e.ts @@ -105,36 +105,36 @@ describe('list', () => { it('should listCollateralTokens', async () => { const result = await controller.listCollateral({ size: 100 }) expect(result.data.length).toStrictEqual(4) - expect(result.data).toStrictEqual([ - { - token: expect.any(String), - tokenId: expect.any(String), - priceFeedId: expect.any(String), - factor: expect.any(String), - activateAfterBlock: expect.any(Number) - }, - { - token: expect.any(String), - tokenId: expect.any(String), - priceFeedId: expect.any(String), - factor: expect.any(String), - activateAfterBlock: expect.any(Number) - }, - { - token: expect.any(String), - tokenId: expect.any(String), - priceFeedId: expect.any(String), - factor: expect.any(String), - activateAfterBlock: expect.any(Number) - }, - { - token: expect.any(String), - tokenId: expect.any(String), - priceFeedId: expect.any(String), - factor: expect.any(String), - activateAfterBlock: expect.any(Number) + expect(result.data[0]).toStrictEqual({ + tokenId: expect.any(String), + priceFeedId: expect.any(String), + factor: expect.any(String), + activateAfterBlock: expect.any(Number), + token: { + collateralAddress: expect.any(String), + creation: { + height: expect.any(Number), + tx: expect.any(String) + }, + decimal: 8, + destruction: { + height: -1, + tx: expect.any(String) + }, + displaySymbol: expect.any(String), + finalized: false, + id: expect.any(String), + isDAT: true, + isLPS: false, + limit: '0', + mintable: true, + minted: '0', + name: expect.any(String), + symbol: expect.any(String), + symbolKey: expect.any(String), + tradeable: true } - ]) + }) }) it('should listCollateralTokens with pagination', async () => { @@ -174,10 +174,33 @@ describe('get', () => { expect(data).toStrictEqual( { tokenId: collateralTokenId1, - token: 'AAPL', priceFeedId: 'AAPL/USD', factor: '0.1', - activateAfterBlock: 108 + activateAfterBlock: 108, + token: { + collateralAddress: expect.any(String), + creation: { + height: expect.any(Number), + tx: expect.any(String) + }, + decimal: 8, + destruction: { + height: -1, + tx: expect.any(String) + }, + displaySymbol: 'dAAPL', + finalized: false, + id: expect.any(String), + isDAT: true, + isLPS: false, + limit: '0', + mintable: true, + minted: '0', + name: 'AAPL', + symbol: 'AAPL', + symbolKey: expect.any(String), + tradeable: true + } } ) }) diff --git a/src/module.api/loan.controller.ts b/src/module.api/loan.controller.ts index 9e8869274..c0565fa16 100644 --- a/src/module.api/loan.controller.ts +++ b/src/module.api/loan.controller.ts @@ -1,17 +1,31 @@ -import { BadRequestException, Controller, Get, NotFoundException, Param, Query } from '@nestjs/common' +import { + BadRequestException, + ConflictException, + Controller, + Get, + NotFoundException, + Param, + Query +} from '@nestjs/common' import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' import { ApiPagedResponse } from '@src/module.api/_core/api.paged.response' import { PaginationQuery } from '@src/module.api/_core/api.query' import { CollateralTokenDetail, GetLoanSchemeResult, - LoanSchemeResult + LoanSchemeResult, + LoanTokenResult } from '@defichain/jellyfish-api-core/dist/category/loan' -import { CollateralToken, LoanScheme } from '@whale-api-client/api/loan' +import { CollateralToken, LoanScheme, LoanToken } from '@whale-api-client/api/loan' +import { mapTokenData } from '@src/module.api/token.controller' +import { DeFiDCache } from '@src/module.api/cache/defid.cache' @Controller('/loans') export class LoanController { - constructor (private readonly client: JsonRpcClient) { + constructor ( + private readonly client: JsonRpcClient, + private readonly deFiDCache: DeFiDCache + ) { } /** @@ -63,9 +77,10 @@ export class LoanController { ): Promise> { const result = (await this.client.loan.listCollateralTokens()) .sort((a, b) => a.tokenId.localeCompare(b.tokenId)) - .map(value => mapCollateralToken(value)) + .map(async value => await this.mapCollateralToken(value)) + const items = await Promise.all(result) - return createFakePagination(query, result, item => item.tokenId) + return createFakePagination(query, items, item => item.tokenId) } /** @@ -78,7 +93,7 @@ export class LoanController { async getCollateral (@Param('id') id: string): Promise { try { const data = await this.client.loan.getCollateralToken(id) - return mapCollateralToken(data) + return await this.mapCollateralToken(data) } catch (err) { if (err?.payload?.message === `Token ${id} does not exist!`) { throw new NotFoundException('Unable to find collateral token') @@ -87,6 +102,62 @@ export class LoanController { } } } + + /** + * Paginate loan tokens. + * + * @param {PaginationQuery} query + * @return {Promise>} + */ + @Get('/tokens') + async listLoanToken ( + @Query() query: PaginationQuery + ): Promise> { + const result = Object.entries(await this.client.loan.listLoanTokens()) + .map(([, value]) => { + return mapLoanToken(value) + }).sort((a, b) => a.tokenId.localeCompare(b.tokenId)) + + return createFakePagination(query, result, item => item.tokenId) + } + + /** + * Get information about a loan token with given loan token. + * + * @param {string} id + * @return {Promise} + */ + @Get('/tokens/:id') + async getLoanToken (@Param('id') id: string): Promise { + try { + const data = await this.client.loan.getLoanToken(id) + return mapLoanToken(data) + } catch (err) { + if (err?.payload?.message === `Token ${id} does not exist!`) { + throw new NotFoundException('Unable to find loan token') + } else { + throw new BadRequestException(err) + } + } + } + + async mapCollateralToken (detail: CollateralTokenDetail): Promise { + const result = await this.deFiDCache.getTokenInfoBySymbol(detail.token) + if (result === undefined) { + throw new ConflictException('unable to find token') + } + + const id = Object.keys(result)[0] + const tokenInfo = result[id] + + return { + tokenId: detail.tokenId, + token: mapTokenData(id, tokenInfo), + factor: detail.factor.toFixed(), + priceFeedId: detail.fixedIntervalPriceId, + activateAfterBlock: detail.activateAfterBlock.toNumber() + } + } } function createFakePagination (query: PaginationQuery, items: T[], mapId: (item: T) => string): ApiPagedResponse { @@ -113,12 +184,13 @@ function mapLoanScheme (result: LoanSchemeResult | GetLoanSchemeResult): LoanSch } } -function mapCollateralToken (detail: CollateralTokenDetail): CollateralToken { +function mapLoanToken (result: LoanTokenResult): LoanToken { + const id = Object.keys(result.token)[0] + const tokenInfo = result.token[id] return { - token: detail.token, - tokenId: detail.tokenId, - factor: detail.factor.toFixed(), - priceFeedId: detail.fixedIntervalPriceId, - activateAfterBlock: detail.activateAfterBlock.toNumber() + tokenId: tokenInfo.creationTx, + token: mapTokenData(id, tokenInfo), + interest: result.interest.toFixed(), + fixedIntervalPriceId: result.fixedIntervalPriceId } } diff --git a/src/module.api/loan.token.controller.e2e.ts b/src/module.api/loan.token.controller.e2e.ts new file mode 100644 index 000000000..5fb1c9c3c --- /dev/null +++ b/src/module.api/loan.token.controller.e2e.ts @@ -0,0 +1,190 @@ +import { NestFastifyApplication } from '@nestjs/platform-fastify' +import { createTestingApp, stopTestingApp } from '@src/e2e.module' +import BigNumber from 'bignumber.js' +import { LoanMasterNodeRegTestContainer } from '@defichain/testcontainers' +import { LoanController } from '@src/module.api/loan.controller' +import { NotFoundException } from '@nestjs/common' +import { Testing } from '@defichain/jellyfish-testing' + +const container = new LoanMasterNodeRegTestContainer() +let app: NestFastifyApplication +let controller: LoanController + +beforeAll(async () => { + await container.start() + await container.waitForWalletCoinbaseMaturity() + await container.waitForWalletBalanceGTE(100) + + app = await createTestingApp(container) + const testing = Testing.create(container) + controller = app.get(LoanController) + + const oracleId = await testing.container.call('appointoracle', [await testing.generateAddress(), [ + { token: 'AAPL', currency: 'USD' }, + { token: 'TSLA', currency: 'USD' }, + { token: 'MSFT', currency: 'USD' }, + { token: 'FB', currency: 'USD' } + ], 1]) + await testing.generate(1) + + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { prices: [{ tokenAmount: '1.5@AAPL', currency: 'USD' }] }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { prices: [{ tokenAmount: '2.5@TSLA', currency: 'USD' }] }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { prices: [{ tokenAmount: '3.5@MSFT', currency: 'USD' }] }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { prices: [{ tokenAmount: '4.5@FB', currency: 'USD' }] }) + await testing.generate(1) + + await testing.container.call('setloantoken', [{ + symbol: 'AAPL', + fixedIntervalPriceId: 'AAPL/USD', + mintable: false, + interest: new BigNumber(0.01) + }]) + await testing.generate(1) + + await testing.container.call('setloantoken', [{ + symbol: 'TSLA', + fixedIntervalPriceId: 'TSLA/USD', + mintable: false, + interest: new BigNumber(0.02) + }]) + await testing.generate(1) + + await testing.container.call('setloantoken', [{ + symbol: 'MSFT', + fixedIntervalPriceId: 'MSFT/USD', + mintable: false, + interest: new BigNumber(0.03) + }]) + await testing.generate(1) + + await testing.container.call('setloantoken', [{ + symbol: 'FB', + fixedIntervalPriceId: 'FB/USD', + mintable: false, + interest: new BigNumber(0.04) + }]) + await testing.generate(1) +}) + +afterAll(async () => { + await stopTestingApp(container, app) +}) + +describe('list', () => { + it('should listLoanTokens', async () => { + const result = await controller.listLoanToken({ size: 100 }) + expect(result.data.length).toStrictEqual(4) + expect(result.data[0]).toStrictEqual({ + tokenId: expect.any(String), + interest: expect.any(String), + fixedIntervalPriceId: expect.any(String), + token: { + collateralAddress: expect.any(String), + creation: { + height: expect.any(Number), + tx: expect.any(String) + }, + decimal: 8, + destruction: { + height: -1, + tx: expect.any(String) + }, + displaySymbol: expect.any(String), + finalized: false, + id: expect.any(String), + isDAT: true, + isLPS: false, + limit: '0', + mintable: false, + minted: '0', + name: '', + symbol: expect.any(String), + symbolKey: expect.any(String), + tradeable: true + } + }) + + expect(result.data[1].tokenId.length).toStrictEqual(64) + expect(result.data[2].tokenId.length).toStrictEqual(64) + expect(result.data[3].tokenId.length).toStrictEqual(64) + }) + + it('should listLoanTokens with pagination', async () => { + const first = await controller.listLoanToken({ size: 2 }) + + expect(first.data.length).toStrictEqual(2) + expect(first.page?.next?.length).toStrictEqual(64) + + const next = await controller.listLoanToken({ + size: 2, + next: first.page?.next + }) + + expect(next.data.length).toStrictEqual(2) + expect(next.page?.next?.length).toStrictEqual(64) + + const last = await controller.listLoanToken({ + size: 2, + next: next.page?.next + }) + + expect(last.data.length).toStrictEqual(0) + expect(last.page).toBeUndefined() + }) + + it('should listLoanTokens with an empty object if size 100 next 300 which is out of range', async () => { + const result = await controller.listLoanToken({ size: 100, next: '300' }) + + expect(result.data.length).toStrictEqual(0) + expect(result.page).toBeUndefined() + }) +}) + +describe('get', () => { + it('should get loan token by symbol', async () => { + const data = await controller.getLoanToken('AAPL') + expect(data).toStrictEqual({ + tokenId: expect.any(String), + fixedIntervalPriceId: 'AAPL/USD', + interest: '0.01', + token: { + collateralAddress: expect.any(String), + creation: { + height: expect.any(Number), + tx: expect.any(String) + }, + decimal: 8, + destruction: { + height: -1, + tx: expect.any(String) + }, + displaySymbol: 'dAAPL', + finalized: false, + id: '1', + isDAT: true, + isLPS: false, + limit: '0', + mintable: false, + minted: '0', + name: '', + symbol: 'AAPL', + symbolKey: 'AAPL', + tradeable: true + } + }) + }) + + it('should throw error while getting non-existent loan token id', async () => { + expect.assertions(2) + try { + await controller.getLoanToken('999') + } catch (err) { + expect(err).toBeInstanceOf(NotFoundException) + expect(err.response).toStrictEqual({ + statusCode: 404, + message: 'Unable to find loan token', + error: 'Not Found' + }) + } + }) +}) diff --git a/src/module.api/token.controller.ts b/src/module.api/token.controller.ts index 7d64e56cf..2c57c45b2 100644 --- a/src/module.api/token.controller.ts +++ b/src/module.api/token.controller.ts @@ -58,7 +58,7 @@ export class TokenController { } } -function mapTokenData (id: string, tokenInfo: TokenInfo): TokenData { +export function mapTokenData (id: string, tokenInfo: TokenInfo): TokenData { return { id: id, symbol: tokenInfo.symbol,