diff --git a/apps/whale-api/src/e2e.defid.module.ts b/apps/whale-api/src/e2e.defid.module.ts new file mode 100644 index 0000000000..03bb1e3add --- /dev/null +++ b/apps/whale-api/src/e2e.defid.module.ts @@ -0,0 +1,1068 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-misused-promises */ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ + +import BigNumber from 'bignumber.js' +import { fetch } from 'cross-fetch' +import { v4 as uuidv4 } from 'uuid' +import fs from 'fs' +import { ChildProcess, exec, spawn } from 'child_process' +import { RegTestFoundationKeys, RegTest } from '@defichain/jellyfish-network' +import { ApiPagedResponse } from './module.api/_core/api.paged.response' +import { AddressToken, AddressHistory } from '@defichain/whale-api-client/dist/api/address' +import { Block } from './module.model/block' +import { MasternodeData } from '@defichain/whale-api-client/dist/api/masternodes' +import { + AllSwappableTokensResult, + BestSwapPathResult, DexPricesResult, + PoolPairData, + PoolSwapAggregatedData, + PoolSwapData, + SwapPathsResult +} from '@defichain/whale-api-client/dist/api/poolpairs' +import { GovernanceProposal, ProposalVotesResult } from '@defichain/whale-api-client/dist/api/governance' +import { CollateralToken, LoanScheme, LoanToken, LoanVaultActive, LoanVaultLiquidated } from '@defichain/whale-api-client/dist/api/loan' +import { ListProposalsStatus, ListProposalsType, MasternodeType } from '@defichain/jellyfish-api-core/dist/category/governance' +import { Oracle } from './module.model/oracle' +import { OraclePriceAggregated } from './module.model/oracle.price.aggregated' +import { OraclePriceFeed } from './module.model/oracle.price.feed' +import { OraclePriceActive } from './module.model/oracle.price.active' +import { PriceTicker } from './module.model/price.ticker' +import { PriceFeedInterval, PriceOracle } from '@defichain/whale-api-client/dist/api/prices' +import { RawTransaction } from '@defichain/jellyfish-api-core/dist/category/rawtx' +import { ScriptActivity } from './module.model/script.activity' +import { ScriptAggregation } from './module.model/script.aggregation' +import { ScriptUnspent } from './module.model/script.unspent' +import { BurnData, RewardDistributionData, StatsData, SupplyData } from '@defichain/whale-api-client/dist/api/stats' +import { TokenData } from '@defichain/whale-api-client/dist/api/tokens' +import { Transaction } from './module.model/transaction' +import { TransactionVin } from './module.model/transaction.vin' +import { TransactionVout } from './module.model/transaction.vout' +import { DeFiDRpcError, waitForCondition } from '@defichain/testcontainers' +import { isSHA256Hash, parseHeight } from './module.api/block.controller' +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' +import { ClientApiError } from '@defichain/jellyfish-api-core/dist/index' +import waitForExpect from 'wait-for-expect' +import { TestingPoolPairAdd, TestingPoolPairCreate, TestingPoolPairRemove, TestingTokenBurn, TestingTokenCreate, TestingTokenDFI, TestingTokenMint, TestingTokenSend } from '@defichain/jellyfish-testing' +import { poolpair } from '@defichain/jellyfish-api-core' +import { Bech32, Elliptic, HRP, WIF } from '@defichain/jellyfish-crypto' +import { AddPoolLiquidityMetadata, CreatePoolPairOptions, CreateTokenOptions, CreateSignedTxnHexOptions, MintTokensOptions, PoolSwapMetadata, UtxosToAccountOptions } from '@defichain/testing' +import { VaultAuctionBatchHistory } from './module.model/vault.auction.batch.history' +import { WhaleApiClientOptions } from '@defichain/whale-api-client/dist/whale.api.client' +import { raiseIfError } from '@defichain/whale-api-client/dist/errors' + +const SPAWNING_TIME = 60_000 + +export interface OceanListQuery { + size: number + next?: string +} + +export interface OceanRawTxQuery { + verbose: boolean +} + +export interface OceanProposalQuery { + masternode?: MasternodeType | string + cycle?: number + all?: boolean + query?: OceanListQuery +} + +interface RawTxDto { + hex: string + maxFeeRate?: number +} + +interface RpcDto { + method: string + params: any[] +} + +class DefidOceanApiClient { + constructor (protected readonly options: WhaleApiClientOptions) { + this.options = { + // default + url: '`http://127.0.0.1:3002', // DEFAULT_OCEAN_ARCHIVE_PORT: 3002 + timeout: 60000, + version: 'v0', + network: 'regtest', + ...options + } + } + + async get (path: string): Promise { + // console.log('path: ', path) + const res = await this.fetchTimeout(path, { + method: 'GET', + headers: { + connection: 'open' + } + }) + const json = await res.json() + raiseIfError(json) + return json + } + + async data (path: string): Promise { + const res = await this.get(path) + return res.error !== undefined ? res.error : res.data + } + + async post (path: string, body?: any): Promise { + const res = await this.fetchTimeout(path, { + method: 'POST', + headers: { + 'content-type': 'application/json', + connection: 'open' + }, + body: JSON.stringify(body) + }) + const json = await res.json() + raiseIfError(json) + return json + } + + private async fetchTimeout (path: string, init: RequestInit): Promise { + const { url: endpoint, version, network, timeout } = this.options + const url = `${endpoint}/${version}/${network}/${path}` + + const controller = new AbortController() + const id = setTimeout(() => controller.abort(), timeout) + + const req = fetch(url, { + cache: 'no-cache', + signal: controller.signal, + keepalive: true, + ...init + }) + + try { + const res = await req + clearTimeout(id) + return res + } catch (err: any) { + if (err.type === 'aborted') { + throw new ClientApiError(`request aborted due to set timeout of ${timeout}ms`) + } + + throw err + } + } +} + +export class DAddressController { + constructor (protected readonly client: DefidOceanApiClient) { + } + + async getAccountHistory (address: string, height: number, txno: number): Promise { + return await this.client.data(`address/${address}/history/${height}/${txno}`) + } + + async listAccountHistory (address: string, query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`address/${address}/history?size=${query.size}&next=${query.next}`) + } + + async getBalance (address: string): Promise { + return await this.client.data(`address/${address}/balance`) + } + + async getAggregation (address: string): Promise { + return await this.client.data(`address/${address}/aggregation`) + } + + async listTokens (address: string, query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`address/${address}/tokens?size=${query.size}&next=${query.next}`) + } + + async listVaults (address: string, query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`address/${address}/vaults?size=${query.size}&next=${query.next}`) + } + + async listTransactions (address: string, query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`address/${address}/transactions?size=${query.size}&next=${query.next}`) + } + + async listTransactionsUnspent (address: string, query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`address/${address}/transactions/unspent?size=${query.size}&next=${query.next}`) + } +} + +export class DBlockController { + constructor (protected readonly client: DefidOceanApiClient) { + } + + async list (query: OceanListQuery = { size: 30 }): Promise> { + // TODO(canonbrother): `next` should be height, not hash + // const next = parseHeight(query.next) + return await this.client.get(`blocks?size=${query.size}&next=${query.next}`) + } + + async get (hashOrHeight: string): Promise { + const height = parseHeight(hashOrHeight) + if (height !== undefined) { + return await this.client.data(`blocks/${height}`) + } + if (isSHA256Hash(hashOrHeight)) { + return await this.client.data(`blocks/${hashOrHeight}`) + } + return undefined + } + + async getTransactions (hash: string, query: OceanListQuery = { size: 30 }): Promise> { + if (!isSHA256Hash(hash)) { + return ApiPagedResponse.empty() + } + return await this.client.get(`blocks/${hash}/transactions?size=${query.size}&next=${query.next}`) + } + + async getHighest (): Promise { + return await this.client.data('blocks/highest') + } +} + +export class DFeeController { + constructor (protected readonly client: DefidOceanApiClient) { + } + + async estimate (target: number = 10): Promise { + return await this.client.data(`fee/estimate?confirmationTarget=${target}`) + } +} + +export class DGovernanceController { + constructor (protected readonly client: DefidOceanApiClient) { + } + + async listProposals ( + status: ListProposalsStatus = ListProposalsStatus.ALL, + type: ListProposalsType = ListProposalsType.ALL, + cycle: number = 0, + all: boolean = false, + query: OceanListQuery = { size: 30 } + ): Promise> { + return await this.client.get(`governance/proposals?status=${status}&type=${type}&cycle=${cycle}&all=${all}&size=${query.size}&next=${query.next}`) + } + + async getProposal (id: string): Promise { + return await this.client.data(`governance/proposals/${id}`) + } + + async listProposalVotes ( + id: string, + masternode: MasternodeType = MasternodeType.MINE, + cycle: number = 0, + all: boolean = false, + query: OceanListQuery = { size: 30 } + ): Promise> { + return await this.client.get(`governance/proposals/${id}/votes?masternode=${masternode}&cycle=${cycle}&all=${all}&size=${query.size}&next=${query.next}`) + } +} + +export class DLoanController { + constructor (protected readonly client: DefidOceanApiClient) { + } + + async listScheme (query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`loans/schemes?size=${query.size}&next=${query.next}`) + } + + async getScheme (id: string): Promise { + return await this.client.data(`loans/schemes/${id}`) + } + + async listCollateral (query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`loans/collaterals?size=${query.size}&next=${query.next}`) + } + + async getCollateral (id: string): Promise { + return await this.client.data(`loans/collaterals/${id}`) + } + + async listLoanToken (query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`loans/tokens?size=${query.size}&next=${query.next}`) + } + + async getLoanToken (id: string): Promise { + return await this.client.data(`loans/tokens/${id}`) + } + + async listVault (query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`loans/vaults?size=${query.size}&next=${query.next}`) + } + + async getVault (id: string): Promise { + return await this.client.data(`loans/vaults/${id}`) + } + + async listVaultAuctionHistory (id: string, height: number, batchIndex: string, query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`loans/vaults/${id}/auctions/${height}/batches/${batchIndex}/history?size=${query.size}&next=${query.next}`) + } + + async listAuction (query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`loans/auctions?size=${query.size}&next=${query.next}`) + } +} + +export class DMasternodeController { + constructor (protected readonly client: DefidOceanApiClient) { + } + + async list (query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`masternodes?size=${query.size}&next=${query.next}`) + } + + async get (id: string): Promise { + return await this.client.data(`masternodes/${id}`) + } +} + +export class DOracleController { + constructor (protected readonly client: DefidOceanApiClient) { + } + + async list (query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`oracles?size=${query.size}&next=${query.next}`) + } + + async getPriceFeed (id: string, key: string): Promise> { + return await this.client.get(`oracles/${id}/${key}/feed`) + } + + async getOracleByAddress (address: string): Promise { + return await this.client.data(`oracles/${address}`) + } +} + +export class DPoolPairController { + constructor (protected readonly client: DefidOceanApiClient) { + } + + async list (query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`poolpairs?size=${query.size}&next=${query.next}`) + } + + async get (id: string): Promise { + return await this.client.data(`poolpairs/${id}`) + } + + async listPoolSwaps (id: string, query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`poolpairs/${id}/swaps?size=${query.size}&next=${query.next}`) + } + + async listPoolSwapsVerbose (id: string, query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`poolpairs/${id}/swaps/verbose?size=${query.size}&next=${query.next}`) + } + + async listPoolSwapAggregates (id: string, interval: number, query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`poolpairs/${id}/swaps/aggregate/${interval}?size=${query.size}&next=${query.next}`) + } + + async listSwappableTokens (id: string): Promise { + return await this.client.data(`poolpairs/paths/swappable/${id}`) + } + + async listPaths (fromTokenId: string, toTokenId: string): Promise { + return await this.client.data(`poolpairs/paths/from/${fromTokenId}/to/${toTokenId}`) + } + + async getBestPath (fromTokenId: string, toTokenId: string): Promise { + return await this.client.data(`poolpairs/paths/best/from/${fromTokenId}/to/${toTokenId}`) + } + + async listDexPrices (denomination: string): Promise { + return await this.client.data(`poolpairs/dexprices?denomination=${denomination}`) + } +} + +export class DPriceController { + constructor (protected readonly client: DefidOceanApiClient) { + } + + async list (query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`prices?size=${query.size}&next=${query.next}`) + } + + async get (id: string): Promise { + return await this.client.data(`prices/${id}`) + } + + async getFeed (id: string): Promise> { + return await this.client.get(`prices/${id}/feed`) + } + + async getFeedActive (id: string): Promise> { + return await this.client.get(`prices/${id}/feed/active`) + } + + async getFeedWithInterval (id: string, interval: number): Promise> { + return await this.client.get(`prices/${id}/feed/interval/${interval}`) + } + + async listPriceOracles (id: string): Promise> { + return await this.client.get(`prices/${id}/oracles`) + } +} + +export class DRawTxController { + constructor (protected readonly client: DefidOceanApiClient) { + } + + async send (rawTxDto: RawTxDto): Promise { + return await this.client.post('rawtx/send', rawTxDto) + } + + async test (rawTxDto: RawTxDto): Promise { + return await this.client.post('rawtx/test', rawTxDto) + } + + async get (id: string, verbose = false): Promise { + return await this.client.get(`rawtx/${id}?verbose=${verbose}`) + } +} + +export class DStatsController { + constructor (protected readonly client: DefidOceanApiClient) { + } + + async get (): Promise { + return await this.client.get('stats') + } + + async getSupply (): Promise { + return await this.client.get('stats/supply') + } + + async getBurn (): Promise { + return await this.client.get('stats/burn') + } + + async getRewardDistribution (): Promise { + return await this.client.data('stats/reward/distribution') + } +} + +export class DTokenController { + constructor (protected readonly client: DefidOceanApiClient) { + } + + async list (query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`tokens?size=${query.size}&next=${query.next}`) + } + + async get (id: string): Promise { + return await this.client.data(`tokens/${id}`) + } +} + +export class DTransactionController { + constructor (protected readonly client: DefidOceanApiClient) { + } + + async get (id: string): Promise { + return await this.client.data(`transactions/${id}`) + } + + async getVins (id: string, query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`transactions/${id}/vins?size=${query.size}&next=${query.next}`) + } + + async getVouts (id: string, query: OceanListQuery = { size: 30 }): Promise> { + return await this.client.get(`transactions/${id}/vouts?size=${query.size}&next=${query.next}`) + } +} + +export class DRpcController { + constructor (protected readonly client: DefidOceanApiClient) {} + + async post (rpcDto: RpcDto): Promise { + return await this.client.post('rpc', rpcDto) + } +} + +export class DefidOcean { + readonly addressController = new DAddressController(this.api) + readonly blockController = new DBlockController(this.api) + readonly feeController = new DFeeController(this.api) + readonly governanceController = new DGovernanceController(this.api) + readonly loanController = new DLoanController(this.api) + readonly masternodeController = new DMasternodeController(this.api) + readonly oracleController = new DOracleController(this.api) + readonly poolPairController = new DPoolPairController(this.api) + readonly priceController = new DPriceController(this.api) + readonly rawTxController = new DRawTxController(this.api) + readonly statsController = new DStatsController(this.api) + readonly transactionController = new DTransactionController(this.api) + readonly tokenController = new DTokenController(this.api) + readonly rpcController = new DRpcController(this.api) + + constructor ( + readonly api: DefidOceanApiClient + ) { + } +} + +export class DefidRpcToken { + constructor (private readonly defid: DefidBin, private readonly rpc: DefidRpcClient) { + } + + async create (options: TestingTokenCreate): Promise { + await this.defid.waitForWalletBalanceGTE(101) // token creation fee + + return await this.rpc.token.createToken({ + name: options.symbol, + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: await this.defid.getNewAddress(), + ...options + }) + } + + async dfi (options: TestingTokenDFI): Promise { + const { amount, address } = options + await this.defid.waitForWalletBalanceGTE(new BigNumber(amount).toNumber()) + + const to = address ?? await this.defid.getNewAddress() + const account = `${new BigNumber(amount).toFixed(8)}@0` + return await this.rpc.account.utxosToAccount({ [to]: account }) + } + + async mint (options: TestingTokenMint): Promise { + const { amount, symbol } = options + const account = `${new BigNumber(amount).toFixed(8)}@${symbol}` + return await this.rpc.token.mintTokens({ amounts: [account] }) + } + + async send (options: TestingTokenSend): Promise { + const { address, amount, symbol } = options + const account = `${new BigNumber(amount).toFixed(8)}@${symbol}` + const to = { [address]: [account] } + return await this.rpc.account.sendTokensToAddress({}, to) + } + + async getTokenId (symbol: string): Promise { + const tokenInfo = await this.rpc.token.getToken(symbol) + return Object.keys(tokenInfo)[0] + } + + async burn (options: TestingTokenBurn): Promise { + const { amount, symbol, from, context } = options + const account = `${new BigNumber(amount).toFixed(8)}@${symbol}` + return await this.rpc.token.burnTokens(account, from, context) + } +} + +export class DefidRpcPoolPair { + constructor ( + private readonly defid: DefidBin, + private readonly rpc: DefidRpcClient + ) { + } + + async get (symbol: string): Promise { + const values = await this.rpc.poolpair.getPoolPair(symbol, true) + return Object.values(values)[0] + } + + async create (options: TestingPoolPairCreate): Promise { + return await this.rpc.poolpair.createPoolPair({ + commission: 0, + status: true, + ownerAddress: await this.defid.getNewAddress(), + ...options + }) + } + + async add (options: TestingPoolPairAdd): Promise { + const accountA = `${new BigNumber(options.a.amount).toFixed(8)}@${options.a.symbol}` + const accountB = `${new BigNumber(options.b.amount).toFixed(8)}@${options.b.symbol}` + const from = { '*': [accountA, accountB] } + const address = options.address ?? await this.defid.getNewAddress() + return await this.rpc.poolpair.addPoolLiquidity(from, address) + } + + async remove (options: TestingPoolPairRemove): Promise { + const { address, symbol, amount } = options + const account = `${new BigNumber(amount).toFixed(8)}@${symbol}` + return await this.rpc.poolpair.removePoolLiquidity(address, account) + } + + async swap (options: poolpair.PoolSwapMetadata): Promise { + return await this.rpc.poolpair.poolSwap(options) + } +} + +export class DefidRpcClient extends JsonRpcClient { +} + +export class DefidRpc { + readonly token = new DefidRpcToken(this.defid, this.client) + readonly poolpair = new DefidRpcPoolPair(this.defid, this.client) + + private readonly addresses: Record = {} + + constructor ( + private readonly defid: DefidBin, + readonly client: DefidRpcClient + ) { + } + + async generate (n: number): Promise { + await this.defid.generate(n) + } + + async address (key: number | string): Promise { + key = key.toString() + if (this.addresses[key] === undefined) { + this.addresses[key] = await this.generateAddress() + } + return this.addresses[key] + } + + generateAddress (): Promise + generateAddress (n: 1): Promise + generateAddress (n: number): Promise + + async generateAddress (n?: number): Promise { + if (n === undefined || n === 1) { + return await this.defid.getNewAddress() + } + + const addresses: string[] = [] + for (let i = 0; i < n; i++) { + addresses[i] = await this.defid.getNewAddress() + } + return addresses + } +} + +export class DefidBin { + tmpDir = `/tmp/${uuidv4()}` + binary: ChildProcess | null = null + + port = this.randomPort() + + rpcPort = this.randomPort() + wsPort = this.randomPort() + ethRpcPort = this.randomPort() + rpcUrl = `http://test:test@127.0.0.1:${this.rpcPort}` + rpcClient = new DefidRpcClient(this.rpcUrl) + rpc = new DefidRpc(this, this.rpcClient) + + oceanPort = this.randomPort(3000, 3999) + oceanClient = new DefidOceanApiClient({ url: `http://127.0.0.1:${this.oceanPort}` }) + ocean = new DefidOcean(this.oceanClient) + + private randomPort (min: number = 10000, max: number = 19999): number { + return Math.floor(Math.random() * max) + min + } + + async start (opts: string[] = []): Promise { + fs.mkdirSync(this.tmpDir) + + if (process.env.DEFID === undefined) { + throw new Error('`process.env.DEFID` is required') + } + + const args = [ + // prepend + `-datadir=${this.tmpDir}`, + '-printtoconsole', + '-rpcuser=test', + '-rpcpassword=test', + `-rpcport=${this.rpcPort}`, + `-port=${this.port}`, + `-wsport=${this.wsPort}`, + `-ethrpcport=${this.ethRpcPort}`, + '-rpcallowip=0.0.0.0/0', + // regtest + '-regtest', + '-jellyfish_regtest=1', + '-regtest-skip-loan-collateral-validation', + '-regtest-minttoken-simulate-mainnet=0', + '-rpc-governance-accept-neutral=1', + '-txnotokens=0', + '-logtimemicros', + '-txindex=1', + '-acindex=1', + // mn + '-dummypos=0', + '-spv=1', + '-anchorquorum=2', + `-masternode_operator=${RegTestFoundationKeys[1].operator.address}`, + // ocean + '-oceanarchive', + '-oceanarchiveserver', + `-oceanarchiveport=${this.oceanPort}`, + '-oceanarchivebind=0.0.0.0', + // hf + '-amkheight=0', + '-bayfrontheight=1', + '-bayfrontgardensheight=2', + '-clarkequayheight=3', + '-dakotaheight=4', + '-dakotacrescentheight=5', + '-eunosheight=6', + '-eunospayaheight=7', + '-fortcanningheight=8', + '-fortcanningmuseumheight=9', + '-fortcanninghillheight=10', + '-fortcanningroadheight=11', + '-fortcanningcrunchheight=12', + '-fortcanningspringheight=13', + '-fortcanninggreatworldheight=14', + '-fortcanningepilogueheight=15', + '-grandcentralheight=16', + '-grandcentralepilogueheight=17', + '-metachainheight=18', + ...opts + ] + + const binary = spawn(process.env.DEFID, args) + + binary.on('error', err => { + if ((err as any).errno === 'ENOENT') { + console.error('\x1b[31mMissing Defid binary.\nPlease compile the Defid\x1b[0m') + } else { + console.error(err) + } + process.exit(1) + }) + + const logs: string[] = [] + + await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + console.error('\x1b[31m Failed to start Defid node.\x1b[0m') + console.error(logs.map(chunk => chunk.toString()).join('\n')) + process.exit(1) + }, SPAWNING_TIME - 2_000) + + const onData = async (chunk: any) => { + logs.push(chunk) + + /* eslint-disable-next-line @typescript-eslint/strict-boolean-expressions */ + if (chunk.toString().match(/addcon thread start/)) { + // wait for ocean + await new Promise((resolve) => setTimeout(resolve, 1_000)) + + try { + await this.ocean.blockController.get('0') + // console.log('[DefidBin.start()] blockController.get genesis.hash: ', res?.hash) + } catch (err) { + console.log('[DefidBin.start()] blockController.get genesis err: ', err) + } + + clearTimeout(timer) + + binary.stderr.off('data', onData) + binary.stdout.off('data', onData) + + await this.call('importprivkey', [RegTestFoundationKeys[1].owner.privKey]) + await this.call('importprivkey', [RegTestFoundationKeys[1].operator.privKey]) + + // setgov + // generate(2) + + resolve() + } + } + + binary.stderr.on('data', onData) + binary.stdout.on('data', onData) + }) + + this.binary = binary + } + + async stop (): Promise { + const interval = setInterval(() => { + if (this.binary?.pid !== undefined) { + clearInterval(interval) + exec(`kill -9 ${this.binary?.pid}`) + fs.rmdirSync(this.tmpDir, { recursive: true }) + } + }, 500) + this.binary?.kill() + } + + async call (method: string, params: any = []): Promise { + const body = JSON.stringify({ + jsonrpc: '1.0', + id: Math.floor(Math.random() * 100000000000000), + method: method, + params: params + }) + + const text = await this.post(body) + const { + result, + error + } = JSON.parse(text) + + if (error !== undefined && error !== null) { + throw new DeFiDRpcError(error) + } + + return result + } + + async post (body: string): Promise { + const response = await fetch(this.rpcUrl, { + method: 'POST', + body: body + }) + return await response.text() + } + + async generate ( + nblocks: number, + address?: string | undefined, + maxTries: number = 1000000 + ): Promise { + if (address === undefined) { + address = await this.call('getnewaddress') + } + for (let minted = 0, tries = 0; minted < nblocks && tries < maxTries; tries++) { + const result = await this.call('generatetoaddress', [1, address, 1]) + if (result === 1) { + minted += 1 + } + } + } + + async waitForBlockHeight (height: number, timeout = 590000): Promise { + return await waitForCondition(async () => { + const count = await this.getBlockCount() + if (count > height) { + return true + } + await this.generate(1) + return false + }, timeout, 100, 'waitForBlockHeight') + } + + async waitForIndexedHeight (height: number, timeout: number = 30000): Promise { + await waitForExpect(async () => { + const block = await this.ocean.blockController.getHighest() + expect(block?.height).toBeGreaterThan(height) + }, timeout) + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + + async waitForWalletCoinbaseMaturity (timeout: number = 180000, mockTime: boolean = true): Promise { + if (!mockTime) { + return await this.waitForBlockHeight(100, timeout) + } + + let fakeTime: number = 1579045065 + await this.call('setmocktime', [fakeTime]) + + const intervalId = setInterval(() => { + fakeTime += 3 + void this.call('setmocktime', [fakeTime]) + }, 200) + + await this.waitForBlockHeight(100, timeout) + + clearInterval(intervalId) + await this.call('setmocktime', [0]) + } + + async waitForAddressTxCount (controller: DAddressController, address: string, txCount: number, timeout: number = 15000): Promise { + return await waitForCondition(async () => { + const agg = await controller.getAggregation(address) + if (agg?.statistic.txCount === txCount) { + return true + } + await this.generate(1) + return false + }, timeout, 100, 'waitForAddressTxCcount') + } + + async waitForWalletBalanceGTE (balance: number, timeout: number = 300000): Promise { + return await waitForCondition(async () => { + const getbalance = await this.call('getbalance') + if (getbalance >= balance) { + return true + } + await this.generate(1) + return false + }, timeout, 100, 'waitForWalletBalanceGTE') + } + + async waitForPath (controller: DPoolPairController, timeout: number = 300000): Promise { + return await waitForCondition(async () => { + const res = await controller.listSwappableTokens('0') + if (res.swappableTokens.length > 0) { + return true + } + return false + }, timeout, 100, 'waitForPath') + } + + async waitForActivePrice (fixedIntervalPriceId: string, activePrice: string, timeout = 30000): Promise { + return await waitForCondition(async () => { + const data: any = await this.call('getfixedintervalprice', [fixedIntervalPriceId]) + // eslint-disable-next-line + if (data.activePrice.toString() !== activePrice) { + await this.generate(1) + return false + } + return true + }, timeout, 100, 'waitForActivePrice') + } + + async fundAddress (address: string, amount: number): Promise<{ txid: string, vout: number }> { + const txid = await this.call('sendtoaddress', [address, amount]) + await this.generate(1) + + const { vout }: { + vout: Array<{ + n: number + scriptPubKey: { + addresses: string[] + } + }> + } = await this.call('getrawtransaction', [txid, true]) + for (const out of vout) { + if (out.scriptPubKey.addresses.includes(address)) { + return { + txid, + vout: out.n + } + } + } + throw new Error('getrawtransaction will always return the required vout') + } + + async getNewAddress (label: string = '', addressType: 'legacy' | 'p2sh-segwit' | 'bech32' | 'eth' | string = 'bech32'): Promise { + return await this.call('getnewaddress', [label, addressType]) + } + + async getBlockCount (): Promise { + return await this.call('getblockcount', []) + } + + async utxosToAccount ( + amount: number, + options?: UtxosToAccountOptions + ): Promise { + await this.waitForWalletBalanceGTE(amount + 0.1) + + const address = options?.address ?? await this.getNewAddress() + const payload: { [key: string]: string } = {} + payload[address] = `${amount.toString()}@0` + await this.call('utxostoaccount', [payload]) + await this.generate(1) + } + + async sendTokensToAddress ( + address: string, + amount: number, + symbol: string + ): Promise { + const txid = await this.call('sendtokenstoaddress', [{}, { [address]: [`${amount}@${symbol}`] }]) + await this.generate(1) + return txid + } + + async createToken (symbol: string, options?: CreateTokenOptions): Promise { + const metadata = { + symbol, + name: options?.name ?? symbol, + isDAT: options?.isDAT ?? true, + mintable: options?.mintable ?? true, + tradeable: options?.tradeable ?? true, + collateralAddress: options?.collateralAddress ?? await this.getNewAddress() + } + + await this.waitForWalletBalanceGTE(101) + await this.call('createtoken', [metadata]) + await this.generate(1) + + const res = await this.call('gettoken', [symbol]) + return Number.parseInt(Object.keys(res)[0]) + } + + async mintTokens ( + symbol: string, + options?: MintTokensOptions + ): Promise { + const address = options?.address ?? await this.getNewAddress() + const utxoAmount = options?.utxoAmount ?? 2000 + const mintAmount = options?.mintAmount ?? 2000 + + await this.utxosToAccount(utxoAmount, { address }) + + const hashed = await this.call('minttokens', [`${mintAmount}@${symbol}`]) + await this.generate(1) + + return hashed + } + + async createPoolPair (aToken: string, bToken: string, options?: CreatePoolPairOptions): Promise { + const metadata = { + tokenA: aToken, + tokenB: bToken, + commission: options?.commission ?? 0, + status: options?.status ?? true, + ownerAddress: options?.ownerAddress ?? await this.getNewAddress() + } + const txid = await this.call('createpoolpair', [metadata, options?.utxos]) + await this.generate(1) + return txid + } + + async addPoolLiquidity ( + metadata: AddPoolLiquidityMetadata + ): Promise { + const { amountA, amountB, tokenA, tokenB, shareAddress } = metadata + const from = { '*': [`${amountA}@${tokenA}`, `${amountB}@${tokenB}`] } + await this.call('addpoolliquidity', [from, shareAddress]) + await this.generate(1) + + const tokens: string[] = await this.call('getaccount', [shareAddress]) + const lpToken = tokens.find(value => value.endsWith(`@${tokenA}-${tokenB}`)) + if (lpToken === undefined) { + throw new Error('LP token not found in account') + } + + const amount = lpToken.replace(`@${tokenA}-${tokenB}`, '') + return new BigNumber(amount) + } + + async poolSwap ( + metadata: PoolSwapMetadata + ): Promise { + const txid = await this.call('poolswap', [metadata]) + await this.generate(1) + return txid + } + + async createSignedTxnHex ( + aAmount: number, + bAmount: number, + options: CreateSignedTxnHexOptions = { + aEllipticPair: Elliptic.fromPrivKey(Buffer.alloc(32, Math.random().toString(), 'ascii')), + bEllipticPair: Elliptic.fromPrivKey(Buffer.alloc(32, Math.random().toString(), 'ascii')) + } + ): Promise { + const aBech32 = Bech32.fromPubKey(await options.aEllipticPair.publicKey(), RegTest.bech32.hrp as HRP) + const bBech32 = Bech32.fromPubKey(await options.bEllipticPair.publicKey(), RegTest.bech32.hrp as HRP) + + const { txid, vout } = await this.fundAddress(aBech32, aAmount) + const inputs = [{ txid: txid, vout: vout }] + + const unsigned = await this.call('createrawtransaction', [inputs, { + [bBech32]: new BigNumber(bAmount) + }]) + + const signed = await this.call('signrawtransactionwithkey', [unsigned, [ + WIF.encode(RegTest.wifPrefix, await options.aEllipticPair.privateKey()) + ]]) + + return signed.hex + } +} diff --git a/apps/whale-api/src/module.api/__defid__/address.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/address.controller.defid.ts new file mode 100644 index 0000000000..5996fcd82f --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/address.controller.defid.ts @@ -0,0 +1,1113 @@ +import { WIF } from '@defichain/jellyfish-crypto' +import { ForbiddenException } from '@nestjs/common' +import BigNumber from 'bignumber.js' +import { RegTestFoundationKeys } from '@defichain/jellyfish-network' +import { DAddressController, DefidBin, DefidRpc } from '../../e2e.defid.module' +import { WhaleApiException } from '@defichain/whale-api-client/dist/errors' + +let testing: DefidRpc +let app: DefidBin +let controller: DAddressController + +let colAddr: string +let usdcAddr: string +let poolAddr: string +let emptyAddr: string +let dfiUsdc + +async function setup (app: DefidBin, testing: DefidRpc): Promise { + 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.client.account.accountToAccount(colAddr, { [usdcAddr]: '10000@USDC' }) + await testing.generate(1) + + await testing.client.poolpair.createPoolPair({ + tokenA: 'DFI', + tokenB: 'USDC', + commission: 0, + status: true, + ownerAddress: poolAddr + }) + await testing.generate(1) + + const poolPairsKeys = Object.keys(await testing.client.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 app.call('setgov', [{ LP_SPLITS: { [dfiUsdc]: 1.0 } }]) + await testing.generate(1) + + await testing.client.poolpair.addPoolLiquidity({ + [colAddr]: '5000@DFI', + [usdcAddr]: '5000@USDC' + }, poolAddr) + await testing.generate(1) + + await testing.client.poolpair.poolSwap({ + from: colAddr, + tokenFrom: 'DFI', + amountFrom: 555, + to: usdcAddr, + tokenTo: 'USDC' + }) + await testing.generate(1) + + await testing.client.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) + + await testing.generate(1) + + // to test rewards listing (only needed if `no_rewards` flag disabled) + // const height = await testing.container.getBlockCount() + // await testing.container.waitForBlockHeight(Math.max(500, height)) +} + +// not being used +describe.skip('listAccountHistory', () => { + beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.addressController + testing = app.rpc + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) + + await setup(app, testing) + }) + + afterAll(async () => { + await app.stop() + }) + + it('should not listAccountHistory with mine filter', async () => { + const promise = controller.listAccountHistory('mine', { size: 30 }) + await expect(promise).rejects.toThrow(ForbiddenException) + await expect(promise).rejects.toThrow('mine is not allowed') + }) + + it('should list empty account history', async () => { + const history = await controller.listAccountHistory(emptyAddr, { size: 30 }) + expect(history.data.length).toStrictEqual(0) + }) + + it('should list account history without rewards', async () => { + const history = await controller.listAccountHistory(poolAddr, { size: 30 }) + expect(history.data.length).toStrictEqual(4) + expect(history.data.every(history => !(['Rewards', 'Commission'].includes(history.type)))) + }) + + // skip test, API currently no included rewards (missing txid/txn will crash query with `next`) + // rewards listing requires extra implementation for pagination + it.skip('should list account history include rewards', async () => { + // benchmarking `listaccounthistory` with `no_rewards` false + // generate couple hundred blocks to check RPC resource impact + + let page = 0 + let next: string | undefined + + while (page >= 0) { + console.log('benchmarking, page: ', page) + console.time('listrewards') + const history = await controller.listAccountHistory(poolAddr, { + size: 30, + next + }) + console.timeEnd('listrewards') + + if (history.page?.next === undefined) { + page = -1 + } else { + page += 1 + next = history.page.next + } + } + }) + + it('should listAccountHistory', async () => { + const history = await controller.listAccountHistory(colAddr, { size: 30 }) + expect(history.data.length).toStrictEqual(30) + for (let i = 0; i < history.data.length; i += 1) { + const accountHistory = history.data[i] + expect(typeof accountHistory.owner).toStrictEqual('string') + expect(typeof accountHistory.block.height).toStrictEqual('number') + expect(typeof accountHistory.block.hash).toStrictEqual('string') + expect(typeof accountHistory.block.time).toStrictEqual('number') + expect(typeof accountHistory.type).toStrictEqual('string') + expect(typeof accountHistory.txn).toStrictEqual('number') + expect(typeof accountHistory.txid).toStrictEqual('string') + expect(accountHistory.amounts.length).toBeGreaterThan(0) + expect(typeof accountHistory.amounts[0]).toStrictEqual('string') + } + }) + + it('should listAccountHistory with size', async () => { + const history = await controller.listAccountHistory(colAddr, { size: 10 }) + expect(history.data.length).toStrictEqual(10) + }) + + it('test listAccountHistory pagination', async () => { + const full = await controller.listAccountHistory(colAddr, { size: 12 }) + + const first = await controller.listAccountHistory(colAddr, { size: 3 }) + expect(first.data[0]).toStrictEqual(full.data[0]) + expect(first.data[1]).toStrictEqual(full.data[1]) + expect(first.data[2]).toStrictEqual(full.data[2]) + + const firstLast = first.data[first.data.length - 1] + const secondToken = `${firstLast.txid}-${firstLast.type}-${firstLast.block.height}` + const second = await controller.listAccountHistory(colAddr, { + size: 3, + next: secondToken + }) + expect(second.data[0]).toStrictEqual(full.data[3]) + expect(second.data[1]).toStrictEqual(full.data[4]) + expect(second.data[2]).toStrictEqual(full.data[5]) + + const secondLast = second.data[second.data.length - 1] + const thirdToken = `${secondLast.txid}-${secondLast.type}-${secondLast.block.height}` + const third = await controller.listAccountHistory(colAddr, { + size: 3, + next: thirdToken + }) + expect(third.data[0]).toStrictEqual(full.data[6]) + expect(third.data[1]).toStrictEqual(full.data[7]) + expect(third.data[2]).toStrictEqual(full.data[8]) + + const thirdLast = third.data[third.data.length - 1] + const forthToken = `${thirdLast.txid}-${thirdLast.type}-${thirdLast.block.height}` + const forth = await controller.listAccountHistory(colAddr, { + size: 3, + next: forthToken + }) + expect(forth.data[0]).toStrictEqual(full.data[9]) + expect(forth.data[1]).toStrictEqual(full.data[10]) + expect(forth.data[2]).toStrictEqual(full.data[11]) + }) +}) + +describe('getAccountHistory', () => { + beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.addressController + testing = app.rpc + + await app.waitForWalletCoinbaseMaturity() + + await setup(app, testing) + }) + + afterAll(async () => { + await app.stop() + }) + + it('should getAccountHistory', async () => { + const history = await app.rpcClient.account.listAccountHistory(colAddr) + for (const h of history) { + if (['sent', 'receive'].includes(h.type)) { + continue + } + const acc = await controller.getAccountHistory(colAddr, h.blockHeight, h.txn) + expect(acc?.owner).toStrictEqual(h.owner) + expect(acc?.txid).toStrictEqual(h.txid) + expect(acc?.txn).toStrictEqual(h.txn) + } + + const poolHistory = await app.rpcClient.account.listAccountHistory(poolAddr) + for (const h of poolHistory) { + if (['sent', 'receive', 'Rewards'].includes(h.type)) { + continue + } + const acc = await controller.getAccountHistory(poolAddr, h.blockHeight, 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 addr = await app.getNewAddress() + try { + await controller.getAccountHistory(addr, Number(`${'0'.repeat(64)}`), 1) + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + at: expect.any(Number), + code: 500, + message: 'Record not found', + type: 'Unknown', + url: `/v0/regtest/address/${addr}/history/0/1` + }) + } + }) + + it('should be failed as invalid height', async () => { + { // NaN + const addr = await app.getNewAddress() + try { + await controller.getAccountHistory(addr, Number('NotANumber'), 1) + } catch (err: any) { + expect(err.error).toStrictEqual({ + at: expect.any(Number), + code: 400, + message: 'key: height, Cannot parse `height` with value `"NaN"` to a `u32`', // JSON value is not an integer as expected + type: 'BadRequest', + url: `/${addr}/history/NaN/1` + }) + } + } + + { // negative height + const addr = await app.getNewAddress() + try { + await controller.getAccountHistory(addr, -1, 1) + } catch (err: any) { + expect(err.error).toStrictEqual({ + at: expect.any(Number), + code: 400, + message: 'key: height, Cannot parse `height` with value `"-1"` to a `u32`', // Record not found + type: 'BadRequest', + url: `/${addr}/history/-1/1` + }) + } + } + }) + + it('should be failed as getting unsupport tx type - sent, received, blockReward', async () => { + const history = await app.rpcClient.account.listAccountHistory(colAddr) + for (const h of history) { + if (['sent', 'receive'].includes(h.type)) { + try { + await controller.getAccountHistory(colAddr, h.blockHeight, h.txn) + } catch (err: any) { + expect(err.error).toStrictEqual({ + at: expect.any(Number), + code: 500, + message: 'Record not found', + type: 'Unknown', + url: expect.any(String) + }) + } + } + } + + // TOOD(): retrieve empty + const operatorAccHistory = await app.call('listaccounthistory', [RegTestFoundationKeys[1].operator.address]) + for (const h of operatorAccHistory) { + if (['blockReward'].includes(h.type)) { + try { + const res = await controller.getAccountHistory(RegTestFoundationKeys[1].operator.address, h.blockHeight, h.txn) + console.log('res 4: ', res) + } catch (err) { + console.log('err4: ', err) + } + // await expect(promise).rejects.toThrow('Record not found') + } + } + }) +}) + +describe('getBalance', () => { + beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.addressController + + await app.waitForBlockHeight(100) + }) + + afterAll(async () => { + await app.stop() + }) + + it('getBalance should be zero', async () => { + const address = await app.getNewAddress() + const balance = await controller.getBalance(address) + expect(balance).toStrictEqual('0.00000000') + }) + + it('should getBalance non zero with bech32 address', async () => { + const address = 'bcrt1qf5v8n3kfe6v5mharuvj0qnr7g74xnu9leut39r' + + await app.fundAddress(address, 1.23) + await app.waitForAddressTxCount(controller, address, 1) + + const balance = await controller.getBalance(address) + expect(balance).toStrictEqual('1.23000000') + }) + + it('should getBalance non zero with legacy address', async () => { + const address = await app.getNewAddress('', 'legacy') + + await app.fundAddress(address, 0.00100000) + await app.waitForAddressTxCount(controller, address, 1) + + const balance = await controller.getBalance(address) + expect(balance).toStrictEqual('0.00100000') + }) + + it('should getBalance non zero with p2sh-segwit address', async () => { + const address = await app.getNewAddress('', 'p2sh-segwit') + + await app.fundAddress(address, 10.99999999) + await app.waitForAddressTxCount(controller, address, 1) + + const balance = await controller.getBalance(address) + expect(balance).toStrictEqual('10.99999999') + }) + + it('should throw error if getBalance with invalid address', async () => { + await expect(controller.getBalance('invalid')).rejects.toThrow('InvalidDefiAddress') + }) + + it('should sum getBalance', async () => { + const address = 'bcrt1qeq2g82kj99mqfvnwc2g5w0azzd298q0t84tc6s' + + await app.fundAddress(address, 0.12340001) + await app.fundAddress(address, 4.32412313) + await app.fundAddress(address, 12.93719381) + await app.waitForAddressTxCount(controller, address, 3) + + const balance = await controller.getBalance(address) + expect(balance).toStrictEqual('17.38471695') + }) +}) + +describe('getAggregation', () => { + beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.addressController + testing = app.rpc + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) + + await app.waitForBlockHeight(100) + }) + + afterAll(async () => { + await app.stop() + }) + + it('should aggregate 3 txn', async () => { + const address = 'bcrt1qxvvp3tz5u8t90nwwjzsalha66zk9em95tgn3fk' + + await app.fundAddress(address, 0.12340001) + await app.fundAddress(address, 4.32412313) + await app.fundAddress(address, 12.93719381) + await app.waitForAddressTxCount(controller, address, 3) + + const agg = await controller.getAggregation(address) + expect(agg).toStrictEqual({ + amount: { + txIn: '17.38471695', + txOut: '0.00000000', + unspent: '17.38471695' + }, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + hid: expect.stringMatching(/[0-f]{64}/), + id: expect.stringMatching(/[0-f]{72}/), + script: { + hex: '0014331818ac54e1d657cdce90a1dfdfbad0ac5cecb4', + type: 'witness_v0_keyhash' + }, + statistic: { + txCount: 3, + txInCount: 3, + txOutCount: 0 + } + }) + }) + + it('should throw error if getAggregation with invalid address', async () => { + await expect(controller.getAggregation('invalid')).rejects.toThrow('InvalidDefiAddress') + }) +}) + +describe('listTransactions', () => { + beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.addressController + testing = app.rpc + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) + + await app.waitForBlockHeight(100) + + await app.fundAddress(addressA.bech32, 34) + await app.fundAddress(addressA.bech32, 0.12340001) + await app.fundAddress(addressA.bech32, 1.32412313) + await app.fundAddress(addressA.bech32, 2.93719381) + + await app.call('sendrawtransaction', [ + // This create vin & vout with 9.5 + await app.createSignedTxnHex(9.5, 9.4999, options) + ]) + await app.call('sendrawtransaction', [ + // This create vin & vout with 1.123 + await app.createSignedTxnHex(1.123, 1.1228, options) + ]) + await app.generate(1) + await app.waitForAddressTxCount(controller, addressB.bech32, 2) + }) + + afterAll(async () => { + await app.stop() + }) + + const addressA = { + bech32: 'bcrt1qykj5fsrne09yazx4n72ue4fwtpx8u65zac9zhn', + privKey: 'cQSsfYvYkK5tx3u1ByK2ywTTc9xJrREc1dd67ZrJqJUEMwgktPWN' + } + const addressB = { + bech32: 'bcrt1qf26rj8895uewxcfeuukhng5wqxmmpqp555z5a7', + privKey: 'cQbfHFbdJNhg3UGaBczir2m5D4hiFRVRKgoU8GJoxmu2gEhzqHtV' + } + const options = { + aEllipticPair: WIF.asEllipticPair(addressA.privKey), + bEllipticPair: WIF.asEllipticPair(addressB.privKey) + } + + it('(addressA) should listTransactions', async () => { + const response = await controller.listTransactions(addressA.bech32, { + size: 30 + }) + + expect(response.data.length).toStrictEqual(8) + expect(response.page).toBeUndefined() + + expect(response.data[5]).toStrictEqual({ + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + hid: expect.stringMatching(/[0-f]{64}/), + id: expect.stringMatching(/[0-f]{72}/), + script: { + hex: '001425a544c073cbca4e88d59f95ccd52e584c7e6a82', + type: 'witness_v0_keyhash' + }, + tokenId: 0, + txid: expect.stringMatching(/[0-f]{64}/), + type: 'vout', + typeHex: '01', + value: '1.32412313', + vout: { + n: expect.any(Number), + txid: expect.stringMatching(/[0-f]{64}/) + } + }) + }) + + it('(addressA) should listTransactions with pagination', async () => { + const first = await controller.listTransactions(addressA.bech32, { + size: 2 + }) + expect(first.data.length).toStrictEqual(2) + expect(first.page?.next).toMatch(/[0-f]{82}/) + expect(first.data[0].value).toStrictEqual('1.12300000') + expect(first.data[0].type).toStrictEqual('vin') + expect(first.data[1].value).toStrictEqual('1.12300000') + expect(first.data[1].type).toStrictEqual('vout') + + const next = await controller.listTransactions(addressA.bech32, { + size: 10, + next: first.page?.next + }) + + expect(next.data.length).toStrictEqual(6) + expect(next.page?.next).toBeUndefined() + expect(next.data[0].value).toStrictEqual('9.50000000') + expect(next.data[0].type).toStrictEqual('vin') + expect(next.data[1].value).toStrictEqual('9.50000000') + expect(next.data[1].type).toStrictEqual('vout') + expect(next.data[2].value).toStrictEqual('2.93719381') + expect(next.data[2].type).toStrictEqual('vout') + expect(next.data[3].value).toStrictEqual('1.32412313') + expect(next.data[3].type).toStrictEqual('vout') + expect(next.data[4].value).toStrictEqual('0.12340001') + expect(next.data[4].type).toStrictEqual('vout') + expect(next.data[5].value).toStrictEqual('34.00000000') + expect(next.data[5].type).toStrictEqual('vout') + }) + + it('should throw error if listTransactions with invalid address', async () => { + await expect(controller.listTransactions('invalid', { size: 30 })) + .rejects.toThrow('InvalidDefiAddress') + }) + + it('(addressB) should listTransactions', async () => { + const response = await controller.listTransactions(addressB.bech32, { + size: 30 + }) + + expect(response.data.length).toStrictEqual(2) + expect(response.page).toBeUndefined() + + expect(response.data[1]).toStrictEqual({ + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + hid: expect.stringMatching(/[0-f]{64}/), + id: expect.stringMatching(/[0-f]{72}/), + script: { + hex: '00144ab4391ce5a732e36139e72d79a28e01b7b08034', + type: 'witness_v0_keyhash' + }, + tokenId: 0, + txid: expect.stringMatching(/[0-f]{64}/), + type: 'vout', + typeHex: '01', + value: '9.49990000', + vout: { + n: 0, + txid: expect.stringMatching(/[0-f]{64}/) + } + }) + }) + + it('(addressA) should listTransactions with undefined next pagination', async () => { + const first = await controller.listTransactions(addressA.bech32, { + size: 2, + next: undefined + }) + + expect(first.data.length).toStrictEqual(2) + expect(first.page?.next).toMatch(/[0-f]{82}/) + }) +}) + +describe('listTransactionsUnspent', () => { + beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.addressController + testing = app.rpc + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) + + await app.waitForBlockHeight(100) + + await app.fundAddress(addressA.bech32, 34) + await app.fundAddress(addressA.bech32, 0.12340001) + await app.fundAddress(addressA.bech32, 1.32412313) + await app.fundAddress(addressA.bech32, 2.93719381) + + await app.call('sendrawtransaction', [ + // This create vin & vout with 9.5 + await app.createSignedTxnHex(9.5, 9.4999, options) + ]) + await app.call('sendrawtransaction', [ + // This create vin & vout with 1.123 + await app.createSignedTxnHex(1.123, 1.1228, options) + ]) + await app.generate(1) + await app.waitForAddressTxCount(controller, addressB.bech32, 2) + }) + + afterAll(async () => { + await app.stop() + }) + + const addressA = { + bech32: 'bcrt1qykj5fsrne09yazx4n72ue4fwtpx8u65zac9zhn', + privKey: 'cQSsfYvYkK5tx3u1ByK2ywTTc9xJrREc1dd67ZrJqJUEMwgktPWN' + } + const addressB = { + bech32: 'bcrt1qf26rj8895uewxcfeuukhng5wqxmmpqp555z5a7', + privKey: 'cQbfHFbdJNhg3UGaBczir2m5D4hiFRVRKgoU8GJoxmu2gEhzqHtV' + } + const addressC = { + bech32: 'bcrt1qyf5c9593u8v5s7exh3mfndw28k6sz84788tlze', + privKey: 'cPEKnsDLWGQXyFEaYxkcgwLddd7tGdJ2vZdEiFTzxMrY5dAMPKH1' + } + const options = { + aEllipticPair: WIF.asEllipticPair(addressC.privKey), + bEllipticPair: WIF.asEllipticPair(addressB.privKey) + } + + it('(addressA) should listTransactionsUnspent', async () => { + const response = await controller.listTransactionsUnspent(addressA.bech32, { + size: 30 + }) + + expect(response.data.length).toStrictEqual(4) + expect(response.page).toBeUndefined() + + expect(response.data[3]).toStrictEqual({ + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + hid: expect.stringMatching(/[0-f]{64}/), + id: expect.stringMatching(/[0-f]{72}/), + script: { + hex: '001425a544c073cbca4e88d59f95ccd52e584c7e6a82', + type: 'witness_v0_keyhash' + }, + sort: expect.stringMatching(/[0-f]{80}/), + vout: { + n: expect.any(Number), + tokenId: 0, + txid: expect.stringMatching(/[0-f]{64}/), + value: '2.93719381' + } + }) + }) + + it('(addressA) should listTransactionsUnspent with pagination', async () => { + const first = await controller.listTransactionsUnspent(addressA.bech32, { + size: 2 + }) + expect(first.data.length).toStrictEqual(2) + expect(first.page?.next).toMatch(/[0-f]{72}/) + expect(first.data[0].vout.value).toStrictEqual('34.00000000') + expect(first.data[1].vout.value).toStrictEqual('0.12340001') + + const next = await controller.listTransactionsUnspent(addressA.bech32, { + size: 10, + next: first.page?.next + }) + + expect(next.data.length).toStrictEqual(2) + expect(next.page?.next).toBeUndefined() + expect(next.data[0].vout.value).toStrictEqual('1.32412313') + expect(next.data[1].vout.value).toStrictEqual('2.93719381') + }) + it('(addressB) should listTransactionsUnspent', async () => { + const response = await controller.listTransactionsUnspent(addressB.bech32, { + size: 30 + }) + + expect(response.data.length).toStrictEqual(2) + expect(response.page).toBeUndefined() + + expect(response.data[1]).toStrictEqual({ + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + hid: expect.stringMatching(/[0-f]{64}/), + id: expect.stringMatching(/[0-f]{72}/), + script: { + hex: '00144ab4391ce5a732e36139e72d79a28e01b7b08034', + type: 'witness_v0_keyhash' + }, + sort: expect.stringMatching(/[0-f]{80}/), + vout: { + n: expect.any(Number), + tokenId: 0, + txid: expect.stringMatching(/[0-f]{64}/), + value: '1.12280000' + } + }) + }) + + it('should listTransactionsUnspent with undefined next pagination', async () => { + const first = await controller.listTransactionsUnspent(addressA.bech32, { + size: 2, + next: undefined + }) + + expect(first.data.length).toStrictEqual(2) + expect(first.page?.next).toMatch(/[0-f]{72}/) + }) +}) + +describe('listTokens', () => { + async function setupLoanToken (): Promise { + const oracleId = await testing.client.oracle.appointOracle(await testing.generateAddress(), [ + { + token: 'DFI', + currency: 'USD' + }, + { + token: 'LOAN', + currency: 'USD' + } + ], { weightage: 1 }) + await testing.generate(1) + + await testing.client.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [ + { + tokenAmount: '2@DFI', + currency: 'USD' + }, + { + tokenAmount: '2@LOAN', + currency: 'USD' + } + ] + }) + await testing.generate(1) + + await testing.client.loan.setCollateralToken({ + token: 'DFI', + factor: new BigNumber(1), + fixedIntervalPriceId: 'DFI/USD' + }) + await testing.client.loan.setLoanToken({ + symbol: 'LOAN', + name: 'LOAN', + fixedIntervalPriceId: 'LOAN/USD', + mintable: true, + interest: new BigNumber(0.02) + }) + await testing.generate(1) + + await testing.token.dfi({ + address: await testing.address('DFI'), + amount: 100 + }) + + await testing.client.loan.createLoanScheme({ + id: 'scheme', + minColRatio: 110, + interestRate: new BigNumber(1) + }) + await testing.generate(1) + + const vaultId = await testing.client.loan.createVault({ + ownerAddress: await testing.address('VAULT'), + loanSchemeId: 'scheme' + }) + await testing.generate(1) + + await testing.client.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [ + { + tokenAmount: '2@DFI', + currency: 'USD' + }, + { + tokenAmount: '2@LOAN', + currency: 'USD' + } + ] + }) + await testing.generate(1) + + await testing.client.loan.depositToVault({ + vaultId: vaultId, + from: await testing.address('DFI'), + amount: '100@DFI' + }) + await testing.generate(1) + await testing.client.loan.takeLoan({ + vaultId: vaultId, + amounts: '10@LOAN', + to: address + }) + await testing.generate(1) + } + + beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.addressController + testing = app.rpc + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) + + await app.waitForBlockHeight(100) + + for (const token of tokens) { + await app.waitForWalletBalanceGTE(110) + await app.createToken(token) + await app.mintTokens(token, { mintAmount: 1000 }) + await app.sendTokensToAddress(address, 10, token) + } + await app.generate(1) + + await setupLoanToken() + }) + + afterAll(async () => { + await app.stop() + }) + + const address = 'bcrt1qf5v8n3kfe6v5mharuvj0qnr7g74xnu9leut39r' + const tokens = ['A', 'B', 'C', 'D', 'E', 'F'] + + it('should listTokens', async () => { + const response = await controller.listTokens(address, { + size: 30 + }) + + expect(response.data.length).toStrictEqual(7) + expect(response.page).toBeUndefined() + + expect(response.data[5]).toStrictEqual({ + id: '6', + amount: '10.00000000', + symbol: 'F', + displaySymbol: 'dF', + symbolKey: 'F', + name: 'F', + isDAT: true, + isLPS: false, + isLoanToken: false + }) + + expect(response.data[6]).toStrictEqual({ + id: '7', + amount: '10.00000000', + symbol: 'LOAN', + displaySymbol: 'dLOAN', + symbolKey: 'LOAN', + name: 'LOAN', + isDAT: true, + isLPS: false, + isLoanToken: true + }) + }) + + it('should listTokens with pagination', async () => { + const first = await controller.listTokens(address, { + size: 2 + }) + expect(first.data.length).toStrictEqual(2) + expect(first.page?.next).toStrictEqual('2') + expect(first.data[0].symbol).toStrictEqual('A') + expect(first.data[1].symbol).toStrictEqual('B') + + const next = await controller.listTokens(address, { + size: 10, + next: first.page?.next + }) + + expect(next.data.length).toStrictEqual(5) + expect(next.page?.next).toBeUndefined() + expect(next.data[0].symbol).toStrictEqual('C') + expect(next.data[1].symbol).toStrictEqual('D') + expect(next.data[2].symbol).toStrictEqual('E') + expect(next.data[3].symbol).toStrictEqual('F') + expect(next.data[4].symbol).toStrictEqual('LOAN') + }) + + it('should listTokens with undefined next pagination', async () => { + const first = await controller.listTokens(address, { + size: 2, + next: undefined + }) + + expect(first.data.length).toStrictEqual(2) + expect(first.page?.next).toStrictEqual('2') + }) + + it('should return empty and page undefined while listTokens with invalid address', async () => { + try { + await controller.listTokens('invalid', { size: 30 }) + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Invalid owner address', + url: '/v0/regtest/address/invalid/tokens?size=30&next=undefined' + }) + } + }) +}) + +describe('listVaults', () => { + let vaultId: string + const address = 'bcrt1qf5v8n3kfe6v5mharuvj0qnr7g74xnu9leut39r' + + beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.addressController + testing = app.rpc + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) + + await app.waitForBlockHeight(100) + + await testing.client.loan.createLoanScheme({ + id: 'scheme', + minColRatio: 110, + interestRate: new BigNumber(1) + }) + await testing.generate(1) + + vaultId = await testing.client.vault.createVault({ + ownerAddress: address, + loanSchemeId: 'scheme' + }) + + await testing.client.vault.createVault({ + ownerAddress: await testing.address('VaultId1'), + loanSchemeId: 'scheme' + }) + await app.generate(1) + }) + + afterAll(async () => { + await app.stop() + }) + + it('should listVaults', async () => { + const response = await controller.listVaults(address, { + size: 30 + }) + expect(response.data.length).toStrictEqual(1) + expect(response.data[0]).toStrictEqual({ + vaultId: vaultId, + loanScheme: expect.any(Object), + ownerAddress: address, + state: 'active', + informativeRatio: '-1', + collateralRatio: '-1', + collateralValue: '0', + loanValue: '0', + interestValue: '0', + collateralAmounts: [], + loanAmounts: [], + interestAmounts: [] + }) + }) + + it('should return empty for other address', async () => { + const response = await controller.listVaults(await app.getNewAddress(), { + size: 30 + }) + expect(response.data).toStrictEqual([]) + }) + + it('should fail if providing empty address', async () => { + try { + await controller.listVaults('', { + size: 30 + }) + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + at: expect.any(Number), + code: 404, + message: 'recipient () does not refer to any valid address', + type: 'NotFound', + url: '/v0/regtest/address//vaults?size=30&next=undefined' + }) + } + }) + + it('should fail if providing invalid address', async () => { + try { + await controller.listVaults('INVALID', { + size: 30 + }) + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + at: expect.any(Number), + code: 404, + message: 'recipient (INVALID) does not refer to any valid address', + type: 'NotFound', + url: '/v0/regtest/address/INVALID/vaults?size=30&next=undefined' + }) + } + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/block.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/block.controller.defid.ts new file mode 100644 index 0000000000..1f1588df06 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/block.controller.defid.ts @@ -0,0 +1,157 @@ +import { parseHeight } from '../block.controller' +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' +import { DBlockController, DefidBin, DefidRpc } from '../../e2e.defid.module' + +let container: DefidRpc +let app: DefidBin +let controller: DBlockController +let client: JsonRpcClient + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.blockController + container = app.rpc + await app.waitForBlockHeight(101) + client = new JsonRpcClient(app.rpcUrl) + + const address = await app.getNewAddress() + for (let i = 0; i < 4; i += 1) { + await app.call('sendtoaddress', [address, 0.1]) + } + + await container.generate(3) + await app.waitForBlockHeight(103) +}) + +afterAll(async () => { + await app.stop() +}) + +describe('get', () => { + it('should get block based on hash', async () => { + const blockHash = await app.call('getblockhash', [100]) + const block = await controller.get(blockHash) + expect(block?.height).toStrictEqual(100) + expect(block?.hash).toStrictEqual(blockHash) + }) + + it('get should get block with height', async () => { + const block = await controller.get('100') + expect(block?.height).toStrictEqual(100) + }) + + it('should get undefined with invalid hash ', async () => { + const block = await controller.get('lajsdl;kfjljklj12lk34j') + expect(block).toStrictEqual(undefined) + }) +}) + +describe('list', () => { + it('should return paginated list of blocks', async () => { + const firstPage = await controller.list({ size: 40 }) + + expect(firstPage.data.length).toStrictEqual(40) + + expect(firstPage.data[0].height).toBeGreaterThanOrEqual(100) + + const secondPage = await controller.list({ size: 40, next: firstPage.page?.next }) + + expect(secondPage.data.length).toStrictEqual(40) + expect(secondPage.data[0].height).toStrictEqual(firstPage.data[39].height - 1) + + const lastPage = await controller.list({ size: 40, next: secondPage.page?.next }) + + expect(lastPage.data[0].height).toStrictEqual(secondPage.data[39].height - 1) + expect(lastPage.page?.next).toBeUndefined() + }) + + it('should return all the blocks if the size is out of range', async () => { + const paginatedBlocks = await controller.list({ size: 100000, next: '100' }) + + expect(paginatedBlocks.data.length).toStrictEqual(100) + expect(paginatedBlocks.data[0].height).toBeGreaterThanOrEqual(99) + }) + + it('list would return the latest set if next is outside of range', async () => { + const paginatedBlocks = await controller.list({ size: 30, next: '100000' }) + + expect(paginatedBlocks.data.length).toStrictEqual(30) + expect(paginatedBlocks.data[0].height).toBeGreaterThanOrEqual(100) + }) + + it('list would return the latest set if next is 0', async () => { + const paginatedBlocks = await controller.list({ size: 30, next: '0' }) + + expect(paginatedBlocks.data.length).toStrictEqual(0) + expect(paginatedBlocks?.page).toBeUndefined() + }) +}) + +describe('getTransactions', () => { + it('should get transactions from a block by hash', async () => { + const blockHash = await app.call('getblockhash', [100]) + const paginatedTransactions = await controller.getTransactions(blockHash, { size: 30 }) + + expect(paginatedTransactions.data.length).toBeGreaterThanOrEqual(1) + expect(paginatedTransactions.data[0].block.height).toStrictEqual(100) + }) + + it('getTransactions should not get transactions by height', async () => { + const paginatedTransactions = await controller.getTransactions('0', { size: 30 }) + + expect(paginatedTransactions.data.length).toStrictEqual(0) + }) + + it('getTransactions should get empty array when hash is not valid', async () => { + const paginatedTransactions = await controller.getTransactions('z1wadfsvq90qlkfalnklvm', { size: 30 }) + + expect(paginatedTransactions.data.length).toStrictEqual(0) + }) + + it('getTransactions should get empty array when height is not valid', async () => { + const paginatedTransactions = await controller.getTransactions('999999999999', { size: 30 }) + + expect(paginatedTransactions.data.length).toStrictEqual(0) + }) + + it('should list transactions in the right order', async () => { + const blockHash = await app.call('getblockhash', [103]) + const paginatedTransactions = await controller.getTransactions(blockHash, { size: 30 }) + + expect(paginatedTransactions.data.length).toBeGreaterThanOrEqual(4) + expect(paginatedTransactions.data[0].block.height).toStrictEqual(103) + + const rpcBlock = await client.blockchain.getBlock(blockHash, 2) + expect(paginatedTransactions.data[0].hash).toStrictEqual(rpcBlock.tx[0].hash) + expect(paginatedTransactions.data[1].hash).toStrictEqual(rpcBlock.tx[1].hash) + expect(paginatedTransactions.data[2].hash).toStrictEqual(rpcBlock.tx[2].hash) + expect(paginatedTransactions.data[3].hash).toStrictEqual(rpcBlock.tx[3].hash) + }) +}) + +describe('parseHeight', () => { + it('should return undefined for negative integer', () => { + expect(parseHeight('-123')).toStrictEqual(undefined) + }) + + it('should return undefined for float', () => { + expect(parseHeight('123.32')).toStrictEqual(undefined) + }) + + it('should return number for positive integers', () => { + expect(parseHeight('123')).toStrictEqual(123) + }) + + it('should return undefined for empty string', () => { + expect(parseHeight('')).toStrictEqual(undefined) + }) + + it('should return undefined for undefined', () => { + expect(parseHeight(undefined)).toStrictEqual(undefined) + }) + + it('should return undefined for strings with characters', () => { + expect(parseHeight('123a')).toStrictEqual(undefined) + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/fee.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/fee.controller.defid.ts new file mode 100644 index 0000000000..11fc860982 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/fee.controller.defid.ts @@ -0,0 +1,43 @@ +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' +import { DFeeController, DefidBin, DefidRpc } from '../../e2e.defid.module' + +let container: DefidRpc +let app: DefidBin +let controller: DFeeController +let client: JsonRpcClient + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.feeController + container = app.rpc + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) + + client = new JsonRpcClient(app.rpcUrl) + + await app.waitForBlockHeight(100) +}) + +afterAll(async () => { + await app.stop() +}) + +describe('fee/estimate', () => { + it('should have fee of 0.00005 and not 0.00005 after adding activity', async () => { + const before = await controller.estimate(10) + expect(before).toStrictEqual(0.00005000) + + for (let i = 0; i < 10; i++) { + for (let x = 0; x < 20; x++) { + await client.wallet.sendToAddress('bcrt1qf5v8n3kfe6v5mharuvj0qnr7g74xnu9leut39r', 0.1, { + subtractFeeFromAmount: true, + avoidReuse: false + }) + } + await container.generate(1) + } + const after = await controller.estimate(10) + expect(after).not.toStrictEqual(0.00005000) + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/governance.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/governance.controller.defid.ts new file mode 100644 index 0000000000..e57da8fe96 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/governance.controller.defid.ts @@ -0,0 +1,407 @@ +import { ListProposalsStatus, ListProposalsType, MasternodeType, VoteDecision } from '@defichain/jellyfish-api-core/dist/category/governance' +import { RegTestFoundationKeys } from '@defichain/jellyfish-network' +import { + GovernanceProposalStatus, + GovernanceProposalType, + ProposalVoteResultType +} from '@defichain/whale-api-client/dist/api/governance' +import BigNumber from 'bignumber.js' +import { DGovernanceController, DefidBin, DefidRpc } from '../../e2e.defid.module' + +let testing: DefidRpc +let app: DefidBin +let controller: DGovernanceController +let cfpProposalId: string +let vocProposalId: string +let payoutAddress: string + +describe('governance - listProposals and getProposal', () => { + beforeAll(async () => { + app = new DefidBin() + await app.start([ + `-masternode_operator=${RegTestFoundationKeys[2].operator.address}`, + `-masternode_operator=${RegTestFoundationKeys[3].operator.address}` + ]) + controller = app.ocean.governanceController + testing = app.rpc + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) + await app.call('setgov', [ + { ATTRIBUTES: { 'v0/params/feature/gov': 'true' } } + ]) + await app.generate(1) + + // Create 1 CFP + 1 VOC + payoutAddress = await testing.generateAddress() + cfpProposalId = await testing.client.governance.createGovCfp({ + title: 'CFP proposal', + context: 'github', + amount: new BigNumber(1.23), + payoutAddress: payoutAddress, + cycles: 2 + }) + await app.generate(1) + + vocProposalId = await testing.client.governance.createGovVoc({ + title: 'VOC proposal', + context: 'github' + }) + await app.generate(1) + }) + + afterAll(async () => { + await app.stop() + }) + + // Listing related tests + it('should listProposals', async () => { + const result = await controller.listProposals() + const cfpResult = result.data.find(proposal => proposal.type === GovernanceProposalType.COMMUNITY_FUND_PROPOSAL) + const vocResult = result.data.find(proposal => proposal.type === GovernanceProposalType.VOTE_OF_CONFIDENCE) + expect(result.data.length).toStrictEqual(2) + expect(cfpResult).toStrictEqual({ + proposalId: cfpProposalId, + creationHeight: expect.any(Number), + title: 'CFP proposal', + context: 'github', + contextHash: '', + status: GovernanceProposalStatus.VOTING, + type: GovernanceProposalType.COMMUNITY_FUND_PROPOSAL, + amount: new BigNumber(1.23).toFixed(8), + payoutAddress: payoutAddress, + currentCycle: 1, + totalCycles: 2, + cycleEndHeight: expect.any(Number), + proposalEndHeight: expect.any(Number), + votingPeriod: expect.any(Number), + quorum: expect.any(String), + approvalThreshold: expect.any(String), + fee: expect.any(Number) + }) + expect(vocResult).toStrictEqual({ + proposalId: vocProposalId, + creationHeight: expect.any(Number), + title: 'VOC proposal', + context: 'github', + contextHash: '', + status: GovernanceProposalStatus.VOTING, + type: GovernanceProposalType.VOTE_OF_CONFIDENCE, + // amount: undefined, + currentCycle: 1, + totalCycles: 1, + cycleEndHeight: expect.any(Number), + proposalEndHeight: expect.any(Number), + votingPeriod: expect.any(Number), + quorum: expect.any(String), + approvalThreshold: expect.any(String), + fee: expect.any(Number) + }) + }) + + it('should listProposals with size', async () => { + const result = await controller.listProposals(undefined, undefined, undefined, undefined, { size: 1 }) + expect(result.data.length).toStrictEqual(1) + }) + + it('should listProposals with status', async () => { + const result = await controller.listProposals(ListProposalsStatus.VOTING) + expect(result.data.length).toStrictEqual(2) + }) + + it('should listProposals with type', async () => { + const result = await controller.listProposals(undefined, ListProposalsType.CFP) + expect(result.data.length).toStrictEqual(1) + }) + + it('should listProposals with cycle', async () => { + const result = await controller.listProposals(undefined, undefined, 0) + expect(result.data.length).toStrictEqual(2) + }) + + it('should listProposals with status and type', async () => { + const result = await controller.listProposals(ListProposalsStatus.VOTING, ListProposalsType.CFP) + expect(result.data.length).toStrictEqual(1) + }) + + it('should listProposals with status, type and cycle', async () => { + const result = await controller.listProposals(ListProposalsStatus.VOTING, ListProposalsType.CFP, 0) + expect(result.data.length).toStrictEqual(1) + }) + + it('should listProposals with pagination', async () => { + const resultPage1 = await controller.listProposals(undefined, undefined, undefined, undefined, { + size: 1 + }) + expect(resultPage1.data.length).toStrictEqual(1) + const resultPage2 = await controller.listProposals(undefined, undefined, undefined, undefined, { + next: resultPage1.page?.next, + size: 1 + }) + expect(resultPage2.data.length).toStrictEqual(1) + }) + + it('should listProposals with all record when limit is 0', async () => { + const result = await controller.listProposals(undefined, undefined, undefined, undefined, { + size: 0 + }) + expect(result.data.length).toStrictEqual(2) + const emptyResult = await controller.listProposals(ListProposalsStatus.REJECTED, undefined, undefined, undefined, { + size: 0 + }) + expect(emptyResult.data.length).toStrictEqual(0) + }) + + it('should listProposals with all record when all flag is true', async () => { + const result = await controller.listProposals(undefined, undefined, undefined, true) + expect(result.data.length).toStrictEqual(2) + }) + + it('should listProposals with status and pagination', async () => { + const resultPage1 = await controller.listProposals(ListProposalsStatus.VOTING, undefined, undefined, undefined, { + size: 1 + }) + expect(resultPage1.data.length).toStrictEqual(1) + const resultPage2 = await controller.listProposals(ListProposalsStatus.VOTING, undefined, undefined, undefined, { + next: resultPage1.page?.next, + size: 1 + }) + expect(resultPage2.data.length).toStrictEqual(1) + }) + + it('should listProposals with type and pagination', async () => { + const resultPage1 = await controller.listProposals(undefined, ListProposalsType.CFP, undefined, undefined, { + size: 1 + }) + expect(resultPage1.data.length).toStrictEqual(1) + const resultPage2 = await controller.listProposals(undefined, ListProposalsType.CFP, undefined, undefined, { + next: resultPage1.page?.next, + size: 1 + }) + expect(resultPage2.data.length).toStrictEqual(0) + }) + + it('should listProposals with status, type and pagination', async () => { + const resultPage1 = await controller.listProposals(ListProposalsStatus.VOTING, ListProposalsType.CFP, undefined, undefined, { + size: 1 + }) + expect(resultPage1.data.length).toStrictEqual(1) + const resultPage2 = await controller.listProposals(ListProposalsStatus.VOTING, ListProposalsType.CFP, undefined, undefined, { + next: resultPage1.page?.next, + size: 1 + }) + expect(resultPage2.data.length).toStrictEqual(0) + }) + + // Get single related tests + it('should getProposal for CFP', async () => { + const result = await controller.getProposal(cfpProposalId) + expect(result).toStrictEqual({ + proposalId: cfpProposalId, + creationHeight: expect.any(Number), + title: 'CFP proposal', + context: 'github', + contextHash: '', + status: GovernanceProposalStatus.VOTING, + type: GovernanceProposalType.COMMUNITY_FUND_PROPOSAL, + amount: new BigNumber(1.23).toFixed(8), + payoutAddress: payoutAddress, + currentCycle: 1, + totalCycles: 2, + cycleEndHeight: expect.any(Number), + proposalEndHeight: expect.any(Number), + votingPeriod: expect.any(Number), + quorum: expect.any(String), + approvalThreshold: expect.any(String), + fee: expect.any(Number) + }) + }) + + it('should getProposal for VOC', async () => { + const result = await controller.getProposal(vocProposalId) + expect(result).toStrictEqual({ + proposalId: vocProposalId, + creationHeight: 104, + title: 'VOC proposal', + context: 'github', + contextHash: '', + status: GovernanceProposalStatus.VOTING, + type: GovernanceProposalType.VOTE_OF_CONFIDENCE, + currentCycle: 1, + totalCycles: 1, + cycleEndHeight: expect.any(Number), + proposalEndHeight: expect.any(Number), + votingPeriod: expect.any(Number), + quorum: expect.any(String), + approvalThreshold: expect.any(String), + fee: expect.any(Number) + // amount: undefined + }) + }) +}) + +describe('governance - listProposalVotes', () => { + beforeAll(async () => { + app = new DefidBin() + await app.start([ + `-masternode_operator=${RegTestFoundationKeys[1].operator.address}`, + `-masternode_operator=${RegTestFoundationKeys[2].operator.address}`, + `-masternode_operator=${RegTestFoundationKeys[3].operator.address}` + ]) + controller = app.ocean.governanceController + testing = app.rpc + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) + await app.call('setgov', [ + { ATTRIBUTES: { 'v0/params/feature/gov': 'true' } } + ]) + await app.generate(1) + + /** + * Import the private keys of the masternode_operator in order to be able to mint blocks and vote on proposals. + * This setup uses the default masternode + two additional masternodes for a total of 3 masternodes. + */ + await testing.client.wallet.importPrivKey(RegTestFoundationKeys[1].owner.privKey) + await testing.client.wallet.importPrivKey(RegTestFoundationKeys[1].operator.privKey) + await testing.client.wallet.importPrivKey(RegTestFoundationKeys[2].owner.privKey) + await testing.client.wallet.importPrivKey(RegTestFoundationKeys[2].operator.privKey) + await testing.client.wallet.importPrivKey(RegTestFoundationKeys[3].owner.privKey) + await testing.client.wallet.importPrivKey(RegTestFoundationKeys[3].operator.privKey) + + // Create 1 CFP + 1 VOC + payoutAddress = await testing.generateAddress() + cfpProposalId = await testing.client.governance.createGovCfp({ + title: 'CFP proposal', + context: 'github', + amount: new BigNumber(1.23), + payoutAddress: payoutAddress, + cycles: 2 + }) + await app.generate(1) + + vocProposalId = await testing.client.governance.createGovVoc({ + title: 'VOC proposal', + context: 'github' + }) + await app.generate(1) + + // Vote on CFP + await testing.client.governance.voteGov({ + proposalId: cfpProposalId, + masternodeId: await getVotableMasternodeId(), + decision: VoteDecision.YES + }) + await app.generate(1) + + // Expires cycle 1 + const creationHeight = await testing.client.governance.getGovProposal(cfpProposalId).then(proposal => proposal.creationHeight) + const votingPeriod = 70 + const cycle1 = creationHeight + (votingPeriod - creationHeight % votingPeriod) + votingPeriod + await app.generate(cycle1 - await app.getBlockCount()) + + // Vote on cycle 2 + const masternodes = await testing.client.masternode.listMasternodes() + const votes = [VoteDecision.YES, VoteDecision.NO, VoteDecision.NEUTRAL] + let index = 0 + for (const [id, data] of Object.entries(masternodes)) { + if (data.operatorIsMine) { + await app.generate(1, data.operatorAuthAddress) // Generate a block to operatorAuthAddress to be allowed to vote on proposal + await testing.client.governance.voteGov({ + proposalId: cfpProposalId, + masternodeId: id, + decision: votes[index] + }) + index++ // all masternodes vote in second cycle + } + } + await app.generate(1) + }) + + afterAll(async () => { + await app.stop() + }) + + it('should listProposalVotes', async () => { + const result = await controller.listProposalVotes(cfpProposalId) + const yesVote = result.data.find(vote => vote.vote === ProposalVoteResultType.YES) + const noVote = result.data.find(vote => vote.vote === ProposalVoteResultType.NO) + const neutralVote = result.data.find(vote => vote.vote === ProposalVoteResultType.NEUTRAL) + expect(result.data.length).toStrictEqual(3) + expect(yesVote).toStrictEqual({ + proposalId: cfpProposalId, + masternodeId: expect.any(String), + cycle: 2, + vote: ProposalVoteResultType.YES, + valid: true + }) + expect(noVote).toStrictEqual({ + proposalId: cfpProposalId, + masternodeId: expect.any(String), + cycle: 2, + vote: ProposalVoteResultType.NO, + valid: true + }) + expect(neutralVote).toStrictEqual({ + proposalId: cfpProposalId, + masternodeId: expect.any(String), + cycle: 2, + vote: ProposalVoteResultType.NEUTRAL, + valid: true + }) + }) + + it('should listProposalVotes with cycle', async () => { + const result = await controller.listProposalVotes(cfpProposalId, undefined, 2) + expect(result.data.length).toStrictEqual(3) + }) + + it('should listProposalVotes with all records when limit is 0', async () => { + const result = await controller.listProposalVotes(cfpProposalId, undefined, undefined, undefined, { size: 0 }) + expect(result.data.length).toStrictEqual(3) + }) + + it('should listProposalVotes with all records when all flag is true', async () => { + const result = await controller.listProposalVotes(cfpProposalId, undefined, undefined, true) + expect(result.data.length).toStrictEqual(3) + const emptyResult = await controller.listProposalVotes(vocProposalId, undefined, undefined, true) + expect(emptyResult.data.length).toStrictEqual(0) + }) + + it('should listProposalVotes with all masternodes', async () => { + const result = await controller.listProposalVotes(cfpProposalId, MasternodeType.ALL) + expect(result.data.length).toStrictEqual(3) + }) + + it('should listProposalVotes with all masternodes and cycle', async () => { + const result = await controller.listProposalVotes(cfpProposalId, MasternodeType.ALL, -1) + expect(result.data.length).toStrictEqual(4) + + const result2 = await controller.listProposalVotes(cfpProposalId, MasternodeType.ALL, 0) + expect(result2.data.length).toStrictEqual(3) + }) + + it('should listProposalVotes with all masternodes, cycle and pagination', async () => { + const resultPage1 = await controller.listProposalVotes(cfpProposalId, MasternodeType.ALL, 2, undefined, { size: 2 }) + expect(resultPage1.data.length).toStrictEqual(2) + const resultPage2 = await controller.listProposalVotes(cfpProposalId, MasternodeType.ALL, 2, undefined, { next: resultPage1.page?.next, size: 2 }) + expect(resultPage2.data.length).toStrictEqual(1) + }) +}) + +/** + * Return masternode that mined at least one block to vote on proposal + */ +async function getVotableMasternodeId (): Promise { + const masternodes = await testing.client.masternode.listMasternodes() + let masternodeId = '' + for (const id in masternodes) { + const masternode = masternodes[id] + if (masternode.mintedBlocks > 0) { + masternodeId = id + break + } + } + if (masternodeId === '') { + throw new Error('No masternode is available to vote') + } + return masternodeId +} diff --git a/apps/whale-api/src/module.api/__defid__/loan.auction.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/loan.auction.controller.defid.ts new file mode 100644 index 0000000000..263bb43399 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/loan.auction.controller.defid.ts @@ -0,0 +1,402 @@ +import BigNumber from 'bignumber.js' +import { DLoanController, DefidBin, DefidRpc, DefidRpcClient } from '../../e2e.defid.module' + +let app: DefidBin +let rpc: DefidRpc +let client: DefidRpcClient +let controller: DLoanController + +let vaultId1: string +let vaultId2: string +let vaultId3: string +let vaultId4: string + +function now (): number { + return Math.floor(new Date().getTime() / 1000) +} + +beforeAll(async () => { + app = new DefidBin() + rpc = app.rpc + client = app.rpcClient + controller = app.ocean.loanController + + await app.start() + await app.waitForWalletCoinbaseMaturity() + + const aliceColAddr = await rpc.generateAddress() + await rpc.token.dfi({ address: aliceColAddr, amount: 300000 }) + await rpc.token.create({ symbol: 'BTC', collateralAddress: aliceColAddr }) + await rpc.generate(1) + + await rpc.token.mint({ symbol: 'BTC', amount: 100 }) + await rpc.generate(1) + + await client.loan.createLoanScheme({ + minColRatio: 100, + interestRate: new BigNumber(1), + id: 'default' + }) + await rpc.generate(1) + + const addr = await rpc.generateAddress() + const priceFeeds = [ + { token: 'DFI', currency: 'USD' }, + { token: 'BTC', currency: 'USD' }, + { token: 'DUSD', currency: 'USD' }, + { token: 'AAPL', currency: 'USD' }, + { token: 'TSLA', currency: 'USD' }, + { token: 'MSFT', currency: 'USD' }, + { token: 'FB', currency: 'USD' } + ] + const oracleId = await client.oracle.appointOracle(addr, priceFeeds, { weightage: 1 }) + await rpc.generate(1) + + await client.oracle.setOracleData(oracleId, now(), { + prices: [ + { tokenAmount: '1@DFI', currency: 'USD' }, + { tokenAmount: '10000@BTC', currency: 'USD' }, + { tokenAmount: '1@DUSD', currency: 'USD' }, + { tokenAmount: '2@AAPL', currency: 'USD' }, + { tokenAmount: '2@TSLA', currency: 'USD' }, + { tokenAmount: '2@MSFT', currency: 'USD' }, + { tokenAmount: '2@FB', currency: 'USD' } + ] + }) + await rpc.generate(1) + + await client.loan.setCollateralToken({ + token: 'DFI', + factor: new BigNumber(1), + fixedIntervalPriceId: 'DFI/USD' + }) + + await client.loan.setCollateralToken({ + token: 'BTC', + factor: new BigNumber(1), + fixedIntervalPriceId: 'BTC/USD' + }) + + await client.loan.setLoanToken({ + symbol: 'DUSD', + fixedIntervalPriceId: 'DUSD/USD' + }) + + await client.loan.setLoanToken({ + symbol: 'AAPL', + fixedIntervalPriceId: 'AAPL/USD' + }) + + await client.loan.setLoanToken({ + symbol: 'TSLA', + fixedIntervalPriceId: 'TSLA/USD' + }) + + await client.loan.setLoanToken({ + symbol: 'MSFT', + fixedIntervalPriceId: 'MSFT/USD' + }) + + await client.loan.setLoanToken({ + symbol: 'FB', + fixedIntervalPriceId: 'FB/USD' + }) + await rpc.generate(1) + + const mVaultId = await client.vault.createVault({ + ownerAddress: await rpc.generateAddress(), + loanSchemeId: 'default' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: mVaultId, from: aliceColAddr, amount: '100000@DFI' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: mVaultId, from: aliceColAddr, amount: '10@BTC' + }) + await rpc.generate(1) + + await client.loan.takeLoan({ + vaultId: mVaultId, + amounts: ['10000@AAPL', '10000@TSLA', '10000@MSFT', '10000@FB'], + to: aliceColAddr + }) + await rpc.generate(1) + + // Vault 1 + vaultId1 = await client.vault.createVault({ + ownerAddress: await rpc.generateAddress(), + loanSchemeId: 'default' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: vaultId1, from: aliceColAddr, amount: '1000@DFI' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: vaultId1, from: aliceColAddr, amount: '0.05@BTC' + }) + await rpc.generate(1) + + await client.loan.takeLoan({ + vaultId: vaultId1, + amounts: '750@AAPL', + to: aliceColAddr + }) + await rpc.generate(1) + + // Vault 2 + vaultId2 = await client.vault.createVault({ + ownerAddress: await rpc.generateAddress(), + loanSchemeId: 'default' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: vaultId2, from: aliceColAddr, amount: '2000@DFI' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: vaultId2, from: aliceColAddr, amount: '0.1@BTC' + }) + await rpc.generate(1) + + await client.loan.takeLoan({ + vaultId: vaultId2, + amounts: '1500@TSLA', + to: aliceColAddr + }) + await rpc.generate(1) + + // Vault 3 + vaultId3 = await client.vault.createVault({ + ownerAddress: await rpc.generateAddress(), + loanSchemeId: 'default' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: vaultId3, from: aliceColAddr, amount: '3000@DFI' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: vaultId3, from: aliceColAddr, amount: '0.15@BTC' + }) + await rpc.generate(1) + + await client.loan.takeLoan({ + vaultId: vaultId3, + amounts: '2250@MSFT', + to: aliceColAddr + }) + await rpc.generate(1) + + // Vault 4 + vaultId4 = await client.vault.createVault({ + ownerAddress: await rpc.generateAddress(), + loanSchemeId: 'default' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: vaultId4, from: aliceColAddr, amount: '4000@DFI' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: vaultId4, from: aliceColAddr, amount: '0.2@BTC' + }) + await rpc.generate(1) + + await client.loan.takeLoan({ + vaultId: vaultId4, + amounts: '3000@FB', + to: aliceColAddr + }) + await rpc.generate(1) + + const auctions = await client.vault.listAuctions() + expect(auctions).toStrictEqual([]) + + const vaults = await client.vault.listVaults() + expect(vaults.every(v => v.state === 'active')) + + // Going to liquidate the vaults by price increase of the loan tokens + await client.oracle.setOracleData(oracleId, now(), { + prices: [ + { tokenAmount: '2.2@AAPL', currency: 'USD' }, + { tokenAmount: '2.2@TSLA', currency: 'USD' }, + { tokenAmount: '2.2@MSFT', currency: 'USD' }, + { tokenAmount: '2.2@FB', currency: 'USD' } + ] + }) + await app.waitForActivePrice('AAPL/USD', '2.2') + await app.waitForActivePrice('TSLA/USD', '2.2') + await app.waitForActivePrice('MSFT/USD', '2.2') + await app.waitForActivePrice('FB/USD', '2.2') + + { + const vaults = await client.vault.listVaults() + expect(vaults.every(v => v.state === 'inLiquidation')) + } + + const bobAddr = await rpc.generateAddress() + const charlieAddr = await rpc.generateAddress() + + await client.wallet.sendToAddress(charlieAddr, 100) + + await client.account.accountToAccount(aliceColAddr, { + [bobAddr]: '4000@AAPL', + [charlieAddr]: '4000@AAPL' + }) + await rpc.generate(1) + + await client.account.accountToAccount(aliceColAddr, { + [bobAddr]: '4000@TSLA', + [charlieAddr]: '4000@TSLA' + }) + await rpc.generate(1) + + await client.account.accountToAccount(aliceColAddr, { + [bobAddr]: '4000@MSFT', + [charlieAddr]: '4000@MSFT' + }) + await rpc.generate(1) + + // bid #1 + await client.vault.placeAuctionBid({ + vaultId: vaultId1, + index: 0, + from: bobAddr, + amount: '800@AAPL' + }) + await rpc.generate(1) + + await client.vault.placeAuctionBid({ + vaultId: vaultId1, + index: 0, + from: charlieAddr, + amount: '900@AAPL' + }) + await rpc.generate(1) + + // bid #2 + await client.vault.placeAuctionBid({ + vaultId: vaultId2, + index: 0, + from: bobAddr, + amount: '2000@TSLA' + }) + await rpc.generate(1) + + await client.vault.placeAuctionBid({ + vaultId: vaultId2, + index: 0, + from: aliceColAddr, + amount: '2100@TSLA' + }) + await rpc.generate(1) + + // bid #3 + await client.loan.placeAuctionBid({ + vaultId: vaultId3, + index: 0, + from: bobAddr, + amount: '3000@MSFT' + }) + await rpc.generate(1) + + await client.loan.placeAuctionBid({ + vaultId: vaultId3, + index: 0, + from: aliceColAddr, + amount: '3100@MSFT' + }) + await rpc.generate(1) + + await client.loan.placeAuctionBid({ + vaultId: vaultId3, + index: 0, + from: charlieAddr, + amount: '3200@MSFT' + }) + await rpc.generate(1) + + const height = await app.call('getblockcount') + await app.waitForBlockHeight(height - 1) +}) + +afterAll(async () => { + await app.stop() +}) + +describe('list', () => { + it('should listAuctions', async () => { + const result = await controller.listAuction({ size: 100 }) + expect(result.data.length).toStrictEqual(4) + + for (let i = 0; i < result.data.length; i += 1) { + const auction = result.data[i] + expect(auction).toStrictEqual({ + batchCount: expect.any(Number), + batches: expect.any(Object), + loanScheme: expect.any(Object), + ownerAddress: expect.any(String), + state: expect.any(String), + liquidationHeight: expect.any(Number), + liquidationPenalty: expect.any(Number), + vaultId: expect.any(String) + }) + + for (let j = 0; j < auction.batches.length; j += 1) { + const batch = auction.batches[j] + expect(typeof batch.index).toBe('number') + expect(typeof batch.collaterals).toBe('object') + expect(typeof batch.loan).toBe('object') + if (auction.vaultId === vaultId4) { + expect(batch.froms.length).toStrictEqual(0) + } else { + expect(batch.froms.length).toBeGreaterThan(0) + expect(batch.froms).toStrictEqual( + expect.arrayContaining([expect.any(String)]) + ) + } + expect(typeof batch.highestBid === 'object' || batch.highestBid === undefined).toBe(true) + } + } + }) + + it('should listAuctions with pagination', async () => { + const first = await controller.listAuction({ size: 2 }) + expect(first.data.length).toStrictEqual(2) + expect(first.page?.next).toStrictEqual(`${first.data[1].vaultId}${first.data[1].liquidationHeight}`) + + const next = await controller.listAuction({ + size: 2, + next: first.page?.next + }) + expect(next.data.length).toStrictEqual(2) + expect(next.page?.next).toStrictEqual(`${next.data[1].vaultId}${next.data[1].liquidationHeight}`) + + const last = await controller.listAuction({ + size: 2, + next: next.page?.next + }) + expect(last.data.length).toStrictEqual(0) + expect(last.page).toBeUndefined() + }) + + it('should listAuctions with an empty object if out of range', async () => { + const result = await controller.listAuction({ size: 100, next: '51f6233c4403f6ce113bb4e90f83b176587f401081605b8a8bb723ff3b0ab5b6300' }) + + expect(result.data.length).toStrictEqual(0) + expect(result.page).toBeUndefined() + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/loan.auction.history.defid.ts b/apps/whale-api/src/module.api/__defid__/loan.auction.history.defid.ts new file mode 100644 index 0000000000..e90dafd975 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/loan.auction.history.defid.ts @@ -0,0 +1,428 @@ +import BigNumber from 'bignumber.js' +import { DLoanController, DefidBin, DefidRpc, DefidRpcClient } from '../../e2e.defid.module' +import { VaultLiquidation } from '@defichain/jellyfish-api-core/dist/category/vault' +import { HexEncoder } from '../../module.model/_hex.encoder' + +let app: DefidBin +let rpc: DefidRpc +let client: DefidRpcClient +let controller: DLoanController + +let colAddr: string +let bobColAddr: string +let vaultId: string +let batch: number +let batch1: number + +function now (): number { + return Math.floor(new Date().getTime() / 1000) +} + +beforeAll(async () => { + app = new DefidBin() + rpc = app.rpc + client = app.rpcClient + controller = app.ocean.loanController + + await app.start() + await app.waitForWalletCoinbaseMaturity() + + colAddr = await rpc.generateAddress() + bobColAddr = await rpc.generateAddress() + await rpc.token.dfi({ address: colAddr, amount: 300000 }) + await rpc.token.create({ symbol: 'BTC', collateralAddress: colAddr }) + await rpc.generate(1) + + await rpc.token.mint({ symbol: 'BTC', amount: 50 }) + await rpc.generate(1) + + await app.sendTokensToAddress(colAddr, 25, 'BTC') + await rpc.generate(1) + + await client.loan.createLoanScheme({ + minColRatio: 100, + interestRate: new BigNumber(1), + id: 'default' + }) + await rpc.generate(1) + + const addr = await rpc.generateAddress() + const priceFeeds = [ + { token: 'DFI', currency: 'USD' }, + { token: 'BTC', currency: 'USD' }, + { token: 'AAPL', currency: 'USD' }, + { token: 'TSLA', currency: 'USD' }, + { token: 'MSFT', currency: 'USD' } + ] + const oracleId = await client.oracle.appointOracle(addr, priceFeeds, { weightage: 1 }) + await rpc.generate(1) + + await client.oracle.setOracleData(oracleId, now(), { + prices: [ + { tokenAmount: '1@DFI', currency: 'USD' }, + { tokenAmount: '10000@BTC', currency: 'USD' }, + { tokenAmount: '2@AAPL', currency: 'USD' }, + { tokenAmount: '2@TSLA', currency: 'USD' }, + { tokenAmount: '2@MSFT', currency: 'USD' } + ] + }) + await rpc.generate(1) + + await client.loan.setCollateralToken({ + token: 'DFI', + factor: new BigNumber(1), + fixedIntervalPriceId: 'DFI/USD' + }) + await rpc.generate(1) + + await client.loan.setCollateralToken({ + token: 'BTC', + factor: new BigNumber(1), + fixedIntervalPriceId: 'BTC/USD' + }) + await rpc.generate(1) + + await client.loan.setLoanToken({ + symbol: 'AAPL', + fixedIntervalPriceId: 'AAPL/USD' + }) + await rpc.generate(1) + + await client.loan.setLoanToken({ + symbol: 'TSLA', + fixedIntervalPriceId: 'TSLA/USD' + }) + await rpc.generate(1) + + await client.loan.setLoanToken({ + symbol: 'MSFT', + fixedIntervalPriceId: 'MSFT/USD' + }) + await rpc.generate(1) + + const mVaultId = await client.vault.createVault({ + ownerAddress: await rpc.generateAddress(), + loanSchemeId: 'default' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: mVaultId, from: colAddr, amount: '200001@DFI' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: mVaultId, from: colAddr, amount: '20@BTC' + }) + await rpc.generate(1) + + await client.loan.takeLoan({ + vaultId: mVaultId, + amounts: ['60000@TSLA', '60000@AAPL', '60000@MSFT'], + to: colAddr + }) + await rpc.generate(1) + + await app.sendTokensToAddress(bobColAddr, 30000, 'TSLA') + await app.sendTokensToAddress(bobColAddr, 30000, 'AAPL') + await app.sendTokensToAddress(bobColAddr, 30000, 'MSFT') + await rpc.generate(1) + + vaultId = await client.vault.createVault({ + ownerAddress: await rpc.generateAddress(), + loanSchemeId: 'default' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: vaultId, from: colAddr, amount: '10001@DFI' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: vaultId, from: colAddr, amount: '1@BTC' + }) + await rpc.generate(1) + + await client.loan.takeLoan({ + vaultId: vaultId, + amounts: '7500@AAPL', + to: colAddr + }) + await rpc.generate(1) + + await client.loan.takeLoan({ + vaultId: vaultId, + amounts: '2500@TSLA', + to: colAddr + }) + await rpc.generate(1) + + const auctions = await client.vault.listAuctions() + expect(auctions).toStrictEqual([]) + + const vaults = await client.vault.listVaults() + expect(vaults.every(v => v.state === 'active')) + + // Going to liquidate the vaults by price increase of the loan tokens + await client.oracle.setOracleData(oracleId, now(), { + prices: [ + { tokenAmount: '2.2@AAPL', currency: 'USD' }, + { tokenAmount: '2.2@TSLA', currency: 'USD' } + ] + }) + await app.waitForActivePrice('AAPL/USD', '2.2') + await app.waitForActivePrice('TSLA/USD', '2.2') + await rpc.generate(13) + + { + const vaults = await client.vault.listVaults() + expect(vaults.every(v => v.state === 'inLiquidation')) + } + + let vault = await rpc.client.vault.getVault(vaultId) as VaultLiquidation + batch = vault.liquidationHeight + + // bid #1 + await client.vault.placeAuctionBid({ + vaultId: vaultId, + index: 0, + from: colAddr, + amount: '5300@AAPL' + }) + await rpc.generate(1) + + await client.vault.placeAuctionBid({ + vaultId: vaultId, + index: 0, + from: bobColAddr, + amount: '5355@AAPL' + }) + await rpc.generate(1) + + await client.vault.placeAuctionBid({ + vaultId: vaultId, + index: 0, + from: colAddr, + amount: '5408.55@AAPL' + }) + await rpc.generate(1) + + // bid #2 + await client.vault.placeAuctionBid({ + vaultId: vaultId, + index: 1, + from: colAddr, + amount: '2700.00012@AAPL' + }) + await rpc.generate(1) + + await client.vault.placeAuctionBid({ + vaultId: vaultId, + index: 1, + from: bobColAddr, + amount: '2730@AAPL' + }) + await rpc.generate(1) + + await client.vault.placeAuctionBid({ + vaultId: vaultId, + index: 1, + from: colAddr, + amount: '2760.0666069@AAPL' + }) + await rpc.generate(1) + + // bid #3 + await client.vault.placeAuctionBid({ + vaultId: vaultId, + index: 2, + from: colAddr, + amount: '2625.01499422@TSLA' + }) + await rpc.generate(1) + + await rpc.generate(40) + + await client.vault.depositToVault({ + vaultId: vaultId, from: colAddr, amount: '10001@DFI' + }) + await rpc.generate(1) + + await client.vault.depositToVault({ + vaultId: vaultId, from: colAddr, amount: '1@BTC' + }) + await rpc.generate(1) + + await client.loan.takeLoan({ + vaultId: vaultId, + amounts: '10000@MSFT', + to: colAddr + }) + await rpc.generate(1) + + // liquidated #2 + await client.oracle.setOracleData(oracleId, now(), { + prices: [ + { tokenAmount: '2.2@MSFT', currency: 'USD' } + ] + }) + await app.waitForActivePrice('MSFT/USD', '2.2') + await rpc.generate(13) + + vault = await rpc.client.vault.getVault(vaultId) as VaultLiquidation + batch1 = vault.liquidationHeight + + await client.vault.placeAuctionBid({ + vaultId: vaultId, + index: 0, + from: colAddr, + amount: '5300.123@MSFT' + }) + await rpc.generate(1) + + await client.vault.placeAuctionBid({ + vaultId: vaultId, + index: 0, + from: bobColAddr, + amount: '5355.123@MSFT' + }) + await rpc.generate(1) + + const height = await app.call('getblockcount') + await app.waitForBlockHeight(height - 1) +}) + +afterAll(async () => { + await app.stop() +}) + +it('should listVaultAuctionHistory', async () => { + { + const list = await controller.listVaultAuctionHistory(vaultId, batch, '0', { size: 30 }) + expect(list.data.length).toStrictEqual(3) + expect(list.data).toStrictEqual([ + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(list.data[0].block.height)}-${list.data[0].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + address: colAddr, + amount: '5408.55', + tokenId: 2, + block: expect.any(Object) + }, + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(list.data[1].block.height)}-${list.data[1].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + address: bobColAddr, + amount: '5355', + tokenId: 2, + block: expect.any(Object) + }, + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(list.data[2].block.height)}-${list.data[2].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + address: colAddr, + amount: '5300', + tokenId: 2, + block: expect.any(Object) + } + ]) + } + + { + const list = await controller.listVaultAuctionHistory(vaultId, batch1, '0', { size: 30 }) + expect(list.data.length).toStrictEqual(2) + expect(list.data).toStrictEqual([ + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(list.data[0].block.height)}-${list.data[0].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + address: bobColAddr, + amount: '5355.123', + tokenId: 4, + block: expect.any(Object) + }, + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(list.data[1].block.height)}-${list.data[1].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + address: colAddr, + amount: '5300.123', + tokenId: 4, + block: expect.any(Object) + } + ]) + } +}) + +it('should listVaultAuctionHistory with pagination', async () => { + const first = await controller.listVaultAuctionHistory(vaultId, batch, '0', { size: 1 }) + expect(first.data.length).toStrictEqual(1) + expect(first.data).toStrictEqual([ + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(first.data[0].block.height)}-${first.data[0].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + address: colAddr, + amount: '5408.55', + tokenId: 2, + block: expect.any(Object) + } + ]) + expect(first.page).toStrictEqual({ next: first.data[0].sort }) + + const next = await controller.listVaultAuctionHistory(vaultId, batch, '0', { size: 1, next: first?.page?.next }) + expect(next.data).toStrictEqual([ + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(next.data[0].block.height)}-${next.data[0].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + address: bobColAddr, + amount: '5355', + tokenId: 2, + block: expect.any(Object) + } + ]) + expect(next.page).toStrictEqual({ next: next.data[0].sort }) + + const last = await controller.listVaultAuctionHistory(vaultId, batch, '0', { size: 2, next: next?.page?.next }) + expect(last.data).toStrictEqual([ + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(last.data[0].block.height)}-${last.data[0].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + address: colAddr, + amount: '5300', + tokenId: 2, + block: expect.any(Object) + } + ]) + expect(last.page).toStrictEqual(undefined) +}) diff --git a/apps/whale-api/src/module.api/__defid__/loan.collateral.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/loan.collateral.controller.defid.ts new file mode 100644 index 0000000000..e412eca831 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/loan.collateral.controller.defid.ts @@ -0,0 +1,221 @@ +import BigNumber from 'bignumber.js' +import { DLoanController, DefidBin, DefidRpc } from '../../e2e.defid.module' +import { WhaleApiException } from '@defichain/whale-api-client/dist/errors' + +let testing: DefidRpc +let app: DefidBin +let controller: DLoanController + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.loanController + testing = app.rpc + await app.waitForBlockHeight(101) + + await testing.token.create({ symbol: 'AAPL' }) + await testing.generate(1) + + await testing.token.create({ symbol: 'TSLA' }) + await testing.generate(1) + + await testing.token.create({ symbol: 'MSFT' }) + await testing.generate(1) + + await testing.token.create({ symbol: 'FB' }) + await testing.generate(1) + + const oracleId = await testing.client.oracle.appointOracle(await app.getNewAddress(), + [ + { token: 'AAPL', currency: 'USD' }, + { token: 'TSLA', currency: 'USD' }, + { token: 'MSFT', currency: 'USD' }, + { token: 'FB', currency: 'USD' } + ], { weightage: 1 }) + await testing.generate(1) + + await testing.client.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '1.5@AAPL', + currency: 'USD' + }] + }) + await testing.client.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '2.5@TSLA', + currency: 'USD' + }] + }) + await testing.client.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '3.5@MSFT', + currency: 'USD' + }] + }) + await testing.client.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '4.5@FB', + currency: 'USD' + }] + }) + await testing.generate(1) + + await testing.client.loan.setCollateralToken({ + token: 'AAPL', + factor: new BigNumber(0.1), + fixedIntervalPriceId: 'AAPL/USD' + }) + await testing.generate(1) + + await testing.client.loan.setCollateralToken({ + token: 'TSLA', + factor: new BigNumber(0.2), + fixedIntervalPriceId: 'TSLA/USD' + }) + await testing.generate(1) + + await testing.client.loan.setCollateralToken({ + token: 'MSFT', + factor: new BigNumber(0.3), + fixedIntervalPriceId: 'MSFT/USD' + }) + await testing.generate(1) + + await testing.client.loan.setCollateralToken({ + token: 'FB', + factor: new BigNumber(0.4), + fixedIntervalPriceId: 'FB/USD' + }) + await testing.generate(1) +}) + +afterAll(async () => { + await app.stop() +}) + +describe('list', () => { + it('should listCollateralTokens', async () => { + const result = await controller.listCollateral({ size: 100 }) + expect(result.data.length).toStrictEqual(4) + expect(result.data[0]).toStrictEqual({ + tokenId: expect.any(String), + fixedIntervalPriceId: expect.any(String), + factor: expect.any(String), + activateAfterBlock: 0, + 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, + isLoanToken: false, + limit: '0', + mintable: true, + minted: '0', + name: expect.any(String), + symbol: expect.any(String), + symbolKey: expect.any(String), + tradeable: true + }, + // activePrice: undefined + activePrice: expect.any(Object) + }) + }) + + it('should listCollateralTokens with pagination', async () => { + const first = await controller.listCollateral({ size: 2 }) + + expect(first.data.length).toStrictEqual(2) + expect(first.page?.next?.length).toStrictEqual(64) + + const next = await controller.listCollateral({ + size: 2, + next: first.page?.next + }) + + expect(next.data.length).toStrictEqual(2) + expect(next.page?.next?.length).toStrictEqual(64) + + const last = await controller.listCollateral({ + size: 2, + next: next.page?.next + }) + + expect(last.data.length).toStrictEqual(0) + expect(last.page).toBeUndefined() + }) + + it('should listCollateralTokens with an empty object if size 100 next 300 which is out of range', async () => { + const result = await controller.listCollateral({ size: 100, next: '300' }) + + expect(result.data.length).toStrictEqual(0) + expect(result.page).toBeUndefined() + }) +}) + +describe('get', () => { + it('should get collateral token by symbol', async () => { + const data = await controller.getCollateral('AAPL') + expect(data).toStrictEqual( + { + tokenId: expect.stringMatching(/[0-f]{64}/), + fixedIntervalPriceId: 'AAPL/USD', + factor: '0.1', + activateAfterBlock: 0, + 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, + isLoanToken: false, + limit: '0', + mintable: true, + minted: '0', + name: 'AAPL', + symbol: 'AAPL', + symbolKey: expect.any(String), + tradeable: true + }, + // activePrice: undefined + activePrice: expect.any(Object) + } + ) + }) + + it('should throw error while getting non-existent collateral token id', async () => { + expect.assertions(2) + try { + await controller.getCollateral('999') + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find collateral token', + url: '/v0/regtest/loans/collaterals/999' + }) + } + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/loan.scheme.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/loan.scheme.controller.defid.ts new file mode 100644 index 0000000000..04d3c39615 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/loan.scheme.controller.defid.ts @@ -0,0 +1,141 @@ +import BigNumber from 'bignumber.js' +import { DLoanController, DefidBin, DefidRpc } from '../../e2e.defid.module' +import { WhaleApiException } from '@defichain/whale-api-client/dist/errors' + +let testing: DefidRpc +let app: DefidBin +let controller: DLoanController + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.loanController + testing = app.rpc + await app.waitForBlockHeight(101) + + await testing.client.loan.createLoanScheme({ + minColRatio: 100, + interestRate: new BigNumber(6.5), + id: 'default' + }) + await app.generate(1) + + await testing.client.loan.createLoanScheme({ + minColRatio: 150, + interestRate: new BigNumber(5.5), + id: 'scheme1' + }) + await app.generate(1) + + await testing.client.loan.createLoanScheme({ + minColRatio: 200, + interestRate: new BigNumber(4.5), + id: 'scheme2' + }) + await app.generate(1) + + await testing.client.loan.createLoanScheme({ + minColRatio: 250, + interestRate: new BigNumber(3.5), + id: 'scheme3' + }) + await app.generate(1) +}) + +afterAll(async () => { + await app.stop() +}) + +describe('loan', () => { + it('should listLoanSchemes', async () => { + const result = await controller.listScheme({ size: 100 }) + expect(result.data.length).toStrictEqual(4) + expect(result.data).toStrictEqual([ + { + id: 'default', + minColRatio: '100', + interestRate: '6.5' + }, + { + id: 'scheme1', + minColRatio: '150', + interestRate: '5.5' + }, + { + id: 'scheme2', + minColRatio: '200', + interestRate: '4.5' + }, + { + id: 'scheme3', + minColRatio: '250', + interestRate: '3.5' + } + ]) + }) + + it('should listSchemes with pagination', async () => { + const first = await controller.listScheme({ size: 2 }) + + expect(first.data.length).toStrictEqual(2) + expect(first.page?.next).toStrictEqual('scheme1') + + expect(first.data[0].id).toStrictEqual('default') + expect(first.data[1].id).toStrictEqual('scheme1') + + const next = await controller.listScheme({ + size: 2, + next: first.page?.next + }) + + expect(next.data.length).toStrictEqual(2) + expect(next.page?.next).toStrictEqual('scheme3') + + expect(next.data[0].id).toStrictEqual('scheme2') + expect(next.data[1].id).toStrictEqual('scheme3') + + const last = await controller.listScheme({ + size: 2, + next: next.page?.next + }) + + expect(last.data.length).toStrictEqual(0) + expect(last.page).toBeUndefined() + }) + + it('should listSchemes with an empty object if size 100 next 300 which is out of range', async () => { + const result = await controller.listScheme({ size: 100, next: '300' }) + + expect(result.data.length).toStrictEqual(0) + expect(result.page).toBeUndefined() + }) +}) + +describe('get', () => { + it('should get scheme by symbol', async () => { + const data = await controller.getScheme('default') + expect(data).toStrictEqual( + { + id: 'default', + minColRatio: '100', + interestRate: '6.5' + } + ) + }) + + it('should throw error while getting non-existent scheme', async () => { + expect.assertions(2) + try { + await controller.getScheme('999') + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find scheme', + url: '/v0/regtest/loans/schemes/999' + }) + } + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/loan.token.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/loan.token.controller.defid.ts new file mode 100644 index 0000000000..a2beff6c88 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/loan.token.controller.defid.ts @@ -0,0 +1,202 @@ +import BigNumber from 'bignumber.js' +import { DLoanController, DefidBin, DefidRpc } from '../../e2e.defid.module' +import { WhaleApiException } from '@defichain/whale-api-client/dist/errors' + +let testing: DefidRpc +let app: DefidBin +let controller: DLoanController + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.loanController + testing = app.rpc + await app.waitForBlockHeight(101) + + const oracleId = await app.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.client.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { prices: [{ tokenAmount: '1.5@AAPL', currency: 'USD' }] }) + await testing.client.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { prices: [{ tokenAmount: '2.5@TSLA', currency: 'USD' }] }) + await testing.client.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { prices: [{ tokenAmount: '3.5@MSFT', currency: 'USD' }] }) + await testing.client.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { prices: [{ tokenAmount: '4.5@FB', currency: 'USD' }] }) + await testing.generate(1) + + await app.call('setloantoken', [{ + symbol: 'AAPL', + fixedIntervalPriceId: 'AAPL/USD', + mintable: false, + interest: new BigNumber(0.01) + }]) + await testing.generate(1) + + await app.call('setloantoken', [{ + symbol: 'TSLA', + fixedIntervalPriceId: 'TSLA/USD', + mintable: false, + interest: new BigNumber(0.02) + }]) + await testing.generate(1) + + await app.call('setloantoken', [{ + symbol: 'MSFT', + fixedIntervalPriceId: 'MSFT/USD', + mintable: false, + interest: new BigNumber(0.03) + }]) + await testing.generate(1) + + await app.call('setloantoken', [{ + symbol: 'FB', + fixedIntervalPriceId: 'FB/USD', + mintable: false, + interest: new BigNumber(0.04) + }]) + await testing.generate(1) +}) + +afterAll(async () => { + await app.stop() +}) + +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, + isLoanToken: true, + limit: '0', + mintable: false, + minted: '0', + name: '', + symbol: expect.any(String), + symbolKey: expect.any(String), + tradeable: true + }, + // NOTE(canonbrother): waitForIndexHeight is needed in ocean-js + activePrice: { + active: null, + next: null, + isLive: false, + block: expect.any(Object) + } + }) + + 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, + isLoanToken: true, + limit: '0', + mintable: false, + minted: '0', + name: '', + symbol: 'AAPL', + symbolKey: 'AAPL', + tradeable: true + }, + // NOTE(canonbrother): waitForIndexHeight is needed in ocean-js + activePrice: { + active: null, + next: null, + isLive: false, + block: expect.any(Object) + } + }) + }) + + it('should throw error while getting non-existent loan token id', async () => { + expect.assertions(2) + try { + await controller.getLoanToken('999') + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find loan token', + url: '/v0/regtest/loans/tokens/999' + }) + } + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/loan.vault.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/loan.vault.controller.defid.ts new file mode 100644 index 0000000000..e5a1e348d1 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/loan.vault.controller.defid.ts @@ -0,0 +1,147 @@ +import BigNumber from 'bignumber.js' +import { DLoanController, DefidBin, DefidRpc } from '../../e2e.defid.module' +import { WhaleApiException } from '@defichain/whale-api-client/dist/errors' + +let testing: DefidRpc +let app: DefidBin +let controller: DLoanController + +let address1: string +let vaultId1: string + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.loanController + testing = app.rpc + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) + + // loan schemes + await testing.client.loan.createLoanScheme({ + minColRatio: 150, + interestRate: new BigNumber(1.5), + id: 'default' + }) + await testing.generate(1) + + await testing.client.loan.createLoanScheme({ + minColRatio: 100, + interestRate: new BigNumber(2.5), + id: 'scheme' + }) + await testing.generate(1) + + // Create vaults + address1 = await testing.generateAddress() + vaultId1 = await testing.client.loan.createVault({ + ownerAddress: address1, + loanSchemeId: 'default' + }) + await testing.generate(1) + + await testing.client.loan.createVault({ + ownerAddress: await testing.generateAddress(), + loanSchemeId: 'default' + }) + await testing.generate(1) + + await testing.client.loan.createVault({ + ownerAddress: await testing.generateAddress(), + loanSchemeId: 'default' + }) + await testing.generate(1) + + await testing.client.loan.createVault({ + ownerAddress: await testing.generateAddress(), + loanSchemeId: 'default' + }) + await testing.generate(1) +}) + +afterAll(async () => { + await app.stop() +}) + +describe('loan', () => { + it('should listVaults with size only', async () => { + const result = await controller.listVault({ + size: 100 + }) + expect(result.data.length).toStrictEqual(4) + result.data.forEach(e => + expect(e).toStrictEqual({ + vaultId: expect.any(String), + loanScheme: { + id: 'default', + interestRate: '1.5', + minColRatio: '150' + }, + ownerAddress: expect.any(String), + state: expect.any(String), + informativeRatio: '-1', + collateralRatio: '-1', + collateralValue: '0', + loanValue: '0', + interestValue: '0', + collateralAmounts: [], + loanAmounts: [], + interestAmounts: [] + }) + ) + }) +}) + +describe('get', () => { + it('should get vault by vaultId', async () => { + const data = await controller.getVault(vaultId1) + expect(data).toStrictEqual({ + vaultId: vaultId1, + loanScheme: { + id: 'default', + interestRate: '1.5', + minColRatio: '150' + }, + ownerAddress: address1, + // state: LoanVaultState.ACTIVE, + state: 'active', + informativeRatio: '-1', + collateralRatio: '-1', + collateralValue: '0', + loanValue: '0', + interestValue: '0', + collateralAmounts: [], + loanAmounts: [], + interestAmounts: [] + }) + }) + + it('should throw error while getting non-existent vault', async () => { + expect.assertions(4) + try { + await controller.getVault('0530ab29a9f09416a014a4219f186f1d5d530e9a270a9f941275b3972b43ebb7') + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find vault', + url: '/v0/regtest/loans/vaults/0530ab29a9f09416a014a4219f186f1d5d530e9a270a9f941275b3972b43ebb7' + }) + } + + try { + await controller.getVault('999') + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find vault', + url: '/v0/regtest/loans/vaults/999' + }) + } + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/masternode.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/masternode.controller.defid.ts new file mode 100644 index 0000000000..0e9a5c838f --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/masternode.controller.defid.ts @@ -0,0 +1,169 @@ +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' +import { MasternodeState } from '@defichain/whale-api-client/dist/api/masternodes' +import { MasternodeTimeLock } from '@defichain/jellyfish-api-core/dist/category/masternode' +import { DefidBin, DefidRpc, DMasternodeController } from '../../e2e.defid.module' + +let container: DefidRpc +let app: DefidBin +let controller: DMasternodeController +let client: JsonRpcClient + +describe('list', () => { + beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.masternodeController + container = app.rpc + await app.waitForBlockHeight(101) + client = new JsonRpcClient(app.rpcUrl) + + await container.generate(1) + const height = await client.blockchain.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + }) + + afterAll(async () => { + await app.stop() + }) + + it('should list masternodes', async () => { + const result = await controller.list({ size: 4 }) + expect(result.data.length).toStrictEqual(4) + expect(Object.keys(result.data[0]).length).toStrictEqual(9) + }) + + it('should list masternodes with pagination', async () => { + const first = await controller.list({ size: 4 }) + expect(first.data.length).toStrictEqual(4) + + const next = await controller.list({ + size: 4, + next: first.page?.next + }) + expect(next.data.length).toStrictEqual(4) + expect(next.page?.next).toStrictEqual(`00000000${next.data[3].id}`) + + const last = await controller.list({ + size: 4, + next: next.page?.next + }) + expect(last.data.length).toStrictEqual(0) + expect(last.page).toStrictEqual(undefined) + }) +}) + +describe('get', () => { + beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.masternodeController + container = app.rpc + await app.waitForBlockHeight(101) + client = new JsonRpcClient(app.rpcUrl) + + await container.generate(1) + const height = await client.blockchain.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + }) + + afterAll(async () => { + await app.stop() + }) + + it('should get a masternode with id', async () => { + // get a masternode from list + const masternode = (await controller.list({ size: 1 })).data[0] + + const result = await controller.get(masternode.id) + expect(Object.keys(result).length).toStrictEqual(9) + expect(result).toStrictEqual({ ...masternode, mintedBlocks: expect.any(Number) }) + }) + + it('should fail due to non-existent masternode', async () => { + await expect(controller.get('8d4d987dee688e400a0cdc899386f243250d3656d802231755ab4d28178c9816')).rejects.toThrowError('404 - NotFound (/v0/regtest/masternodes/8d4d987dee688e400a0cdc899386f243250d3656d802231755ab4d28178c9816): Unable to find masternode') + }) +}) + +describe('resign', () => { + beforeAll(async () => { + app = new DefidBin() + await app.start(['-eunospayaheight=200']) + controller = app.ocean.masternodeController + container = app.rpc + await app.waitForBlockHeight(101) + client = new JsonRpcClient(app.rpcUrl) + + await container.generate(1) + const height = await client.blockchain.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + }) + + afterAll(async () => { + await app.stop() + }) + + it('should get masternode with pre-resigned state', async () => { + await container.generate(1) + + const ownerAddress = await client.wallet.getNewAddress() + const masternodeId = await client.masternode.createMasternode(ownerAddress) + await container.generate(1) + + const height = await client.blockchain.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + + const resignTx = await client.masternode.resignMasternode(masternodeId) + + await container.generate(1) + const resignHeight = await client.blockchain.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(resignHeight) + + const result = await controller.get(masternodeId) + expect(result.state).toStrictEqual(MasternodeState.PRE_RESIGNED) + expect(result?.resign?.tx).toStrictEqual(resignTx) + expect(result?.resign?.height).toStrictEqual(resignHeight) + }) +}) + +describe('timelock', () => { + beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.masternodeController + container = app.rpc + await app.waitForBlockHeight(101) + client = new JsonRpcClient(app.rpcUrl) + + await container.generate(1) + const height = await client.blockchain.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + }) + + afterAll(async () => { + await app.stop() + }) + + it('should get masternode with timelock', async () => { + await container.generate(1) + + const ownerAddress = await client.wallet.getNewAddress() + const masternodeId = await client.masternode.createMasternode(ownerAddress, undefined, { + timelock: MasternodeTimeLock.TEN_YEAR, + utxos: [] + }) + + await container.generate(1) + const height = await client.blockchain.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + + const result = await controller.get(masternodeId) + expect(result.timelock).toStrictEqual(520) + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/oracles.defid.ts b/apps/whale-api/src/module.api/__defid__/oracles.defid.ts new file mode 100644 index 0000000000..2e4830de29 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/oracles.defid.ts @@ -0,0 +1,256 @@ +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' +import { DOracleController, DefidBin, DefidRpc } from '../../e2e.defid.module' + +let container: DefidRpc +let app: DefidBin +let controller: DOracleController +let client: JsonRpcClient + +interface OracleSetup { + id: string + address: string + weightage: number + feed: Array<{ token: string, currency: string }> + prices: Array> +} + +const oracles: Record = { + a: { + id: undefined as any, + address: undefined as any, + weightage: 1, + feed: [ + { token: 'TA', currency: 'USD' }, + { token: 'TB', currency: 'USD' }, + { token: 'TC', currency: 'USD' }, + { token: 'TD', currency: 'USD' } + ], + prices: [ + [ + { tokenAmount: '1.1@TA', currency: 'USD' }, + { tokenAmount: '2.1@TB', currency: 'USD' }, + { tokenAmount: '3.1@TC', currency: 'USD' }, + { tokenAmount: '4.1@TD', currency: 'USD' } + ], + [ + { tokenAmount: '1@TA', currency: 'USD' }, + { tokenAmount: '2@TB', currency: 'USD' }, + { tokenAmount: '3@TC', currency: 'USD' } + ], + [ + { tokenAmount: '0.9@TA', currency: 'USD' }, + { tokenAmount: '1.9@TB', currency: 'USD' } + ] + ] + }, + b: { + id: undefined as any, + address: undefined as any, + weightage: 2, + feed: [ + { token: 'TA', currency: 'USD' }, + { token: 'TB', currency: 'USD' }, + { token: 'TD', currency: 'USD' } + ], + prices: [ + [ + { tokenAmount: '1.5@TA', currency: 'USD' }, + { tokenAmount: '2.5@TB', currency: 'USD' }, + { tokenAmount: '4.5@TD', currency: 'USD' } + ], + [ + { tokenAmount: '1.5@TA', currency: 'USD' }, + { tokenAmount: '2.5@TB', currency: 'USD' }, + { tokenAmount: '4.5@TD', currency: 'USD' } + ] + ] + }, + c: { + id: undefined as any, + address: undefined as any, + weightage: 0, + feed: [ + { token: 'TA', currency: 'USD' }, + { token: 'TB', currency: 'USD' }, + { token: 'TC', currency: 'USD' } + ], + prices: [ + [ + { tokenAmount: '1.25@TA', currency: 'USD' }, + { tokenAmount: '2.25@TB', currency: 'USD' }, + { tokenAmount: '4.25@TC', currency: 'USD' } + ] + ] + } +} + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.oracleController + container = app.rpc + await app.waitForBlockHeight(101) + client = new JsonRpcClient(app.rpcUrl) + + for (const setup of Object.values(oracles)) { + setup.address = await app.getNewAddress() + setup.id = await client.oracle.appointOracle(setup.address, setup.feed, { + weightage: setup.weightage + }) + await container.generate(1) + } + + for (const setup of Object.values(oracles)) { + for (const price of setup.prices) { + const timestamp = Math.floor(new Date().getTime() / 1000) + await client.oracle.setOracleData(setup.id, timestamp, { + prices: price + }) + await container.generate(1) + } + } + + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) +}) + +afterAll(async () => { + await app.stop() +}) + +it('should list', async () => { + const { data: oracles } = await controller.list() + + expect(oracles.length).toStrictEqual(3) + expect(oracles[0]).toStrictEqual({ + id: expect.stringMatching(/[0-f]{64}/), + ownerAddress: expect.any(String), + weightage: expect.any(Number), + priceFeeds: expect.any(Array), + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + medianTime: expect.any(Number), + time: expect.any(Number) + } + }) +}) + +it('should get oracle a TA-USD feed', async () => { + const { data: feed } = await controller.getPriceFeed(oracles.a.id, 'TA-USD') + + expect(feed.length).toStrictEqual(3) + expect(feed[0]).toStrictEqual({ + id: expect.any(String), + key: expect.any(String), + sort: expect.any(String), + amount: expect.any(String), + currency: 'USD', + token: 'TA', + time: expect.any(Number), + oracleId: oracles.a.id, + txid: expect.stringMatching(/[0-f]{64}/), + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + medianTime: expect.any(Number), + time: expect.any(Number) + } + }) +}) + +it('should get oracle a TB-USD feed', async () => { + const { data: feed } = await controller.getPriceFeed(oracles.a.id, 'TB-USD') + + expect(feed.length).toStrictEqual(3) + expect(feed[0]).toStrictEqual({ + id: expect.any(String), + key: expect.any(String), + sort: expect.any(String), + amount: expect.any(String), + currency: 'USD', + token: 'TB', + time: expect.any(Number), + oracleId: oracles.a.id, + txid: expect.stringMatching(/[0-f]{64}/), + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + medianTime: expect.any(Number), + time: expect.any(Number) + } + }) +}) + +it('should get oracle b TB-USD feed', async () => { + const { data: feed } = await controller.getPriceFeed(oracles.b.id, 'TB-USD') + + expect(feed.length).toStrictEqual(2) + expect(feed[0]).toStrictEqual({ + id: expect.any(String), + key: expect.any(String), + sort: expect.any(String), + amount: '2.5', + currency: 'USD', + token: 'TB', + time: expect.any(Number), + oracleId: oracles.b.id, + txid: expect.stringMatching(/[0-f]{64}/), + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + medianTime: expect.any(Number), + time: expect.any(Number) + } + }) +}) + +it('should get oracles by owner address', async () => { + const { data: oracles } = await controller.list() + + for (const oracle of oracles) { + const toCompare = await controller.getOracleByAddress(oracle.ownerAddress) + expect(toCompare).toStrictEqual(oracle) + } +}) + +it('test UpdateOracle and RemoveOracle', async () => { + const { data: oraclesBefore } = await controller.list() + + const before = oraclesBefore[0] + const oracleId = before.id + const address = before.ownerAddress + + { + await client.oracle.updateOracle(oracleId, address, { + priceFeeds: [ + { token: 'TD', currency: 'USD' } + ], + weightage: 3 + }) + await container.generate(1) + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + } + + const { data: oracles } = await controller.list() + const after = oracles.find((oracle) => oracle.id === oracleId) + expect(after?.id).toStrictEqual(before.id) + expect(after?.priceFeeds).toStrictEqual([{ token: 'TD', currency: 'USD' }]) + expect(after?.weightage).toStrictEqual(3) + + { + await client.oracle.removeOracle(oracleId) + await container.generate(1) + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + } + + const { data: oraclesFinal } = await controller.list() + expect(oraclesFinal.length).toStrictEqual(oraclesBefore.length - 1) + const removed = oraclesFinal.find((oracle) => oracle.id === oracleId) + expect(removed).toBeUndefined() +}) diff --git a/apps/whale-api/src/module.api/__defid__/poolpair.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/poolpair.controller.defid.ts new file mode 100644 index 0000000000..2f1556d439 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/poolpair.controller.defid.ts @@ -0,0 +1,1241 @@ + +import { BigNumber } from 'bignumber.js' +import { DPoolPairController, DefidBin, DefidRpc } from '../../e2e.defid.module' +import { WhaleApiException } from '@defichain/whale-api-client/dist/errors' + +let container: DefidRpc +let app: DefidBin +let controller: DPoolPairController + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.poolPairController + container = app.rpc + await app.waitForBlockHeight(101) + await setup() + + // const cache = app.get(CACHE_MANAGER) + // const defiCache = app.get(DeFiDCache) + + // const tokenResult = await app.call('listtokens') + // // precache + // for (const k in tokenResult) { + // await defiCache.getTokenInfo(k) as TokenInfo + // } + + // // ensure precache is working + // const tkey = `${CachePrefix.TOKEN_INFO} 31` + // const token = await cache.get(tkey) + // expect(token?.symbolKey).toStrictEqual('USDT-DFI') + + // await app.waitForPath(controller) +}) + +afterAll(async () => { + await app.stop() +}) + +async function setup (): Promise { + const tokens = [ + 'A', 'B', 'C', 'D', 'E', 'F', + + // For testing swap paths + 'G', // bridged via A to the rest + 'H', // isolated - no associated poolpair + 'I', 'J', 'K', 'L', 'M', 'N' // isolated from the rest - only swappable with one another + ] + + for (const token of tokens) { + await app.waitForWalletBalanceGTE(110) + await app.createToken(token) + await app.mintTokens(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 app.waitForWalletBalanceGTE(110) + await app.call('createtoken', [{ + symbol: 'O', + name: 'O', + isDAT: false, + mintable: true, + tradeable: true, + collateralAddress: await app.getNewAddress() + }]) + await container.generate(1) + + await app.createPoolPair('A', 'DFI') + await app.createPoolPair('B', 'DFI') + await app.createPoolPair('C', 'DFI') + await app.createPoolPair('D', 'DFI') + await app.createPoolPair('E', 'DFI') + await app.createPoolPair('F', 'DFI') + + await app.createPoolPair('G', 'A') + await app.createPoolPair('I', 'J') + await app.createPoolPair('J', 'K', { commission: 0.25 }) + await app.createPoolPair('J', 'L', { commission: 0.1 }) + await app.createPoolPair('L', 'K') + await app.createPoolPair('L', 'M', { commission: 0.50 }) + await app.createPoolPair('M', 'N') + + await app.addPoolLiquidity({ + tokenA: 'A', + amountA: 100, + tokenB: 'DFI', + amountB: 200, + shareAddress: await app.getNewAddress() + }) + await app.addPoolLiquidity({ + tokenA: 'B', + amountA: 50, + tokenB: 'DFI', + amountB: 300, + shareAddress: await app.getNewAddress() + }) + await app.addPoolLiquidity({ + tokenA: 'C', + amountA: 90, + tokenB: 'DFI', + amountB: 360, + shareAddress: await app.getNewAddress() + }) + + // 1 G = 5 A = 10 DFI + await app.addPoolLiquidity({ + tokenA: 'G', + amountA: 10, + tokenB: 'A', + amountB: 50, + shareAddress: await app.getNewAddress() + }) + + // 1 J = 7 K + await app.addPoolLiquidity({ + tokenA: 'J', + amountA: 10, + tokenB: 'K', + amountB: 70, + shareAddress: await app.getNewAddress() + }) + + // 1 J = 2 L = 8 K + await app.addPoolLiquidity({ + tokenA: 'J', + amountA: 4, + tokenB: 'L', + amountB: 8, + shareAddress: await app.getNewAddress() + }) + await app.addPoolLiquidity({ + tokenA: 'L', + amountA: 5, + tokenB: 'K', + amountB: 20, + shareAddress: await app.getNewAddress() + }) + + await app.addPoolLiquidity({ + tokenA: 'L', + amountA: 6, + tokenB: 'M', + amountB: 48, + shareAddress: await app.getNewAddress() + }) + await app.addPoolLiquidity({ + tokenA: 'M', + amountA: 7, + tokenB: 'N', + amountB: 70, + shareAddress: await app.getNewAddress() + }) + + // BURN should not be listed as swappable + await app.createToken('BURN') + await app.createPoolPair('BURN', 'DFI', { status: false }) + await app.mintTokens('BURN', { mintAmount: 1 }) + await app.addPoolLiquidity({ + tokenA: 'BURN', + amountA: 1, + tokenB: 'DFI', + amountB: 1, + shareAddress: await app.getNewAddress() + }) + + // dexUsdtDfi setup + await app.createToken('USDT') + await app.createPoolPair('USDT', 'DFI') + await app.mintTokens('USDT') + await app.addPoolLiquidity({ + tokenA: 'USDT', + amountA: 1000, + tokenB: 'DFI', + amountB: 431.51288, + shareAddress: await app.getNewAddress() + }) + + await app.call('setgov', [{ LP_SPLITS: { 16: 1.0 } }]) + await app.generate(1) + + // dex fee set up + await app.call('setgov', [{ + ATTRIBUTES: { + 'v0/poolpairs/16/token_a_fee_pct': '0.05', + 'v0/poolpairs/16/token_b_fee_pct': '0.08', + 'v0/poolpairs/26/token_a_fee_pct': '0.07', + 'v0/poolpairs/26/token_b_fee_pct': '0.09' + } + }]) + await app.generate(1) +} + +describe('list', () => { + it('should list', async () => { + const response = await controller.list({ + size: 30 + }) + + expect(response.data.length).toStrictEqual(14) + expect(response.page).toBeUndefined() + + expect(response.data[1]).toStrictEqual({ + id: '16', + symbol: 'B-DFI', + displaySymbol: 'dB-DFI', + name: 'B-Default Defi token', + status: true, + tokenA: { + id: '2', + name: 'B', + symbol: 'B', + reserve: '50', + blockCommission: '0', + displaySymbol: 'dB', + fee: { + pct: '0.05', + inPct: '0.05', + outPct: '0.05' + } + }, + tokenB: { + id: '0', + name: 'Default Defi token', + symbol: 'DFI', + reserve: '300', + blockCommission: '0', + displaySymbol: 'DFI', + fee: { + pct: '0.08', + inPct: '0.08', + outPct: '0.08' + } + }, + apr: { + reward: 2229.42, + total: 2229.42, + commission: 0 + }, + commission: '0', + totalLiquidity: { + token: '122.47448713', + // usd: '1390.4567576291117892' + usd: '1390.4567576291117892008229279' + }, + tradeEnabled: true, + ownerAddress: expect.any(String), + priceRatio: { + ab: '0.16666666', + ba: '6' + }, + rewardPct: '1', + rewardLoanPct: '0', + // customRewards: undefined, + creation: { + tx: expect.any(String), + height: expect.any(Number) + }, + volume: { + d30: 0, + h24: 0 + } + }) + }) + + it('should list with pagination', async () => { + const first = await controller.list({ + size: 2 + }) + expect(first.data.length).toStrictEqual(2) + expect(first.page?.next).toStrictEqual('16') + expect(first.data[0].symbol).toStrictEqual('A-DFI') + expect(first.data[1].symbol).toStrictEqual('B-DFI') + + const next = await controller.list({ + size: 14, + next: first.page?.next + }) + + expect(next.data.length).toStrictEqual(12) + expect(next.page?.next).toBeUndefined() + expect(next.data[0].symbol).toStrictEqual('C-DFI') + expect(next.data[1].symbol).toStrictEqual('D-DFI') + expect(next.data[2].symbol).toStrictEqual('E-DFI') + expect(next.data[3].symbol).toStrictEqual('F-DFI') + }) + + it('should list with undefined next pagination', async () => { + const first = await controller.list({ + size: 2, + next: undefined + }) + + expect(first.data.length).toStrictEqual(2) + expect(first.page?.next).toStrictEqual('16') + }) +}) + +describe('get', () => { + it('should get', async () => { + const response = await controller.get('15') + + expect(response).toStrictEqual({ + id: '15', + symbol: 'A-DFI', + displaySymbol: 'dA-DFI', + name: 'A-Default Defi token', + status: true, + tokenA: { + id: expect.any(String), + name: 'A', + symbol: 'A', + reserve: '100', + blockCommission: '0', + displaySymbol: 'dA' + // fee: undefined + }, + tokenB: { + id: '0', + name: 'Default Defi token', + symbol: 'DFI', + reserve: '200', + blockCommission: '0', + displaySymbol: 'DFI' + // fee: undefined + }, + apr: { + reward: 0, + total: 0, + commission: 0 + }, + commission: '0', + totalLiquidity: { + token: '141.42135623', + // usd: '926.9711717527411928' + usd: '926.9711717527411928005486186' + }, + tradeEnabled: true, + ownerAddress: expect.any(String), + priceRatio: { + ab: '0.5', + ba: '2' + }, + rewardPct: '0', + rewardLoanPct: '0', + // 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: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find poolpair', + url: '/v0/regtest/poolpairs/999' + }) + } + }) +}) + +describe('get best path', () => { + it('should be bidirectional swap path - listPaths(a, b) === listPaths(b, a)', async () => { + const paths1 = await controller.getBestPath('1', '0') // A to DFI + expect(paths1).toStrictEqual({ + fromToken: { + id: '1', + name: 'A', + symbol: 'A', + displaySymbol: 'dA' + }, + toToken: { + id: '0', + name: 'Default Defi token', + symbol: 'DFI', + displaySymbol: 'DFI' + }, + bestPath: [ + { + symbol: 'A-DFI', + poolPairId: '15', + priceRatio: { ab: '0.50000000', ba: '2.00000000' }, + tokenA: { id: '1', name: 'A', symbol: 'A', displaySymbol: 'dA' }, + tokenB: { id: '0', name: 'Default Defi token', symbol: 'DFI', displaySymbol: 'DFI' }, + commissionFeeInPct: '0' + } + ], + estimatedReturn: '2.00000000', + estimatedReturnLessDexFees: '2.00000000' + }) + }) + + it('should get best swap path - 2 legs', async () => { + const response = await controller.getBestPath('1', '3') // A to C + expect(response).toStrictEqual({ + fromToken: { + id: '1', + name: 'A', + symbol: 'A', + displaySymbol: 'dA' + }, + toToken: { + id: '3', + name: 'C', + symbol: 'C', + displaySymbol: 'dC' + }, + bestPath: [ + { + symbol: 'A-DFI', + poolPairId: '15', + priceRatio: { ab: '0.50000000', ba: '2.00000000' }, + tokenA: { id: '1', name: 'A', symbol: 'A', displaySymbol: 'dA' }, + tokenB: { id: '0', name: 'Default Defi token', symbol: 'DFI', displaySymbol: 'DFI' }, + commissionFeeInPct: '0' + }, + { + symbol: 'C-DFI', + poolPairId: '17', + priceRatio: { ab: '0.25000000', ba: '4.00000000' }, + tokenA: { id: '3', name: 'C', symbol: 'C', displaySymbol: 'dC' }, + tokenB: { id: '0', name: 'Default Defi token', symbol: 'DFI', displaySymbol: 'DFI' }, + commissionFeeInPct: '0' + } + ], + estimatedReturn: '0.50000000', + estimatedReturnLessDexFees: '0.50000000' + }) + }) + + it('should get correct swap path - 3 legs', async () => { + const response = await controller.getBestPath('7', '3') // G to C + expect(response).toStrictEqual({ + fromToken: { + id: '7', + name: 'G', + symbol: 'G', + displaySymbol: 'dG' + }, + toToken: { + id: '3', + name: 'C', + symbol: 'C', + displaySymbol: 'dC' + }, + bestPath: [ + { + symbol: 'G-A', + poolPairId: '21', + priceRatio: { ab: '0.20000000', ba: '5.00000000' }, + tokenA: { id: '7', name: 'G', symbol: 'G', displaySymbol: 'dG' }, + tokenB: { id: '1', name: 'A', symbol: 'A', displaySymbol: 'dA' }, + commissionFeeInPct: '0' + }, + { + symbol: 'A-DFI', + poolPairId: '15', + priceRatio: { ab: '0.50000000', ba: '2.00000000' }, + tokenA: { id: '1', name: 'A', symbol: 'A', displaySymbol: 'dA' }, + tokenB: { id: '0', name: 'Default Defi token', symbol: 'DFI', displaySymbol: 'DFI' }, + commissionFeeInPct: '0' + }, + { + symbol: 'C-DFI', + poolPairId: '17', + priceRatio: { ab: '0.25000000', ba: '4.00000000' }, + tokenA: { id: '3', name: 'C', symbol: 'C', displaySymbol: 'dC' }, + tokenB: { id: '0', name: 'Default Defi token', symbol: 'DFI', displaySymbol: 'DFI' }, + commissionFeeInPct: '0' + } + ], + estimatedReturn: '2.50000000', + estimatedReturnLessDexFees: '2.50000000' + }) + }) + + it('should ignore correct swap path > 3 legs', async () => { + const response = await controller.getBestPath('9', '14') // I to N + /* paths available + (4 legs) Swap I through -> I-J -> J-L -> L-M -> M-N to get N + (5 legs) Swap I through -> I-J -> J-K -> K-L -> L-M -> M-N to get N + */ + expect(response).toStrictEqual({ + fromToken: { + id: '9', + name: 'I', + symbol: 'I', + displaySymbol: 'dI' + }, + toToken: { + id: '14', + name: 'N', + symbol: 'N', + displaySymbol: 'dN' + }, + bestPath: [], + estimatedReturn: '0', + estimatedReturnLessDexFees: '0' + }) + }) + + 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') + expect(response).toStrictEqual({ + fromToken: { + id: '10', + name: 'J', + symbol: 'J', + displaySymbol: 'dJ' + }, + toToken: { + id: '11', + name: 'K', + symbol: 'K', + displaySymbol: 'dK' + }, + bestPath: [ + { + symbol: 'J-K', + poolPairId: '23', + priceRatio: { ab: '0.14285714', ba: '7.00000000' }, + tokenA: { id: '10', name: 'J', symbol: 'J', displaySymbol: 'dJ' }, + tokenB: { id: '11', name: 'K', symbol: 'K', displaySymbol: 'dK' }, + commissionFeeInPct: '0.25000000' + } + ], + estimatedReturn: '7.00000000', + estimatedReturnLessDexFees: '5.25000000' + }) + }) + + it('should deduct commission fee - 1 leg', async () => { + const response = await controller.getBestPath('10', '12') + expect(response).toStrictEqual({ + fromToken: { + id: '10', + name: 'J', + symbol: 'J', + displaySymbol: 'dJ' + }, + toToken: { + id: '12', + name: 'L', + symbol: 'L', + displaySymbol: 'dL' + }, + bestPath: [ + { + symbol: 'J-L', + poolPairId: '24', + priceRatio: { ab: '0.50000000', ba: '2.00000000' }, + tokenA: { id: '10', name: 'J', symbol: 'J', displaySymbol: 'dJ' }, + tokenB: { id: '12', name: 'L', symbol: 'L', displaySymbol: 'dL' }, + commissionFeeInPct: '0.10000000' + } + ], + estimatedReturn: '2.00000000', + estimatedReturnLessDexFees: '1.80000000' + }) + }) + + it('should deduct commission and dex fees - 2 legs', async () => { + const response = await controller.getBestPath('10', '13') + expect(response).toStrictEqual({ + fromToken: { + id: '10', + name: 'J', + symbol: 'J', + displaySymbol: 'dJ' + }, + toToken: { + id: '13', + name: 'M', + symbol: 'M', + displaySymbol: 'dM' + }, + bestPath: [ + { + symbol: 'J-L', + poolPairId: '24', + priceRatio: { ab: '0.50000000', ba: '2.00000000' }, + tokenA: { id: '10', name: 'J', symbol: 'J', displaySymbol: 'dJ' }, + tokenB: { id: '12', name: 'L', symbol: 'L', displaySymbol: 'dL' }, + commissionFeeInPct: '0.10000000' + }, + { + symbol: 'L-M', + poolPairId: '26', + priceRatio: { ab: '0.12500000', ba: '8.00000000' }, + tokenA: { id: '12', name: 'L', symbol: 'L', displaySymbol: 'dL' }, + tokenB: { id: '13', name: 'M', symbol: 'M', displaySymbol: 'dM' }, + commissionFeeInPct: '0.50000000', + estimatedDexFeesInPct: { + ab: '0.09000000', + ba: '0.07000000' + } + } + ], + estimatedReturn: '16.00000000', + /* + Swap through first leg -- J -> L (No DEX fees) + Deduct commission fee: 1 * 0.1 + = 1 - (1 * 0.1) + Convert fromToken -> toToken by price ratio + = 0.9 * 2 + Swap through second leg -- L -> M (With DEX fees) + Deduct commission fee: 1.8 * 0.5 + = 1.8 - 0.9 + Deduct dex fees fromToken: estLessDexFees * 0.07 + = 0.9 - 0.063 + Convert fromToken -> toToken by price ratio + = 0.837 * 8 + Deduct dex fees toToken: estLessDexFees * 0.09 + = 6.696 - 0.60264 + + Estimated return less commission and dex fees + = 6.09336 + */ + estimatedReturnLessDexFees: '6.09336000' + }) + }) + + it('should have no swap path - isolated token H', async () => { + const response = await controller.getBestPath('8', '1') // H to A impossible + expect(response).toStrictEqual({ + fromToken: { + id: '8', + name: 'H', + symbol: 'H', + displaySymbol: 'dH' + }, + toToken: { + id: '1', + name: 'A', + symbol: 'A', + displaySymbol: 'dA' + }, + bestPath: [], + estimatedReturn: '0', + estimatedReturnLessDexFees: '0' + }) + }) + + it('should throw error for invalid tokenId', async () => { + await expect(controller.getBestPath('-1', '1')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/paths/best/from/-1/to/1): Unable to find token -1') + await expect(controller.getBestPath('1', '-1')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/paths/best/from/1/to/-1): Unable to find token -1') + await expect(controller.getBestPath('100', '1')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/paths/best/from/100/to/1): Unable to find token 100') + await expect(controller.getBestPath('1', '100')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/paths/best/from/1/to/100): Unable to find token 100') + await expect(controller.getBestPath('-1', '100')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/paths/best/from/-1/to/100): Unable to find token -1') + // await expect(controller.getBestPath('-1', '1')).rejects.toThrowError('Unable to find token -1') + // await expect(controller.getBestPath('1', '-1')).rejects.toThrowError('Unable to find token -1') + // await expect(controller.getBestPath('100', '1')).rejects.toThrowError('Unable to find token 100') + // await expect(controller.getBestPath('1', '100')).rejects.toThrowError('Unable to find token 100') + // await expect(controller.getBestPath('-1', '100')).rejects.toThrowError('Unable to find token -1') + }) +}) + +describe('get all paths', () => { + it('should be bidirectional swap path - listPaths(a, b) === listPaths(b, a)', async () => { + const paths1 = await controller.listPaths('1', '0') // A to DFI + expect(paths1).toStrictEqual({ + fromToken: { + id: '1', + name: 'A', + symbol: 'A', + displaySymbol: 'dA' + }, + toToken: { + id: '0', + name: 'Default Defi token', + symbol: 'DFI', + displaySymbol: 'DFI' + }, + paths: [ + [ + { + symbol: 'A-DFI', + poolPairId: '15', + tokenA: { id: '1', name: 'A', symbol: 'A', displaySymbol: 'dA' }, + tokenB: { id: '0', name: 'Default Defi token', symbol: 'DFI', displaySymbol: 'DFI' }, + priceRatio: { ab: '0.50000000', ba: '2.00000000' }, + commissionFeeInPct: '0' + } + ] + ] + }) + }) + + it('should get correct swap path - 2 legs', async () => { + const response = await controller.listPaths('1', '3') // A to C + expect(response).toStrictEqual({ + fromToken: { + id: '1', + name: 'A', + symbol: 'A', + displaySymbol: 'dA' + }, + toToken: { + id: '3', + name: 'C', + symbol: 'C', + displaySymbol: 'dC' + }, + paths: [ + [ + { + symbol: 'A-DFI', + poolPairId: '15', + priceRatio: { ab: '0.50000000', ba: '2.00000000' }, + tokenA: { id: '1', name: 'A', symbol: 'A', displaySymbol: 'dA' }, + tokenB: { id: '0', name: 'Default Defi token', symbol: 'DFI', displaySymbol: 'DFI' }, + commissionFeeInPct: '0' + }, + { + symbol: 'C-DFI', + poolPairId: '17', + priceRatio: { ab: '0.25000000', ba: '4.00000000' }, + tokenA: { id: '3', name: 'C', symbol: 'C', displaySymbol: 'dC' }, + tokenB: { id: '0', name: 'Default Defi token', symbol: 'DFI', displaySymbol: 'DFI' }, + commissionFeeInPct: '0' + } + ] + ] + }) + }) + + it('should get correct swap path - 3 legs', async () => { + const response = await controller.listPaths('7', '3') // G to C + expect(response).toStrictEqual({ + fromToken: { + id: '7', + name: 'G', + symbol: 'G', + displaySymbol: 'dG' + }, + toToken: { + id: '3', + name: 'C', + symbol: 'C', + displaySymbol: 'dC' + }, + paths: [ + [ + { + symbol: 'G-A', + poolPairId: '21', + priceRatio: { ab: '0.20000000', ba: '5.00000000' }, + tokenA: { id: '7', name: 'G', symbol: 'G', displaySymbol: 'dG' }, + tokenB: { id: '1', name: 'A', symbol: 'A', displaySymbol: 'dA' }, + commissionFeeInPct: '0' + }, + { + symbol: 'A-DFI', + poolPairId: '15', + priceRatio: { ab: '0.50000000', ba: '2.00000000' }, + tokenA: { id: '1', name: 'A', symbol: 'A', displaySymbol: 'dA' }, + tokenB: { id: '0', name: 'Default Defi token', symbol: 'DFI', displaySymbol: 'DFI' }, + commissionFeeInPct: '0' + }, + { + symbol: 'C-DFI', + poolPairId: '17', + priceRatio: { ab: '0.25000000', ba: '4.00000000' }, + tokenA: { id: '3', name: 'C', symbol: 'C', displaySymbol: 'dC' }, + tokenB: { id: '0', name: 'Default Defi token', symbol: 'DFI', displaySymbol: 'DFI' }, + commissionFeeInPct: '0' + } + ] + ] + }) + }) + + it('should ignore correct swap paths > 3 legs', async () => { + const response = await controller.listPaths('9', '14') // I to N + + /* paths available + (4 legs) Swap I through -> I-J -> J-L -> L-M -> M-N to get N + (5 legs) Swap I through -> I-J -> J-K -> K-L -> L-M -> M-N to get N + */ + expect(response).toStrictEqual({ + fromToken: { + id: '9', + name: 'I', + symbol: 'I', + displaySymbol: 'dI' + }, + toToken: { + id: '14', + name: 'N', + symbol: 'N', + displaySymbol: 'dN' + }, + paths: [] + }) + }) + + it('should get multiple swap paths', async () => { + const response = await controller.listPaths('9', '11') // I to K + expect(response).toStrictEqual({ + fromToken: { + id: '9', + name: 'I', + symbol: 'I', + displaySymbol: 'dI' + }, + toToken: { + id: '11', + name: 'K', + symbol: 'K', + displaySymbol: 'dK' + }, + paths: [ + [ + { + symbol: 'I-J', + poolPairId: '22', + priceRatio: { ab: '0', ba: '0' }, + tokenA: { id: '9', name: 'I', symbol: 'I', displaySymbol: 'dI' }, + tokenB: { id: '10', name: 'J', symbol: 'J', displaySymbol: 'dJ' }, + commissionFeeInPct: '0' + }, + { + symbol: 'J-L', + poolPairId: '24', + priceRatio: { ab: '0.50000000', ba: '2.00000000' }, + tokenA: { id: '10', name: 'J', symbol: 'J', displaySymbol: 'dJ' }, + tokenB: { id: '12', name: 'L', symbol: 'L', displaySymbol: 'dL' }, + commissionFeeInPct: '0.10000000' + }, + { + symbol: 'L-K', + poolPairId: '25', + priceRatio: { ab: '0.25000000', ba: '4.00000000' }, + tokenA: { id: '12', name: 'L', symbol: 'L', displaySymbol: 'dL' }, + tokenB: { id: '11', name: 'K', symbol: 'K', displaySymbol: 'dK' }, + commissionFeeInPct: '0' + } + ], + [ + { + symbol: 'I-J', + poolPairId: '22', + priceRatio: { ab: '0', ba: '0' }, + tokenA: { id: '9', name: 'I', symbol: 'I', displaySymbol: 'dI' }, + tokenB: { id: '10', name: 'J', symbol: 'J', displaySymbol: 'dJ' }, + commissionFeeInPct: '0' + }, + { + symbol: 'J-K', + poolPairId: '23', + priceRatio: { ab: '0.14285714', ba: '7.00000000' }, + tokenA: { id: '10', name: 'J', symbol: 'J', displaySymbol: 'dJ' }, + tokenB: { id: '11', name: 'K', symbol: 'K', displaySymbol: 'dK' }, + commissionFeeInPct: '0.25000000' + } + ] + ] + }) + }) + + it('should handle cyclic swap paths', async () => { + const response = await controller.listPaths('10', '11') // J to K + expect(response).toStrictEqual({ + fromToken: { + id: '10', + name: 'J', + symbol: 'J', + displaySymbol: 'dJ' + }, + toToken: { + id: '11', + name: 'K', + symbol: 'K', + displaySymbol: 'dK' + }, + paths: [ + [ + { + symbol: 'J-L', + poolPairId: '24', + priceRatio: { ab: '0.50000000', ba: '2.00000000' }, + tokenA: { id: '10', name: 'J', symbol: 'J', displaySymbol: 'dJ' }, + tokenB: { id: '12', name: 'L', symbol: 'L', displaySymbol: 'dL' }, + commissionFeeInPct: '0.10000000' + }, + { + symbol: 'L-K', + poolPairId: '25', + priceRatio: { ab: '0.25000000', ba: '4.00000000' }, + tokenA: { id: '12', name: 'L', symbol: 'L', displaySymbol: 'dL' }, + tokenB: { id: '11', name: 'K', symbol: 'K', displaySymbol: 'dK' }, + commissionFeeInPct: '0' + } + ], + [ + { + symbol: 'J-K', + poolPairId: '23', + priceRatio: { ab: '0.14285714', ba: '7.00000000' }, + tokenA: { id: '10', name: 'J', symbol: 'J', displaySymbol: 'dJ' }, + tokenB: { id: '11', name: 'K', symbol: 'K', displaySymbol: 'dK' }, + commissionFeeInPct: '0.25000000' + } + ] + ] + }) + }) + + it('should have no swap path - isolated token H', async () => { + const response = await controller.listPaths('8', '1') // H to A impossible + expect(response).toStrictEqual({ + fromToken: { + id: '8', + name: 'H', + symbol: 'H', + displaySymbol: 'dH' + }, + toToken: { + id: '1', + name: 'A', + symbol: 'A', + displaySymbol: 'dA' + }, + 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('404 - NotFound (/v0/regtest/poolpairs/paths/from/-1/to/1): Unable to find token -1') + await expect(controller.listPaths('1', '-1')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/paths/from/1/to/-1): Unable to find token -1') + await expect(controller.listPaths('100', '1')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/paths/from/100/to/1): Unable to find token 100') + await expect(controller.listPaths('1', '100')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/paths/from/1/to/100): Unable to find token 100') + await expect(controller.listPaths('-1', '100')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/paths/from/-1/to/100): Unable to find token -1') + // 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') + // await expect(controller.listPaths('100', '1')).rejects.toThrowError('Unable to find token 100') + // await expect(controller.listPaths('1', '100')).rejects.toThrowError('Unable to find token 100') + // await expect(controller.listPaths('-1', '100')).rejects.toThrowError('Unable to find token -1') + }) +}) + +describe('get list swappable tokens', () => { + it('should list correct swappable tokens', async () => { + const result = await controller.listSwappableTokens('1') // A + expect(result).toStrictEqual(expect.objectContaining({ + fromToken: { id: '1', name: 'A', symbol: 'A', displaySymbol: 'dA' }, + swappableTokens: expect.arrayContaining([ + { id: '7', name: 'G', symbol: 'G', displaySymbol: 'dG' }, + { id: '0', name: 'Default Defi token', symbol: 'DFI', displaySymbol: 'DFI' }, + { id: '30', name: 'USDT', symbol: 'USDT', displaySymbol: 'dUSDT' }, + { id: '6', name: 'F', symbol: 'F', displaySymbol: 'dF' }, + { id: '5', name: 'E', symbol: 'E', displaySymbol: 'dE' }, + { id: '4', name: 'D', symbol: 'D', displaySymbol: 'dD' }, + { id: '3', name: 'C', symbol: 'C', displaySymbol: 'dC' }, + { id: '2', name: 'B', symbol: 'B', displaySymbol: 'dB' } + ]) + })) + }) + + 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({ + fromToken: { id: '8', name: 'H', symbol: 'H', displaySymbol: 'dH' }, + swappableTokens: [] + }) + }) + + it('should throw error for invalid / non-existent tokenId', async () => { + // rust-ocean + // skip as `-1` failed throw path validation which is not u32, hit ParseIntErr + // await expect(controller.listSwappableTokens('-1')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/paths/swappable/-1): Unable to find token -1') + // await expect(controller.listSwappableTokens('a')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/paths/swappable/a): Unable to find token a') + await expect(controller.listSwappableTokens('100')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/paths/swappable/100): Unable to find token 100') + + // js-ocean + // await expect(controller.listSwappableTokens('-1')).rejects.toThrowError('Unable to find token -1') + // await expect(controller.listSwappableTokens('a')).rejects.toThrowError('Unable to find token a') + // await expect(controller.listSwappableTokens('100')).rejects.toThrowError('Unable to find token 100') + }) +}) + +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', name: 'Default Defi token', symbol: 'DFI' }, + dexPrices: { + USDT: { + token: { displaySymbol: 'dUSDT', id: '30', name: 'USDT', symbol: 'USDT' }, + denominationPrice: '0.43151288' + }, + N: { + token: { displaySymbol: 'dN', id: '14', name: 'N', symbol: 'N' }, + denominationPrice: '0' + }, + M: { + token: { displaySymbol: 'dM', id: '13', name: 'M', symbol: 'M' }, + denominationPrice: '0' + }, + L: { + token: { displaySymbol: 'dL', id: '12', name: 'L', symbol: 'L' }, + denominationPrice: '0' + }, + K: { + token: { displaySymbol: 'dK', id: '11', name: 'K', symbol: 'K' }, + denominationPrice: '0' + }, + J: { + token: { displaySymbol: 'dJ', id: '10', name: 'J', symbol: 'J' }, + denominationPrice: '0' + }, + I: { + token: { displaySymbol: 'dI', id: '9', name: 'I', symbol: 'I' }, + denominationPrice: '0' + }, + H: { + token: { displaySymbol: 'dH', id: '8', name: 'H', symbol: 'H' }, + denominationPrice: '0' + }, + G: { + token: { displaySymbol: 'dG', id: '7', name: 'G', symbol: 'G' }, + denominationPrice: '10.00000000' + }, + F: { + token: { displaySymbol: 'dF', id: '6', name: 'F', symbol: 'F' }, + denominationPrice: '0' + }, + E: { + token: { displaySymbol: 'dE', id: '5', name: 'E', symbol: 'E' }, + denominationPrice: '0' + }, + D: { + token: { displaySymbol: 'dD', id: '4', name: 'D', symbol: 'D' }, + denominationPrice: '0' + }, + C: { + token: { displaySymbol: 'dC', id: '3', name: 'C', symbol: 'C' }, + denominationPrice: '4.00000000' + }, + B: { + token: { displaySymbol: 'dB', id: '2', name: 'B', symbol: 'B' }, + denominationPrice: '6.00000000' + }, + A: { + token: { displaySymbol: 'dA', id: '1', name: 'A', 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: '30', name: 'USDT', symbol: 'USDT' }, + dexPrices: { + DFI: { + token: { displaySymbol: 'DFI', id: '0', name: 'Default Defi token', symbol: 'DFI' }, + denominationPrice: '2.31742792' // 1 DFI = 2.31 USDT + }, + A: { + token: { displaySymbol: 'dA', id: '1', name: 'A', symbol: 'A' }, + denominationPrice: '4.63485584' // 1 A = 4.63 USDT + }, + G: { + token: { displaySymbol: 'dG', id: '7', name: 'G', symbol: 'G' }, + denominationPrice: '23.17427920' // 1 G = 5 A = 10 DFI = 23 USDT + }, + B: { + token: { displaySymbol: 'dB', id: '2', name: 'B', symbol: 'B' }, + denominationPrice: '13.90456752' + }, + C: { + token: { displaySymbol: 'dC', id: '3', name: 'C', symbol: 'C' }, + denominationPrice: '9.26971168' + }, + N: { + token: { displaySymbol: 'dN', id: '14', name: 'N', symbol: 'N' }, + denominationPrice: '0' + }, + M: { + token: { displaySymbol: 'dM', id: '13', name: 'M', symbol: 'M' }, + denominationPrice: '0' + }, + L: { + token: { displaySymbol: 'dL', id: '12', name: 'L', symbol: 'L' }, + denominationPrice: '0' + }, + K: { + token: { displaySymbol: 'dK', id: '11', name: 'K', symbol: 'K' }, + denominationPrice: '0' + }, + J: { + token: { displaySymbol: 'dJ', id: '10', name: 'J', symbol: 'J' }, + denominationPrice: '0' + }, + I: { + token: { displaySymbol: 'dI', id: '9', name: 'I', symbol: 'I' }, + denominationPrice: '0' + }, + H: { + token: { displaySymbol: 'dH', id: '8', name: 'H', symbol: 'H' }, + denominationPrice: '0' + }, + F: { + token: { displaySymbol: 'dF', id: '6', name: 'F', symbol: 'F' }, + denominationPrice: '0' + }, + E: { + token: { displaySymbol: 'dE', id: '5', name: 'E', symbol: 'E' }, + denominationPrice: '0' + }, + D: { + token: { displaySymbol: 'dD', id: '4', name: 'D', 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 - O (non-DAT token) is not included in result', async () => { + // O not included in any denominated dex prices + const result = await controller.listDexPrices('DFI') + expect(result.dexPrices.O).toBeUndefined() + + // O is not a valid 'denomination' token + await expect(controller.listDexPrices('O')) + .rejects + .toThrowError('404 - NotFound (/v0/regtest/poolpairs/dexprices?denomination=O): Unable to find token') + // .toThrowError('Could not find token with symbol \'O\'') + }) + + 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('500 - Unknown (/v0/regtest/poolpairs/dexprices?denomination=BURN): Token "BURN" is invalid as it is not tradeable') + // .toThrowError('Token \'BURN\' is invalid as it is not tradeable') + }) + + describe('param validation - denomination', () => { + it('should throw error for invalid denomination', async () => { + await expect(controller.listDexPrices('aaaaa')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/dexprices?denomination=aaaaa): Unable to find token') + await expect(controller.listDexPrices('-1')).rejects.toThrowError('404 - NotFound (/v0/regtest/poolpairs/dexprices?denomination=-1): Unable to find token') + // 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('404 - NotFound (/v0/regtest/poolpairs/dexprices?denomination=dfi): Unable to find token') + // await expect(controller.listDexPrices('dfi')).rejects.toThrowError('Could not find token with symbol \'dfi\'') + }) + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/poolpair.fees.service.defid.ts b/apps/whale-api/src/module.api/__defid__/poolpair.fees.service.defid.ts new file mode 100644 index 0000000000..e7a72fa4f3 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/poolpair.fees.service.defid.ts @@ -0,0 +1,666 @@ +import { DPoolPairController, DefidBin, DefidRpc } from '../../e2e.defid.module' + +let testing: DefidRpc +let app: DefidBin +let controller: DPoolPairController + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.poolPairController + testing = app.rpc + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) + await setup() + + // const defiCache = app.get(DeFiDCache) + // const tokenResult = await app.call('listtokens') + // // precache + // for (const k in tokenResult) { + // await defiCache.getTokenInfo(k) + // } + + await app.waitForPath(controller) +}) + +afterAll(async () => { + await app.stop() +}) + +async function setup (): Promise { + const tokens = ['DUSD', 'CAT', 'DOG', 'KOALA', 'FISH', 'TURTLE', 'PANDA', 'RABBIT', 'FOX', 'LION', 'TIGER'] + + for (const token of tokens) { + await app.createToken(token) + await app.createPoolPair(token, 'DFI') + await app.mintTokens(token) + await testing.generate(1) + } + + await app.addPoolLiquidity({ + tokenA: 'DUSD', + amountA: 10, + tokenB: 'DFI', + amountB: 10, + shareAddress: await app.getNewAddress() + }) + + await testing.generate(1) + + await app.addPoolLiquidity({ + tokenA: 'CAT', + amountA: 100, + tokenB: 'DFI', + amountB: 1000, + shareAddress: await app.getNewAddress() + }) + await testing.generate(1) + + await app.addPoolLiquidity({ + tokenA: 'DOG', + amountA: 10, + tokenB: 'DFI', + amountB: 100, + shareAddress: await app.getNewAddress() + }) + await testing.generate(1) + + await app.addPoolLiquidity({ + tokenA: 'KOALA', + amountA: 10, + tokenB: 'DFI', + amountB: 100, + shareAddress: await app.getNewAddress() + }) + await testing.generate(1) + + await app.addPoolLiquidity({ + tokenA: 'FISH', + amountA: 5, + tokenB: 'DFI', + amountB: 100, + shareAddress: await app.getNewAddress() + }) + await testing.generate(1) + + await app.addPoolLiquidity({ + tokenA: 'TURTLE', + amountA: 1, + tokenB: 'DFI', + amountB: 100, + shareAddress: await app.getNewAddress() + }) + await testing.generate(1) + + await app.addPoolLiquidity({ + tokenA: 'PANDA', + amountA: 2, + tokenB: 'DFI', + amountB: 100, + shareAddress: await app.getNewAddress() + }) + await testing.generate(1) + + await app.addPoolLiquidity({ + tokenA: 'RABBIT', + amountA: 7, + tokenB: 'DFI', + amountB: 100, + shareAddress: await app.getNewAddress() + }) + await testing.generate(1) + + await app.addPoolLiquidity({ + tokenA: 'FOX', + amountA: 8, + tokenB: 'DFI', + amountB: 100, + shareAddress: await app.getNewAddress() + }) + await testing.generate(1) + + await app.addPoolLiquidity({ + tokenA: 'LION', + amountA: 10, + tokenB: 'DFI', + amountB: 100, + shareAddress: await app.getNewAddress() + }) + await testing.generate(1) + + await app.addPoolLiquidity({ + tokenA: 'TIGER', + amountA: 12, + tokenB: 'DFI', + amountB: 100, + shareAddress: await app.getNewAddress() + }) + await testing.generate(1) + + // dex fee set up + await app.call('setgov', [{ + ATTRIBUTES: { + 'v0/poolpairs/4/token_a_fee_pct': '0.001', // CAT + 'v0/poolpairs/4/token_a_fee_direction': 'in', // CAT + 'v0/poolpairs/4/token_b_fee_pct': '0.002', // DFI + 'v0/poolpairs/4/token_b_fee_direction': 'in', // DFI + + 'v0/poolpairs/6/token_a_fee_pct': '0.003', // DOG + 'v0/poolpairs/6/token_a_fee_direction': 'out', // DOG + 'v0/poolpairs/6/token_b_fee_pct': '0.004', // DFI + 'v0/poolpairs/6/token_b_fee_direction': 'out', // DFI + + 'v0/poolpairs/8/token_a_fee_pct': '0.005', // KOALA + 'v0/poolpairs/8/token_a_fee_direction': 'in', // KOALA + + 'v0/poolpairs/10/token_b_fee_pct': '0.006', // FISH + 'v0/poolpairs/10/token_b_fee_direction': 'out', // FISH + + 'v0/poolpairs/12/token_a_fee_pct': '0.007', // TURTLE + 'v0/poolpairs/12/token_a_fee_direction': 'both', // TURTLE + + 'v0/poolpairs/14/token_b_fee_pct': '0.008', // PANDA + 'v0/poolpairs/14/token_b_fee_direction': 'both', // PANDA + + 'v0/poolpairs/16/token_a_fee_pct': '0.009', // RABBIT + 'v0/poolpairs/16/token_a_fee_direction': 'both', // RABBIT + 'v0/poolpairs/16/token_b_fee_pct': '0.010', // RABBIT + 'v0/poolpairs/16/token_b_fee_direction': 'both', // RABBIT + + 'v0/poolpairs/18/token_a_fee_pct': '0.011', // FOX + + 'v0/poolpairs/20/token_b_fee_pct': '0.012', // LION + + 'v0/poolpairs/22/token_a_fee_pct': '0.013', // TIGER + 'v0/poolpairs/22/token_b_fee_pct': '0.014' // TIGER + } + }]) + await app.generate(1) +} + +describe('get best path - DEX burn fees', () => { + it('should return fees - CAT to DFI - Both token fees direction are in', async () => { + const paths1 = await controller.getBestPath('3', '0') + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.00100000', + ab: '0.00000000' + }, + poolPairId: '4', + priceRatio: { + ab: '0.10000000', + ba: '10.00000000' + }, + symbol: 'CAT-DFI', + tokenA: { + displaySymbol: 'dCAT', + id: '3', + name: 'CAT', + symbol: 'CAT' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + }) + + it('should return fees - DFI to CAT - Both token fees direction are in', async () => { + const paths1 = await controller.getBestPath('0', '3') + + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.00000000', + ab: '0.00200000' + }, + poolPairId: '4', + priceRatio: { + ab: '0.10000000', + ba: '10.00000000' + }, + symbol: 'CAT-DFI', + tokenA: { + displaySymbol: 'dCAT', + id: '3', + name: 'CAT', + symbol: 'CAT' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + }) + + it('should return fees - DFI to DOG - Both token fees direction is out', async () => { + const paths1 = await controller.getBestPath('0', '5') + + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.00300000', + ab: '0.00000000' + }, + poolPairId: '6', + priceRatio: { + ab: '0.10000000', + ba: '10.00000000' + }, + symbol: 'DOG-DFI', + tokenA: { + displaySymbol: 'dDOG', + id: '5', + name: 'DOG', + symbol: 'DOG' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + }) + + it('should return fees - DOG to DFI - Both token fees direction is out', async () => { + const paths1 = await controller.getBestPath('5', '0') + + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.00000000', + ab: '0.00400000' + }, + poolPairId: '6', + priceRatio: { + ab: '0.10000000', + ba: '10.00000000' + }, + symbol: 'DOG-DFI', + tokenA: { + displaySymbol: 'dDOG', + id: '5', + name: 'DOG', + symbol: 'DOG' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + }) + + it('should return fees - KOALA to DFI - TokenA fee direction is in', async () => { + const paths1 = await controller.getBestPath('7', '0') + + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.00500000', + ab: '0.00000000' + }, + poolPairId: '8', + priceRatio: { + ab: '0.10000000', + ba: '10.00000000' + }, + symbol: 'KOALA-DFI', + tokenA: { + displaySymbol: 'dKOALA', + id: '7', + name: 'KOALA', + symbol: 'KOALA' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + }) + + it('should return fees - DFI to KOALA - TokenA fee direction is in', async () => { + const paths1 = await controller.getBestPath('0', '7') + + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.00000000', + ab: '0.00000000' + }, + poolPairId: '8', + priceRatio: { + ab: '0.10000000', + ba: '10.00000000' + }, + symbol: 'KOALA-DFI', + tokenA: { + displaySymbol: 'dKOALA', + id: '7', + name: 'KOALA', + symbol: 'KOALA' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + }) + + it('should return fees - FISH to DFI - TokenB fee direction is out', async () => { + const paths1 = await controller.getBestPath('9', '0') + + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.00000000', + ab: '0.00600000' + }, + poolPairId: '10', + priceRatio: { + ab: '0.05000000', + ba: '20.00000000' + }, + symbol: 'FISH-DFI', + tokenA: { + displaySymbol: 'dFISH', + id: '9', + name: 'FISH', + symbol: 'FISH' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + }) + + it('should return fees - DFI to FISH - TokenB fee direction is out', async () => { + const paths1 = await controller.getBestPath('0', '9') + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.00000000', + ab: '0.00000000' + }, + poolPairId: '10', + priceRatio: { + ab: '0.05000000', + ba: '20.00000000' + }, + symbol: 'FISH-DFI', + tokenA: { + displaySymbol: 'dFISH', + id: '9', + name: 'FISH', + symbol: 'FISH' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + }) + + it('should return fees (bidirectional) - DFI <-> TURTLE - TokenA fee direction is both', async () => { + const paths1 = await controller.getBestPath('0', '11') + const paths2 = await controller.getBestPath('11', '0') + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.00700000', + ab: '0.00000000' + }, + poolPairId: '12', + priceRatio: { + ab: '0.01000000', + ba: '100.00000000' + }, + symbol: 'TURTLE-DFI', + tokenA: { + displaySymbol: 'dTURTLE', + id: '11', + name: 'TURTLE', + symbol: 'TURTLE' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + expect(paths1.bestPath).toStrictEqual(paths2.bestPath) + }) + + it('should return fees (bidirectional) - DFI <-> PANDA - TokenA fee direction is both', async () => { + const paths1 = await controller.getBestPath('0', '13') + const paths2 = await controller.getBestPath('13', '0') + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.00000000', + ab: '0.00800000' + }, + poolPairId: '14', + priceRatio: { + ab: '0.02000000', + ba: '50.00000000' + }, + symbol: 'PANDA-DFI', + tokenA: { + displaySymbol: 'dPANDA', + id: '13', + name: 'PANDA', + symbol: 'PANDA' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + expect(paths1.bestPath).toStrictEqual(paths2.bestPath) + }) + + it('should return fees (bidirectional) - DFI <-> RABBIT - Both token fees direction are both', async () => { + const paths1 = await controller.getBestPath('0', '15') + const paths2 = await controller.getBestPath('15', '0') + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.00900000', + ab: '0.01000000' + }, + poolPairId: '16', + priceRatio: { + ab: '0.07000000', + ba: '14.28571428' + }, + symbol: 'RABBIT-DFI', + tokenA: { + displaySymbol: 'dRABBIT', + id: '15', + name: 'RABBIT', + symbol: 'RABBIT' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + expect(paths1.bestPath).toStrictEqual(paths2.bestPath) + }) + + it('should return fees (bidirectional) - DFI <-> FOX - if tokenA fee direction is not set', async () => { + const paths1 = await controller.getBestPath('0', '17') + const paths2 = await controller.getBestPath('17', '0') + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.01100000', + ab: '0.00000000' + }, + poolPairId: '18', + priceRatio: { + ab: '0.08000000', + ba: '12.50000000' + }, + symbol: 'FOX-DFI', + tokenA: { + displaySymbol: 'dFOX', + id: '17', + name: 'FOX', + symbol: 'FOX' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + expect(paths1.bestPath).toStrictEqual(paths2.bestPath) + }) + + it('should return fees (bidirectional) - DFI <-> LION - if tokenB fee direction is not set', async () => { + const paths1 = await controller.getBestPath('0', '19') + const paths2 = await controller.getBestPath('19', '0') + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.00000000', + ab: '0.01200000' + }, + poolPairId: '20', + priceRatio: { + ab: '0.10000000', + ba: '10.00000000' + }, + symbol: 'LION-DFI', + tokenA: { + displaySymbol: 'dLION', + id: '19', + name: 'LION', + symbol: 'LION' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + expect(paths1.bestPath).toStrictEqual(paths2.bestPath) + }) + + it('should return fees (bidirectional) - DFI <-> TIGER - if both token fees direction are not set', async () => { + const paths1 = await controller.getBestPath('0', '21') + const paths2 = await controller.getBestPath('21', '0') + expect(paths1.bestPath).toStrictEqual([ + { + commissionFeeInPct: '0', + estimatedDexFeesInPct: { + ba: '0.01300000', + ab: '0.01400000' + }, + poolPairId: '22', + priceRatio: { + ab: '0.12000000', + ba: '8.33333333' + }, + symbol: 'TIGER-DFI', + tokenA: { + displaySymbol: 'dTIGER', + id: '21', + name: 'TIGER', + symbol: 'TIGER' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI' + } + }]) + expect(paths1.bestPath).toStrictEqual(paths2.bestPath) + }) + + it('should return [] if dex fees is not set', async () => { + const paths1 = await controller.getBestPath('1', '0') + const paths2 = await controller.getBestPath('0', '1') + expect(paths1.bestPath[0].estimatedDexFeesInPct).toStrictEqual(undefined) + expect(paths2.bestPath[0].estimatedDexFeesInPct).toStrictEqual(undefined) + }) +}) + +describe('get best path - DEX estimated return', () => { + it('should less dex fees in estimatedReturnLessDexFees - 1 leg', async () => { + const paths1 = await controller.getBestPath('15', '0') + + /* + 1 RABBIT = 14.28571428 DFI, 1 DFI = 0.07 RABBIT + DFIAfterFeeIn = 1 - (1 * 0.009) = 0.991 DFI + DFIToRABBIT = 0.991 * 14.28571428 = 14.1571428515 RABBIT + RABBITAfterFeeOut = 14.1571428515 - (14.1571428515 * 0.01) = 14.01557143 RABBIT + */ + expect(paths1.estimatedReturnLessDexFees).toStrictEqual('14.01557143') + }) + + it('should less dex fees in estimatedReturnLessDexFees - 2 legs', async () => { + const paths1 = await controller.getBestPath('15', '3') + + /* + 1 RABBIT = 14.28571428 DFI, 1 DFI = 0.07 RABBIT + RABBITAFterFeeIn = 1 - (1 * 0.009) = 0.991 RABBIT + RABBIT->DFI = 0.991 * 14.28571428 = 14.1571428515 DFI + DFIAfterFeeOut = 14.1571428515 - (14.1571428515 * 0.01) = 14.01557143 DFI + + 1 DFI = 0.1 CAT, 1 CAT = 10 DFI, + DFIAfterFeeIn = 14.01557143 - (14.01557143 * 0.002) = 13.9875402802871 DFI + DFI->CAT= 13.9875402802871 * 0.1 = 1.398754028 CAT + */ + expect(paths1.estimatedReturnLessDexFees).toStrictEqual('1.39875403') + }) + + it('should not less dex fees if dex fees is not set', async () => { + const paths1 = await controller.getBestPath('1', '0') + expect(paths1.estimatedReturn).toStrictEqual('1.00000000') + expect(paths1.estimatedReturnLessDexFees).toStrictEqual('1.00000000') + }) + + // TODO(PIERRE): estimated return with less total should be returned + // it('should return direct path even if composite swap paths has greater return', async () => { + // it('should return composite swap paths even if direct path has greater return', async () => { +}) diff --git a/apps/whale-api/src/module.api/__defid__/poolpair.swap.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/poolpair.swap.controller.defid.ts new file mode 100644 index 0000000000..eaf9f4d361 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/poolpair.swap.controller.defid.ts @@ -0,0 +1,337 @@ +import { PoolSwapData } from '@defichain/whale-api-client/dist/api/poolpairs' +import { DPoolPairController, DefidBin, DefidRpc } from '../../e2e.defid.module' +import { ApiPagedResponse } from '../_core/api.paged.response' + +let app: DefidBin +let container: DefidRpc +let controller: DPoolPairController + +beforeEach(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.poolPairController + container = app.rpc + await app.waitForBlockHeight(101) + + const tokens = ['A', 'B', 'C'] + + await app.rpc.token.dfi({ address: await app.rpc.address('swap'), amount: 10000 }) + await app.generate(1) + + for (const token of tokens) { + await app.waitForWalletBalanceGTE(110) + await app.rpc.token.create({ symbol: token }) + await container.generate(1) + await app.rpc.token.mint({ amount: 10000, symbol: token }) + await container.generate(1) + await app.rpc.token.send({ address: await app.rpc.address('swap'), symbol: token, amount: 1000 }) + } + + await app.rpc.poolpair.create({ tokenA: 'A', tokenB: 'B' }) + await container.generate(1) + await app.rpc.poolpair.add({ a: { symbol: 'A', amount: 100 }, b: { symbol: 'B', amount: 200 } }) + + await app.rpc.poolpair.create({ tokenA: 'C', tokenB: 'B' }) + await container.generate(1) + await app.rpc.poolpair.add({ a: { symbol: 'C', amount: 100 }, b: { symbol: 'B', amount: 200 } }) + + await app.rpc.poolpair.create({ tokenA: 'DFI', tokenB: 'C' }) + await container.generate(1) + await app.rpc.poolpair.add({ a: { symbol: 'DFI', amount: 100 }, b: { symbol: 'C', amount: 200 } }) + await container.generate(1) +}) + +afterEach(async () => { + await app.stop() +}) + +describe('poolswap buy-sell indicator', () => { + it('should get pool swap details', async () => { + await app.rpc.poolpair.swap({ + from: await app.rpc.address('swap'), + tokenFrom: 'C', + amountFrom: 15, + to: await app.rpc.address('swap'), + tokenTo: 'DFI' + }) + + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + + const res = await controller.listPoolSwaps('6') + expect(res.data.length).toStrictEqual(1) + expect(res.page).toBeUndefined() + + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '6', + sort: expect.any(String), + fromAmount: '15.00000000', + fromTokenId: 3, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + } + } + ) + }) + + it('should get composite pool swap for 2 jumps', async () => { + await app.rpc.client.poolpair.compositeSwap({ + from: await app.rpc.address('swap'), + tokenFrom: 'B', + amountFrom: 10, + to: await app.rpc.address('swap'), + tokenTo: 'DFI' + }) + + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + + { + const res: ApiPagedResponse = await controller.listPoolSwaps('5') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '5', + sort: expect.any(String), + fromAmount: '10.00000000', + fromTokenId: 2, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + } + } + ) + } + + { + const res: ApiPagedResponse = await controller.listPoolSwaps('6') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '6', + sort: expect.any(String), + fromAmount: '10.00000000', + fromTokenId: 2, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + } + } + ) + } + }) + + it('should get composite pool swap for 2 jumps scenario 2', async () => { + await app.rpc.client.poolpair.compositeSwap({ + from: await app.rpc.address('swap'), + tokenFrom: 'DFI', + amountFrom: 5, + to: await app.rpc.address('swap'), + tokenTo: 'B' + }) + + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + + { + const res: ApiPagedResponse = await controller.listPoolSwaps('5') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '5', + sort: expect.any(String), + fromAmount: '5.00000000', + fromTokenId: 0, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + } + } + ) + } + + { + const res: ApiPagedResponse = await controller.listPoolSwaps('6') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '6', + sort: expect.any(String), + fromAmount: '5.00000000', + fromTokenId: 0, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + } + } + ) + } + }) + + it('should get composite pool swap for 3 jumps', async () => { + await app.rpc.client.poolpair.compositeSwap({ + from: await app.rpc.address('swap'), + tokenFrom: 'A', + amountFrom: 20, + to: await app.rpc.address('swap'), + tokenTo: 'DFI' + }) + + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + + { + const res: ApiPagedResponse = await controller.listPoolSwaps('4') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '4', + sort: expect.any(String), + fromAmount: '20.00000000', + fromTokenId: 1, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + } + } + ) + } + + { + const res: ApiPagedResponse = await controller.listPoolSwaps('5') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '5', + sort: expect.any(String), + fromAmount: '20.00000000', + fromTokenId: 1, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + } + } + ) + } + + { + const res: ApiPagedResponse = await controller.listPoolSwaps('6') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '6', + sort: expect.any(String), + fromAmount: '20.00000000', + fromTokenId: 1, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + } + } + ) + } + }) + + it('should get direct pool swap for composite swap', async () => { + await app.rpc.client.poolpair.compositeSwap({ + from: await app.rpc.address('swap'), + tokenFrom: 'C', + amountFrom: 10, + to: await app.rpc.address('swap'), + tokenTo: 'DFI' + }) + + await app.rpc.client.poolpair.compositeSwap({ + from: await app.rpc.address('swap'), + tokenFrom: 'A', + amountFrom: 10, + to: await app.rpc.address('swap'), + tokenTo: 'B' + }) + + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + + { + const res: ApiPagedResponse = await controller.listPoolSwaps('6') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '6', + sort: expect.any(String), + fromAmount: '10.00000000', + fromTokenId: 3, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + } + } + ) + } + + { + const res: ApiPagedResponse = await controller.listPoolSwaps('4') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '4', + sort: expect.any(String), + fromAmount: '10.00000000', + fromTokenId: 1, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + } + } + ) + } + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/poolpair.swap.split.defid.ts b/apps/whale-api/src/module.api/__defid__/poolpair.swap.split.defid.ts new file mode 100644 index 0000000000..65c84a2ed0 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/poolpair.swap.split.defid.ts @@ -0,0 +1,245 @@ +import { PoolSwapAggregatedInterval } from '@defichain/whale-api-client/dist/api/poolpairs' +import { DPoolPairController, DefidBin } from '../../e2e.defid.module' +import BigNumber from 'bignumber.js' + +let app: DefidBin +let controller: DPoolPairController + +function now (): number { + return Math.floor(new Date().getTime() / 1000) +} + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.poolPairController + await app.waitForBlockHeight(101) + + await setup() +}) + +afterAll(async () => { + await app.stop() +}) + +async function setup (): Promise { + const tokens = ['A', 'B'] + + for (const token of tokens) { + await app.waitForWalletBalanceGTE(110) + await app.createToken(token, { + collateralAddress: await app.rpc.address('swap') + }) + await app.mintTokens(token, { + mintAmount: 10000 + }) + } + + await app.rpc.token.dfi({ + address: await app.rpc.address('swap'), + amount: 300000 + }) + await app.generate(1) + + const oracleId = await app.rpcClient.oracle.appointOracle(await app.getNewAddress(), + [{ + token: 'DFI', + currency: 'USD' + }, { + token: 'DUSD', + currency: 'USD' + }], + { weightage: 1 }) + await app.generate(1) + + await app.rpcClient.oracle.setOracleData(oracleId, now(), { + prices: [{ + tokenAmount: '1@DFI', + currency: 'USD' + }, { + tokenAmount: '1@DUSD', + currency: 'USD' + }] + }) + await app.generate(1) + + await app.rpcClient.loan.setCollateralToken({ + token: 'DFI', + factor: new BigNumber(1), + fixedIntervalPriceId: 'DFI/USD' + }) + await app.generate(1) + + await app.rpcClient.loan.setLoanToken({ + symbol: 'DUSD', + fixedIntervalPriceId: 'DUSD/USD' + }) + await app.generate(1) + + await app.rpcClient.loan.createLoanScheme({ + minColRatio: 100, + interestRate: new BigNumber(1), + id: 'default' + }) + await app.generate(1) + + const vaultId = await app.rpcClient.vault.createVault({ + ownerAddress: await app.rpc.address('swap'), + loanSchemeId: 'default' + }) + await app.generate(15) + + await app.rpcClient.vault.depositToVault({ + vaultId, from: await app.rpc.address('swap'), amount: '4000@DFI' + }) + await app.generate(1) + + await app.rpcClient.loan.takeLoan({ + vaultId, amounts: '3000@DUSD', to: await app.rpc.address('swap') + }) + await app.generate(1) + + await app.createPoolPair('A', 'DUSD') + await app.createPoolPair('B', 'DUSD') + + await app.addPoolLiquidity({ + tokenA: 'A', + amountA: 100, + tokenB: 'DUSD', + amountB: 200, + shareAddress: await app.getNewAddress() + }) + await app.addPoolLiquidity({ + tokenA: 'B', + amountA: 50, + tokenB: 'DUSD', + amountB: 300, + shareAddress: await app.getNewAddress() + }) +} + +it('should not getting empty after pool split', async () => { + const fiveMinutes = 60 * 5 + const numBlocks = 24 * 16 // 1.333 days + // before + { + const dateNow = new Date() + dateNow.setUTCSeconds(0) + dateNow.setUTCMinutes(2) + dateNow.setUTCHours(0) + dateNow.setUTCDate(dateNow.getUTCDate() + 2) + const timeNow = Math.floor(dateNow.getTime() / 1000) + await app.rpcClient.misc.setMockTime(timeNow) + await app.generate(10) + + for (let i = 0; i <= numBlocks; i++) { + const mockTime = timeNow + i * fiveMinutes + await app.rpcClient.misc.setMockTime(mockTime) + + await app.rpc.poolpair.swap({ + from: await app.rpc.address('swap'), + tokenFrom: 'DUSD', + amountFrom: 0.1, + to: await app.rpc.address('swap'), + tokenTo: 'B' + }) + + await app.generate(1) + } + + const height = await app.getBlockCount() + await app.generate(1) + await app.waitForBlockHeight(height) + } + + const beforePool = await app.rpc.client.poolpair.getPoolPair('B-DUSD') + // console.log('beforePool: ', beforePool) + const poolId = Object.keys(beforePool)[0] + // console.log('poolId: ', poolId) + let dusdToken = await app.rpc.client.token.getToken('DUSD') + let dusdTokenId = Object.keys(dusdToken)[0] + // console.log('dusdTokenId: ', dusdTokenId) + const { data: dayAggregated } = await controller.listPoolSwapAggregates(poolId, PoolSwapAggregatedInterval.ONE_DAY, { size: 10 }) + expect(dayAggregated.length).toBeGreaterThan(0) + // console.log('dayAggregated: ', dayAggregated) + + // split + await app.rpcClient.masternode.setGov({ + ATTRIBUTES: { + [`v0/poolpairs/${poolId}/token_a_fee_pct`]: '0.01', + [`v0/poolpairs/${poolId}/token_b_fee_pct`]: '0.03' + } + }) + await app.generate(1) + + await app.rpcClient.masternode.setGov({ LP_SPLITS: { [Number(poolId)]: 1 } }) + await app.generate(1) + + await app.rpcClient.masternode.setGov({ LP_LOAN_TOKEN_SPLITS: { [Number(poolId)]: 1 } }) + await app.generate(1) + + await app.rpc.client.masternode.setGov({ + ATTRIBUTES: { + [`v0/locks/token/${dusdTokenId}`]: 'true' + } + }) + await app.generate(1) + + const count = await app.rpc.client.blockchain.getBlockCount() + 2 + await app.rpc.client.masternode.setGov({ + ATTRIBUTES: { + [`v0/oracles/splits/${count}`]: `${dusdTokenId}/2` + } + }) + await app.generate(2) + + // after + const afterPool = await app.rpc.client.poolpair.getPoolPair(beforePool[poolId].symbol) + // console.log('afterPool: ', afterPool) + const afterPoolId = Object.keys(afterPool)[0] + dusdToken = await app.rpc.client.token.getToken('DUSD') + dusdTokenId = Object.keys(dusdToken)[0] + // console.log('dusdTokenId: ', dusdTokenId) + + await app.rpc.client.masternode.setGov({ + ATTRIBUTES: { + [`v0/locks/token/${dusdTokenId}`]: 'false' + } + }) + await app.generate(1) + + { + const dateNow = new Date() + dateNow.setUTCSeconds(0) + dateNow.setUTCMinutes(2) + dateNow.setUTCHours(0) + dateNow.setUTCDate(dateNow.getUTCDate() + 20) + const timeNow = Math.floor(dateNow.getTime() / 1000) + await app.rpcClient.misc.setMockTime(timeNow) + await app.generate(10) + + for (let i = 0; i <= numBlocks; i++) { + const mockTime = timeNow + i * fiveMinutes + await app.rpcClient.misc.setMockTime(mockTime) + + await app.rpc.poolpair.swap({ + from: await app.rpc.address('swap'), + tokenFrom: 'B', + amountFrom: 0.1, + to: await app.rpc.address('swap'), + tokenTo: 'DUSD' + }) + + await app.generate(1) + } + + const height = await app.getBlockCount() + await app.generate(1) + await app.waitForBlockHeight(height) + } + + // Note(canonbrother): PoolSwapAggregated with new poolId should already be indexed at ocean index_block_start + const { data: dayAggregatedAfter } = await controller.listPoolSwapAggregates(afterPoolId, PoolSwapAggregatedInterval.ONE_DAY, { size: 10 }) + expect(dayAggregatedAfter.length).toBeGreaterThan(0) + // console.log('dayAggregatedAfter: ', dayAggregatedAfter) +}) diff --git a/apps/whale-api/src/module.api/__defid__/poolpair.swap.verbose.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/poolpair.swap.verbose.controller.defid.ts new file mode 100644 index 0000000000..5d6d2b9c92 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/poolpair.swap.verbose.controller.defid.ts @@ -0,0 +1,467 @@ +import { PoolSwapData } from '@defichain/whale-api-client/dist/api/poolpairs' +import { DPoolPairController, DefidBin, DefidRpc } from '../../e2e.defid.module' +import { ApiPagedResponse } from '../_core/api.paged.response' + +let app: DefidBin +let container: DefidRpc +let controller: DPoolPairController + +beforeEach(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.poolPairController + container = app.rpc + await app.waitForBlockHeight(101) + + const tokens = ['A', 'B', 'C'] + + await app.rpc.token.dfi({ address: await app.rpc.address('swap'), amount: 10000 }) + await app.generate(1) + + for (const token of tokens) { + await app.waitForWalletBalanceGTE(110) + await app.rpc.token.create({ symbol: token }) + await container.generate(1) + await app.rpc.token.mint({ amount: 10000, symbol: token }) + await container.generate(1) + await app.rpc.token.send({ address: await app.rpc.address('swap'), symbol: token, amount: 1000 }) + } + + await app.rpc.poolpair.create({ tokenA: 'A', tokenB: 'B' }) + await container.generate(1) + await app.rpc.poolpair.add({ a: { symbol: 'A', amount: 100 }, b: { symbol: 'B', amount: 200 } }) + + await app.rpc.poolpair.create({ tokenA: 'C', tokenB: 'B' }) + await container.generate(1) + await app.rpc.poolpair.add({ a: { symbol: 'C', amount: 100 }, b: { symbol: 'B', amount: 200 } }) + + await app.rpc.poolpair.create({ tokenA: 'DFI', tokenB: 'C' }) + await container.generate(1) + await app.rpc.poolpair.add({ a: { symbol: 'DFI', amount: 100 }, b: { symbol: 'C', amount: 200 } }) + await container.generate(1) +}) + +afterEach(async () => { + await app.stop() +}) + +describe('poolswap buy-sell indicator', () => { + it('should get pool swap details', async () => { + await app.rpc.poolpair.swap({ + from: await app.rpc.address('swap'), + tokenFrom: 'C', + amountFrom: 15, + to: await app.rpc.address('swap'), + tokenTo: 'DFI' + }) + + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + + const res = await controller.listPoolSwapsVerbose('6') + expect(res.data.length).toStrictEqual(1) + expect(res.page).toBeUndefined() + + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '6', + sort: expect.any(String), + fromAmount: '15.00000000', + fromTokenId: 3, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + from: { + address: expect.any(String), + symbol: 'C', + amount: '15.00000000', + displaySymbol: 'dC' + }, + to: { + address: expect.any(String), + amount: '6.97674418', + symbol: 'DFI', + displaySymbol: 'DFI' + }, + type: 'BUY' + } + ) + }) + + it('should get composite pool swap for 2 jumps', async () => { + await app.rpc.client.poolpair.compositeSwap({ + from: await app.rpc.address('swap'), + tokenFrom: 'B', + amountFrom: 10, + to: await app.rpc.address('swap'), + tokenTo: 'DFI' + }) + + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + + { + const res: ApiPagedResponse = await controller.listPoolSwapsVerbose('5') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '5', + sort: expect.any(String), + fromAmount: '10.00000000', + fromTokenId: 2, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + from: { + address: expect.any(String), + symbol: 'B', + amount: '10.00000000', + displaySymbol: 'dB' + }, + to: { + address: expect.any(String), + amount: '2.32558139', + symbol: 'DFI', + displaySymbol: 'DFI' + }, + type: 'BUY' + } + ) + } + + { + const res: ApiPagedResponse = await controller.listPoolSwapsVerbose('6') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '6', + sort: expect.any(String), + fromAmount: '10.00000000', + fromTokenId: 2, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + from: { + address: expect.any(String), + symbol: 'B', + amount: '10.00000000', + displaySymbol: 'dB' + }, + to: { + address: expect.any(String), + amount: '2.32558139', + symbol: 'DFI', + displaySymbol: 'DFI' + }, + type: 'BUY' + } + ) + } + }) + + it('should get composite pool swap for 2 jumps scenario 2', async () => { + await app.rpc.client.poolpair.compositeSwap({ + from: await app.rpc.address('swap'), + tokenFrom: 'DFI', + amountFrom: 5, + to: await app.rpc.address('swap'), + tokenTo: 'B' + }) + + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + + { + const res: ApiPagedResponse = await controller.listPoolSwapsVerbose('5') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '5', + sort: expect.any(String), + fromAmount: '5.00000000', + fromTokenId: 0, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + from: { + address: expect.any(String), + symbol: 'DFI', + amount: '5.00000000', + displaySymbol: 'DFI' + }, + to: { + address: expect.any(String), + amount: '17.39130434', + symbol: 'B', + displaySymbol: 'dB' + }, + type: 'SELL' + } + ) + } + + { + const res: ApiPagedResponse = await controller.listPoolSwapsVerbose('6') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '6', + sort: expect.any(String), + fromAmount: '5.00000000', + fromTokenId: 0, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + from: { + address: expect.any(String), + symbol: 'DFI', + amount: '5.00000000', + displaySymbol: 'DFI' + }, + to: { + address: expect.any(String), + amount: '17.39130434', + symbol: 'B', + displaySymbol: 'dB' + }, + type: 'SELL' + } + ) + } + }) + + it('should get composite pool swap for 3 jumps', async () => { + await app.rpc.client.poolpair.compositeSwap({ + from: await app.rpc.address('swap'), + tokenFrom: 'A', + amountFrom: 20, + to: await app.rpc.address('swap'), + tokenTo: 'DFI' + }) + + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + + { + const res: ApiPagedResponse = await controller.listPoolSwapsVerbose('4') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '4', + sort: expect.any(String), + fromAmount: '20.00000000', + fromTokenId: 1, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + from: { + address: expect.any(String), + symbol: 'A', + amount: '20.00000000', + displaySymbol: 'dA' + }, + to: { + address: expect.any(String), + amount: '6.66666666', + symbol: 'DFI', + displaySymbol: 'DFI' + }, + type: 'SELL' + } + ) + } + + { + const res: ApiPagedResponse = await controller.listPoolSwapsVerbose('5') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '5', + sort: expect.any(String), + fromAmount: '20.00000000', + fromTokenId: 1, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + from: { + address: expect.any(String), + symbol: 'A', + amount: '20.00000000', + displaySymbol: 'dA' + }, + to: { + address: expect.any(String), + amount: '6.66666666', + symbol: 'DFI', + displaySymbol: 'DFI' + }, + type: 'BUY' + } + ) + } + + { + const res: ApiPagedResponse = await controller.listPoolSwapsVerbose('6') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '6', + sort: expect.any(String), + fromAmount: '20.00000000', + fromTokenId: 1, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + from: { + address: expect.any(String), + symbol: 'A', + amount: '20.00000000', + displaySymbol: 'dA' + }, + to: { + address: expect.any(String), + amount: '6.66666666', + symbol: 'DFI', + displaySymbol: 'DFI' + }, + type: 'BUY' + } + ) + } + }) + + it('should get direct pool swap for composite swap', async () => { + await app.rpc.client.poolpair.compositeSwap({ + from: await app.rpc.address('swap'), + tokenFrom: 'C', + amountFrom: 10, + to: await app.rpc.address('swap'), + tokenTo: 'DFI' + }) + + await app.rpc.client.poolpair.compositeSwap({ + from: await app.rpc.address('swap'), + tokenFrom: 'A', + amountFrom: 10, + to: await app.rpc.address('swap'), + tokenTo: 'B' + }) + + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + + { + const res: ApiPagedResponse = await controller.listPoolSwapsVerbose('6') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '6', + sort: expect.any(String), + fromAmount: '10.00000000', + fromTokenId: 3, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + from: { + address: expect.any(String), + symbol: 'C', + amount: '10.00000000', + displaySymbol: 'dC' + }, + to: { + address: expect.any(String), + amount: '4.76190476', + symbol: 'DFI', + displaySymbol: 'DFI' + }, + type: 'BUY' + } + ) + } + + { + const res: ApiPagedResponse = await controller.listPoolSwapsVerbose('4') + expect(res.data[0]).toStrictEqual( + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '4', + sort: expect.any(String), + fromAmount: '10.00000000', + fromTokenId: 1, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + from: { + address: expect.any(String), + symbol: 'A', + amount: '10.00000000', + displaySymbol: 'dA' + }, + to: { + address: expect.any(String), + amount: '18.18181818', + symbol: 'B', + displaySymbol: 'dB' + }, + type: 'SELL' + } + ) + } + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/poolpairs.defid.ts b/apps/whale-api/src/module.api/__defid__/poolpairs.defid.ts new file mode 100644 index 0000000000..947ddd5b1f --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/poolpairs.defid.ts @@ -0,0 +1,238 @@ +import { PoolSwapAggregatedInterval } from '@defichain/whale-api-client/dist/api/poolpairs' +import { DPoolPairController, DefidBin } from '../../e2e.defid.module' + +let app: DefidBin +let controller: DPoolPairController + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.poolPairController + await app.waitForBlockHeight(101) + + await setup() +}) + +afterAll(async () => { + await app.stop() +}) + +async function setup (): Promise { + const tokens = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'] + + for (const token of tokens) { + await app.waitForWalletBalanceGTE(110) + await app.createToken(token, { + collateralAddress: await app.rpc.address('swap') + }) + await app.mintTokens(token, { + mintAmount: 10000 + }) + } + await app.createPoolPair('A', 'DFI') + await app.createPoolPair('B', 'DFI') + await app.createPoolPair('C', 'DFI') + await app.createPoolPair('D', 'DFI') + await app.createPoolPair('E', 'DFI') + await app.createPoolPair('F', 'DFI') + await app.createPoolPair('G', 'DFI') + await app.createPoolPair('H', 'DFI') + await app.createPoolPair('H', 'I') + + await app.addPoolLiquidity({ + tokenA: 'A', + amountA: 100, + tokenB: 'DFI', + amountB: 200, + shareAddress: await app.getNewAddress() + }) + await app.addPoolLiquidity({ + tokenA: 'B', + amountA: 50, + tokenB: 'DFI', + amountB: 300, + shareAddress: await app.getNewAddress() + }) + await app.addPoolLiquidity({ + tokenA: 'C', + amountA: 90, + tokenB: 'DFI', + amountB: 360, + shareAddress: await app.getNewAddress() + }) + await app.addPoolLiquidity({ + tokenA: 'H', + amountA: 200, + tokenB: 'DFI', + amountB: 550, + shareAddress: await app.getNewAddress() + }) + + await app.addPoolLiquidity({ + tokenA: 'H', + amountA: 100, + tokenB: 'I', + amountB: 300, + shareAddress: await app.getNewAddress() + }) + + // dexUsdtDfi setup + await app.createToken('USDT') + await app.createPoolPair('USDT', 'DFI') + await app.mintTokens('USDT') + await app.addPoolLiquidity({ + tokenA: 'USDT', + amountA: 1000, + tokenB: 'DFI', + amountB: 431.51288, + shareAddress: await app.getNewAddress() + }) + + await app.createToken('USDC') + await app.createPoolPair('USDC', 'H') + await app.mintTokens('USDC') + await app.addPoolLiquidity({ + tokenA: 'USDC', + amountA: 500, + tokenB: 'H', + amountB: 31.51288, + shareAddress: await app.getNewAddress() + }) + + await app.createToken('DUSD') + await app.createToken('TEST', { + collateralAddress: await app.rpc.address('swap') + }) + await app.createPoolPair('TEST', 'DUSD', { + commission: 0.002 + }) + await app.mintTokens('DUSD') + await app.mintTokens('TEST') + await app.addPoolLiquidity({ + tokenA: 'TEST', + amountA: 20, + tokenB: 'DUSD', + amountB: 100, + shareAddress: await app.getNewAddress() + }) + + await app.rpc.token.dfi({ + address: await app.rpc.address('swap'), + amount: 20 + }) + + await app.createToken('BURN') + await app.createPoolPair('BURN', 'DFI', { status: false }) + await app.mintTokens('BURN', { mintAmount: 1 }) + await app.addPoolLiquidity({ + tokenA: 'BURN', + amountA: 1, + tokenB: 'DFI', + amountB: 1, + shareAddress: await app.getNewAddress() + }) +} + +it('should show aggregated swaps for 24h and 30d', async () => { + { + const fiveMinutes = 60 * 5 + const numBlocks = 24 * 16 // 1.333 days + const dateNow = new Date() + dateNow.setUTCSeconds(0) + dateNow.setUTCMinutes(2) + dateNow.setUTCHours(0) + dateNow.setUTCDate(dateNow.getUTCDate() + 2) + const timeNow = Math.floor(dateNow.getTime() / 1000) + await app.rpcClient.misc.setMockTime(timeNow) + await app.generate(10) + + for (let i = 0; i <= numBlocks; i++) { + const mockTime = timeNow + i * fiveMinutes + await app.rpcClient.misc.setMockTime(mockTime) + + await app.rpc.poolpair.swap({ + from: await app.rpc.address('swap'), + tokenFrom: 'B', + amountFrom: 0.1, + to: await app.rpc.address('swap'), + tokenTo: 'DFI' + }) + + await app.generate(1) + } + + const height = await app.getBlockCount() + await app.generate(1) + await app.waitForBlockHeight(height) + } + + const { data: dayAggregated } = await controller.listPoolSwapAggregates('11', PoolSwapAggregatedInterval.ONE_DAY, { size: 10 }) + expect([...dayAggregated]).toStrictEqual([ + { + aggregated: { + amounts: { 2: '9.50000000' }, + usd: 42.16329700024263 + }, + block: expect.any(Object), + bucket: expect.any(Number), + id: expect.any(String), + key: '11-86400' + }, + { + aggregated: { + amounts: { + 2: '29.00000000' + }, + usd: 128.7090118954775 + }, + block: expect.any(Object), + bucket: expect.any(Number), + id: expect.any(String), + key: '11-86400' + }, + { + aggregated: { + amounts: {}, + usd: 0 + }, + block: expect.any(Object), + bucket: expect.any(Number), + id: expect.any(String), + key: '11-86400' + } + ]) + + const { data: hourAggregated } = await controller.listPoolSwapAggregates('11', PoolSwapAggregatedInterval.ONE_HOUR, { size: 3 }) + expect([...hourAggregated]).toStrictEqual([ + { + aggregated: { + amounts: { 2: '1.10000000' }, + usd: 4.8820659684491465 + }, + block: expect.any(Object), + bucket: expect.any(Number), + id: expect.any(String), + key: '11-3600' + }, + { + aggregated: { + amounts: { 2: '1.20000000' }, + usd: 5.325890147399068 + }, + block: expect.any(Object), + bucket: expect.any(Number), + id: expect.any(String), + key: '11-3600' + }, + { + aggregated: { + amounts: { 2: '1.20000000' }, + usd: 5.325890147399068 + }, + block: expect.any(Object), + bucket: expect.any(Number), + id: expect.any(String), + key: '11-3600' + } + ]) +}) diff --git a/apps/whale-api/src/module.api/__defid__/prices.active.defid.ts b/apps/whale-api/src/module.api/__defid__/prices.active.defid.ts new file mode 100644 index 0000000000..ae60659c31 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/prices.active.defid.ts @@ -0,0 +1,154 @@ +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' +import { DPriceController, DefidBin, DefidRpc } from '../../e2e.defid.module' + +let container: DefidRpc +let app: DefidBin +let controller: DPriceController +let client: JsonRpcClient + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.priceController + container = app.rpc + await app.waitForWalletCoinbaseMaturity() + client = new JsonRpcClient(app.rpcUrl) + + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) +}) + +afterAll(async () => { + await app.stop() +}) + +const now = Math.floor(Date.now() / 1000) + +it('should get active price with 2 active oracles (exact values)', async () => { + const address = await app.getNewAddress() + const oracles = [] + + for (let i = 0; i < 2; i++) { + oracles.push(await client.oracle.appointOracle(address, [ + { token: 'S1', currency: 'USD' } + ], { + weightage: 1 + })) + await container.generate(1) + } + + { + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) + } + + await app.generate(1) + const { data: beforeActivePrice } = await controller.getFeedActive('S1-USD') + expect(beforeActivePrice.length).toStrictEqual(0) + + const oneMinute = 60 + const timestamp = now + for (let i = 0; i < oracles.length; i++) { + await client.oracle.setOracleData(oracles[i], timestamp + i * oneMinute, { + prices: [ + { tokenAmount: '10.0@S1', currency: 'USD' } + ] + }) + } + await app.generate(1) + + await client.loan.setLoanToken({ + symbol: 'S1', + fixedIntervalPriceId: 'S1/USD' + }) + await app.generate(1) + + for (let i = 0; i <= 6; i++) { + const mockTime = now + i * oneMinute + await client.misc.setMockTime(mockTime) + const price = i > 3 ? '12.0' : '10.0' + for (const oracle of oracles) { + await client.oracle.setOracleData(oracle, mockTime, { + prices: [ + { tokenAmount: `${price}@S1`, currency: 'USD' } + ] + }) + } + await app.generate(1) + } + + { + const height = await app.getBlockCount() + await app.generate(1) + await app.waitForBlockHeight(height) + } + + const { data: activePrice } = await controller.getFeedActive('S1-USD') + expect(activePrice[0]).toStrictEqual({ + block: { + hash: expect.any(String), + height: expect.any(Number), + medianTime: expect.any(Number), + time: expect.any(Number) + }, + id: expect.any(String), + key: 'S1-USD', + active: { + amount: '10.00000000', + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + next: { + amount: '12.00000000', + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + sort: expect.any(String), + isLive: true + }) + + { + await app.generate(1) + const height = await app.getBlockCount() + await app.generate(1) + await app.waitForBlockHeight(height) + } + + const { data: nextActivePrice } = await controller.getFeedActive('S1-USD') + expect(nextActivePrice[0]).toStrictEqual({ + active: { + amount: '10.00000000', + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + block: { + hash: expect.any(String), + height: expect.any(Number), + medianTime: expect.any(Number), + time: expect.any(Number) + }, + id: expect.any(String), + key: 'S1-USD', + next: { + amount: '12.00000000', + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + sort: expect.any(String), + isLive: true + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/prices.defid.ts b/apps/whale-api/src/module.api/__defid__/prices.defid.ts new file mode 100644 index 0000000000..111417b45d --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/prices.defid.ts @@ -0,0 +1,253 @@ +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' +import { DPriceController, DefidBin, DefidRpc } from '../../e2e.defid.module' + +let container: DefidRpc +let app: DefidBin +let controller: DPriceController +let client: JsonRpcClient + +interface OracleSetup { + id: string + address: string + weightage: number + feed: Array<{ token: string, currency: string }> + prices: Array> +} + +const setups: Record = { + a: { + id: undefined as any, + address: undefined as any, + weightage: 1, + feed: [ + { token: 'TA', currency: 'USD' }, + { token: 'TB', currency: 'USD' }, + { token: 'TC', currency: 'USD' }, + { token: 'TD', currency: 'USD' }, + { token: 'TE', currency: 'USD' }, + { token: 'TF', currency: 'USD' } + ], + prices: [ + [ + { tokenAmount: '1.1@TA', currency: 'USD' }, + { tokenAmount: '2.1@TB', currency: 'USD' }, + { tokenAmount: '3.1@TC', currency: 'USD' }, + { tokenAmount: '4.1@TD', currency: 'USD' }, + { tokenAmount: '5.1@TE', currency: 'USD' }, + { tokenAmount: '6.1@TF', currency: 'USD' } + ], + [ + { tokenAmount: '1@TA', currency: 'USD' }, + { tokenAmount: '2@TB', currency: 'USD' }, + { tokenAmount: '3@TC', currency: 'USD' }, + { tokenAmount: '4@TD', currency: 'USD' }, + { tokenAmount: '5@TE', currency: 'USD' }, + { tokenAmount: '6@TF', currency: 'USD' } + ], + [ + { tokenAmount: '0.9@TA', currency: 'USD' }, + { tokenAmount: '1.9@TB', currency: 'USD' } + ] + ] + }, + b: { + id: undefined as any, + address: undefined as any, + weightage: 2, + feed: [ + { token: 'TA', currency: 'USD' }, + { token: 'TB', currency: 'USD' }, + { token: 'TD', currency: 'USD' }, + { token: 'TG', currency: 'USD' } + ], + prices: [ + [ + { tokenAmount: '1.5@TA', currency: 'USD' }, + { tokenAmount: '2.5@TB', currency: 'USD' }, + { tokenAmount: '4.5@TD', currency: 'USD' }, + { tokenAmount: '6.5@TG', currency: 'USD' } + ], + [ + { tokenAmount: '1.5@TA', currency: 'USD' }, + { tokenAmount: '2.5@TB', currency: 'USD' }, + { tokenAmount: '4.5@TD', currency: 'USD' }, + { tokenAmount: '6.5@TG', currency: 'USD' } + ] + ] + }, + c: { + id: undefined as any, + address: undefined as any, + weightage: 0, + feed: [ + { token: 'TA', currency: 'USD' }, + { token: 'TB', currency: 'USD' }, + { token: 'TC', currency: 'USD' }, + { token: 'TH', currency: 'USD' } + ], + prices: [ + [ + { tokenAmount: '1.25@TA', currency: 'USD' }, + { tokenAmount: '2.25@TB', currency: 'USD' }, + { tokenAmount: '4.25@TC', currency: 'USD' }, + { tokenAmount: '8.25@TH', currency: 'USD' } + ] + ] + } +} + +const now = Math.floor(Date.now() / 1000) + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.priceController + container = app.rpc + await app.waitForWalletCoinbaseMaturity() + client = new JsonRpcClient(app.rpcUrl) + + for (const setup of Object.values(setups)) { + setup.address = await app.getNewAddress() + setup.id = await client.oracle.appointOracle(setup.address, setup.feed, { + weightage: setup.weightage + }) + await container.generate(1) + } + + for (const setup of Object.values(setups)) { + for (const price of setup.prices) { + await client.oracle.setOracleData(setup.id, now, { + prices: price + }) + await container.generate(1) + } + } + + const height = await app.getBlockCount() + await container.generate(1) + await app.waitForBlockHeight(height) +}) + +afterAll(async () => { + await app.stop() +}) + +it('should list', async () => { + const { data: prices } = await controller.list() + expect(prices.length).toStrictEqual(7) + expect(prices[0]).toStrictEqual({ + id: 'TB-USD', + sort: '000000030000006eTB-USD', + price: { + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + medianTime: expect.any(Number), + time: expect.any(Number) + }, + currency: 'USD', + token: 'TB', + id: 'TB-USD-110', + key: 'TB-USD', + sort: expect.any(String), + aggregated: { + amount: expect.any(String), + weightage: 3, + oracles: { + active: 2, + total: 3 + } + } + } + }) + + // check order + const expectOrders = [ + 'TB-USD', + 'TA-USD', + 'TC-USD', + 'TD-USD', + 'TG-USD', + 'TF-USD', + 'TE-USD' + ] + const ids = prices.map(p => p.id) + for (let i = 0; i < ids.length; i += 1) { + expect(ids[i]).toStrictEqual(expectOrders[i]) + } +}) + +it('should get ticker', async () => { + const ticker = await controller.get('TA-USD') + expect(ticker).toStrictEqual({ + id: 'TA-USD', + sort: '000000030000006eTA-USD', + price: { + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + medianTime: expect.any(Number), + time: expect.any(Number) + }, + aggregated: { + // amount: '1.30000000', + amount: expect.any(String), // 1.33333333 + weightage: 3, + oracles: { + active: 2, + total: 3 + } + }, + currency: 'USD', + token: 'TA', + id: 'TA-USD-110', + key: 'TA-USD', + sort: expect.any(String) + } + }) +}) + +it('should get feeds', async () => { + const { data: feeds } = await controller.getFeed('TA-USD') + expect(feeds.length).toStrictEqual(6) +}) + +it('should get oracles', async () => { + const { data: oracles } = await controller.listPriceOracles('TA-USD') + expect(oracles.length).toStrictEqual(3) + + expect(oracles[0]).toStrictEqual({ + id: expect.stringMatching(/TA-USD-[0-f]{64}/), + key: 'TA-USD', + oracleId: expect.stringMatching(/[0-f]{64}/), + token: 'TA', + currency: 'USD', + weightage: expect.any(Number), + feed: { + id: expect.any(String), + key: expect.any(String), + sort: expect.any(String), + amount: expect.any(String), + currency: 'USD', + token: 'TA', + time: expect.any(Number), + oracleId: oracles[0].oracleId, + txid: expect.stringMatching(/[0-f]{64}/), + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + medianTime: expect.any(Number), + time: expect.any(Number) + } + }, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + medianTime: expect.any(Number), + time: expect.any(Number) + } + }) +}) + +// NOTE(canonbrother): `getFeedWithInterval` are skipped on origin test suite due to flaky +// to do while needed diff --git a/apps/whale-api/src/module.api/__defid__/rawtx.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/rawtx.controller.defid.ts new file mode 100644 index 0000000000..ee7b3e131d --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/rawtx.controller.defid.ts @@ -0,0 +1,174 @@ +import { Bech32, Elliptic, HRP } from '@defichain/jellyfish-crypto' +import { RegTest } from '@defichain/jellyfish-network' +import { DRawTxController, DefidBin, DefidRpc } from '../../e2e.defid.module' +import { WhaleApiException } from '@defichain/whale-api-client/dist/errors' + +let container: DefidRpc +let app: DefidBin +let controller: DRawTxController + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.rawTxController + container = app.rpc + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) +}) + +afterAll(async () => { + await app.stop() +}) + +async function expectTxn (txid: string, amount: number, pubKey: Buffer): Promise { + const details = await app.call('gettxout', [txid, 0]) + + expect(details.value.toString(10)).toStrictEqual(amount.toString()) + expect(details.scriptPubKey.addresses[0]).toStrictEqual( + Bech32.fromPubKey(pubKey, RegTest.bech32.hrp as HRP) + ) +} + +describe('test', () => { + it('should accept valid txn', async () => { + const hex = await app.createSignedTxnHex(10, 9.9999) + await controller.test({ + hex: hex + }) + }) + + it('should accept valid txn with given maxFeeRate', async () => { + const hex = await app.createSignedTxnHex(10, 9.995) + await controller.test({ + hex: hex, + maxFeeRate: 0.05 + }) + }) + + it('should throw BadRequestError due to invalid txn', async () => { + // expect.assertions(2) + try { + await controller.test({ hex: '0400000100881133bb11aa00cc' }) + } catch (err: any) { + // expect(err).toBeInstanceOf(BadRequestApiException) + expect(err.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + message: 'Transaction decode failed', + at: expect.any(Number), + url: '/v0/regtest/rawtx/test' + }) + } + }) + + it('should throw BadRequestError due to high fees', async () => { + const hex = await app.createSignedTxnHex(10, 9) + // expect.assertions(2) + try { + await controller.test({ + hex: hex, maxFeeRate: 1.0 + }) + } catch (err: any) { + // expect(err).toBeInstanceOf(BadRequestApiException) + expect(err.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + message: 'Transaction is not allowed to be inserted' + }) + } + }) +}) + +describe('send', () => { + it('should send valid txn and validate tx out', async () => { + const aPair = Elliptic.fromPrivKey(Buffer.alloc(32, Math.random().toString(), 'ascii')) + const bPair = Elliptic.fromPrivKey(Buffer.alloc(32, Math.random().toString(), 'ascii')) + + const hex = await app.createSignedTxnHex(10, 9.9999, { aEllipticPair: aPair, bEllipticPair: bPair }) + const txid = await controller.send({ + hex: hex + }) + + await container.generate(1) + await expectTxn(txid, 9.9999, await bPair.publicKey()) + }) + + it('should send valid txn with given maxFeeRate and validate tx out', async () => { + const aPair = Elliptic.fromPrivKey(Buffer.alloc(32, Math.random().toString(), 'ascii')) + const bPair = Elliptic.fromPrivKey(Buffer.alloc(32, Math.random().toString(), 'ascii')) + + const hex = await app.createSignedTxnHex(10, 9.995, { aEllipticPair: aPair, bEllipticPair: bPair }) + const txid = await controller.send({ + hex: hex, + maxFeeRate: 0.05 + }) + + await container.generate(1) + await expectTxn(txid, 9.995, await bPair.publicKey()) + }) + + it('should throw BadRequestException due to invalid txn', async () => { + // expect.assertions(2) + try { + await controller.send({ + hex: '0400000100881133bb11aa00cc' + }) + } catch (err: any) { + // expect(err).toBeInstanceOf(BadRequestApiException) + expect(err.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + message: 'Transaction decode failed', + url: '/v0/regtest/rawtx/send' + }) + } + }) + + it('should throw BadRequestException due to high fees', async () => { + const hex = await app.createSignedTxnHex(10, 9) + // expect.assertions(2) + try { + await controller.send({ + hex: hex, maxFeeRate: 1 + }) + } catch (err: any) { + // expect(err).toBeInstanceOf(BadRequestApiException) + expect(err.response.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + message: 'Absurdly high fee' + }) + } + }) +}) + +describe('get', () => { + it('should accept valid txn and return hex', async () => { + const hex = await app.createSignedTxnHex(10, 9.9999) + const txid = await controller.send({ + hex: hex + }) + + const getResult = await controller.get(txid, false) + + expect(hex).toStrictEqual(getResult) + }) + + it('should throw NotFoundException due to tx id not found', async () => { + try { + await controller.get('4f9f92b4b2cade30393ecfcd0656db06e57f6edb0a176452b2fecf361dd3a061', false) + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find rawtx', + url: '/v0/regtest/rawtx/4f9f92b4b2cade30393ecfcd0656db06e57f6edb0a176452b2fecf361dd3a061?verbose=false' + }) + } + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/rpc.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/rpc.controller.defid.ts new file mode 100644 index 0000000000..5516597033 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/rpc.controller.defid.ts @@ -0,0 +1,32 @@ +import { DRpcController, DefidBin } from '../../e2e.defid.module' + +let app: DefidBin +let controller: DRpcController + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.rpcController + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) + + await app.waitForBlockHeight(100) +}) + +afterAll(async () => { + await app.stop() +}) + +it('test whitelisted getblockcount rpc call', async () => { + const res = await controller.post({ method: 'getblockcount', params: [] }) + expect(res.result).toStrictEqual(101) +}) + +it('test **NOT** whitelisted listpoolpairs rpc call', async () => { + await expect( + controller.post({ + method: 'listpoolpairs', + params: [{ start: 0, including_start: true, limit: 3 }, true] + }) + ).rejects.toThrowError('403 - Unknown (/v0/regtest/rpc): Rpc listpoolpairs method is not whitelisted') +}) diff --git a/apps/whale-api/src/module.api/__defid__/stats.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/stats.controller.defid.ts new file mode 100644 index 0000000000..3d5e5b861d --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/stats.controller.defid.ts @@ -0,0 +1,33 @@ +import { DStatsController, DefidBin, DefidRpc } from '../../e2e.defid.module' + +let container: DefidRpc +let app: DefidBin +let controller: DStatsController + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.statsController + container = app.rpc + await app.waitForBlockHeight(100) +}) + +afterAll(async () => { + await app.stop() +}) + +it('should getRewardDistribution', async () => { + await container.generate(10) + await app.waitForBlockHeight(110) + + const data = await controller.getRewardDistribution() + expect(data).toStrictEqual({ + masternode: 66.66, + community: 9.82, + anchor: 0.04, + liquidity: 50.9, + loan: 49.36, + options: 19.76, + unallocated: 3.46 + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/token.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/token.controller.defid.ts new file mode 100644 index 0000000000..affa03c4dd --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/token.controller.defid.ts @@ -0,0 +1,243 @@ +import { WhaleApiException } from '@defichain/whale-api-client/dist/errors' +import { DTokenController, DefidBin } from '../../e2e.defid.module' + +let app: DefidBin +let controller: DTokenController + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.tokenController + await app.waitForBlockHeight(101) + + await app.createToken('DBTC') + await app.createToken('DETH') + await app.createPoolPair('DBTC', 'DETH') +}) + +afterAll(async () => { + await app.stop() +}) + +describe('list', () => { + it('should listTokens', async () => { + const result = await controller.list({ size: 100 }) + expect(result.data.length).toStrictEqual(4) + + expect(result.data[0]).toStrictEqual({ + id: '0', + symbol: 'DFI', + symbolKey: 'DFI', + displaySymbol: 'DFI', + name: 'Default Defi token', + decimal: 8, + limit: '0', + mintable: false, + tradeable: true, + isDAT: true, + isLPS: false, + isLoanToken: false, + finalized: true, + minted: '0', + creation: { + tx: '0000000000000000000000000000000000000000000000000000000000000000', + height: 0 + }, + destruction: { + tx: '0000000000000000000000000000000000000000000000000000000000000000', + height: -1 + } + // collateralAddress: undefined + }) + + expect(result.data[1]).toStrictEqual({ + id: '1', + symbol: 'DBTC', + symbolKey: 'DBTC', + displaySymbol: 'dDBTC', + name: 'DBTC', + decimal: 8, + limit: '0', + mintable: true, + tradeable: true, + isDAT: true, + isLPS: false, + isLoanToken: false, + finalized: false, + minted: '0', + creation: { + tx: expect.any(String), + height: expect.any(Number) + }, + destruction: { + tx: '0000000000000000000000000000000000000000000000000000000000000000', + height: -1 + }, + collateralAddress: expect.any(String) + }) + + expect(result.data[2]).toStrictEqual({ + id: '2', + symbol: 'DETH', + symbolKey: 'DETH', + displaySymbol: 'dDETH', + name: 'DETH', + decimal: 8, + limit: '0', + mintable: true, + tradeable: true, + isDAT: true, + isLPS: false, + isLoanToken: false, + finalized: false, + minted: '0', + creation: { + tx: expect.any(String), + height: expect.any(Number) + }, + destruction: { + tx: '0000000000000000000000000000000000000000000000000000000000000000', + height: -1 + }, + collateralAddress: expect.any(String) + }) + + expect(result.data[3]).toStrictEqual({ + id: '3', + symbol: 'DBTC-DETH', + symbolKey: 'DBTC-DETH', + displaySymbol: 'dDBTC-dDETH', + name: 'DBTC-DETH', + decimal: 8, + limit: '0', + mintable: false, + tradeable: true, + isDAT: true, + isLPS: true, + isLoanToken: false, + finalized: true, + minted: '0', + creation: { + tx: expect.any(String), + height: expect.any(Number) + }, + destruction: { + tx: '0000000000000000000000000000000000000000000000000000000000000000', + height: -1 + }, + collateralAddress: expect.any(String) + }) + }) + + it('should listTokens with pagination', async () => { + const first = await controller.list({ size: 2 }) + + expect(first.data.length).toStrictEqual(2) + expect(first.page?.next).toStrictEqual('1') + + expect(first.data[0]).toStrictEqual(expect.objectContaining({ id: '0', symbol: 'DFI', symbolKey: 'DFI' })) + expect(first.data[1]).toStrictEqual(expect.objectContaining({ id: '1', symbol: 'DBTC', symbolKey: 'DBTC' })) + + const next = await controller.list({ + size: 2, + next: first.page?.next + }) + + expect(next.data.length).toStrictEqual(2) + expect(next.page?.next).toStrictEqual('3') + + expect(next.data[0]).toStrictEqual(expect.objectContaining({ id: '2', symbol: 'DETH', symbolKey: 'DETH' })) + expect(next.data[1]).toStrictEqual(expect.objectContaining({ id: '3', symbol: 'DBTC-DETH', symbolKey: 'DBTC-DETH' })) + + const last = await controller.list({ + size: 1, + next: next.page?.next + }) + + expect(last.data.length).toStrictEqual(0) + expect(last.page).toBeUndefined() + }) + + it('should listTokens with an empty object if size 100 next 300 which is out of range', async () => { + const result = await controller.list({ size: 100, next: '300' }) + + expect(result.data.length).toStrictEqual(0) + expect(result.page).toBeUndefined() + }) +}) + +describe('get', () => { + it('should get DFI by DFI numeric id', async () => { + const data = await controller.get('0') + expect(data).toStrictEqual({ + id: '0', + symbol: 'DFI', + symbolKey: 'DFI', + displaySymbol: 'DFI', + name: 'Default Defi token', + decimal: 8, + limit: '0', + mintable: false, + tradeable: true, + isDAT: true, + isLPS: false, + isLoanToken: false, + finalized: true, + minted: '0', + creation: { + tx: '0000000000000000000000000000000000000000000000000000000000000000', + height: 0 + }, + destruction: { + tx: '0000000000000000000000000000000000000000000000000000000000000000', + height: -1 + } + // collateralAddress: undefined + }) + }) + + it('should get DBTC-DETH by DBTC-DETH numeric id', async () => { + const data = await controller.get('3') + expect(data).toStrictEqual({ + id: '3', + symbol: 'DBTC-DETH', + symbolKey: 'DBTC-DETH', + displaySymbol: 'dDBTC-dDETH', + name: 'DBTC-DETH', + decimal: 8, + limit: '0', + mintable: false, + tradeable: true, + isDAT: true, + isLPS: true, + isLoanToken: false, + finalized: true, + minted: '0', + creation: { + tx: expect.any(String), + height: expect.any(Number) + }, + destruction: { + tx: '0000000000000000000000000000000000000000000000000000000000000000', + height: -1 + }, + collateralAddress: expect.any(String) + }) + }) + + it('should throw error while getting non-existent token', async () => { + expect.assertions(2) + try { + await controller.get('999') + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find token', + url: '/v0/regtest/tokens/999' + }) + } + }) +}) diff --git a/apps/whale-api/src/module.api/__defid__/transaction.controller.defid.ts b/apps/whale-api/src/module.api/__defid__/transaction.controller.defid.ts new file mode 100644 index 0000000000..b7ef8d34f7 --- /dev/null +++ b/apps/whale-api/src/module.api/__defid__/transaction.controller.defid.ts @@ -0,0 +1,151 @@ +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' +import { DTransactionController, DefidBin, DefidRpc } from '../../e2e.defid.module' + +let container: DefidRpc +let app: DefidBin +let controller: DTransactionController +let client: JsonRpcClient + +beforeAll(async () => { + app = new DefidBin() + await app.start() + controller = app.ocean.transactionController + container = app.rpc + await app.waitForWalletCoinbaseMaturity() + await app.waitForWalletBalanceGTE(100) + + client = new JsonRpcClient(app.rpcUrl) + + await app.waitForBlockHeight(100) +}) + +afterAll(async () => { + await app.stop() +}) + +describe('get', () => { + let txid: string + + async function setup (): Promise { + const address = await app.getNewAddress() + const metadata = { + symbol: 'ETH', + name: 'ETH', + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: address + } + + txid = await app.call('createtoken', [metadata]) + + await container.generate(1) + + const height = await app.call('getblockcount') + + await container.generate(1) + + await app.waitForBlockHeight(height) + } + + beforeAll(async () => { + await setup() + }) + + it('should get a single transaction', async () => { + const transaction = await controller.get(txid) + expect(transaction).toStrictEqual({ + id: txid, + order: expect.any(Number), + block: { + hash: expect.any(String), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + txid, + hash: expect.any(String), + version: expect.any(Number), + size: expect.any(Number), + vSize: expect.any(Number), + weight: expect.any(Number), + lockTime: expect.any(Number), + vinCount: expect.any(Number), + voutCount: expect.any(Number), + totalVoutValue: expect.any(String) + }) + }) + + it('should fail due to non-existent transaction', async () => { + // expect.assertions(2) + // try { + // await controller.get('invalidtransactionid') + // } catch (err: any) { + // expect(err).toBeInstanceOf(NotFoundException) + // expect(err.response).toStrictEqual({ + // statusCode: 404, + // message: 'transaction not found', + // error: 'Not Found' + // }) + // } + await expect(controller.get('invalidtransactionid')).rejects.toThrowError('400 - BadRequest (/invalidtransactionid): bad hex string length 20 (expected 64)') + }) +}) + +describe('getVins', () => { + it('should return list of vin', async () => { + const blockHash = await app.call('getblockhash', [100]) + const block = await client.blockchain.getBlock(blockHash, 2) + + const txid = block.tx[0].txid + const vin = await controller.getVins(txid, { size: 30 }) + + expect(vin.data.length).toBeGreaterThanOrEqual(1) + }) + + it.skip('should return list of vin when next is out of range', async () => { + const blockHash = await app.call('getblockhash', [100]) + const block = await client.blockchain.getBlock(blockHash, 2) + + const txid = block.tx[0].txid + const vin = await controller.getVins(txid, { size: 30, next: '100' }) + + expect(vin.data.length).toBeGreaterThanOrEqual(1) + }) + + it('should return empty page if txid is not valid', async () => { + const vin = await controller.getVins('9d87a6b6b77323b6dab9d8971fff0bc7a6c341639ebae39891024f4800528532', { size: 30 }) + + expect(vin.data.length).toStrictEqual(0) + expect(vin.page).toBeUndefined() + }) +}) + +describe('getVouts', () => { + it('should return list of vout', async () => { + const blockHash = await app.call('getblockhash', [37]) + const block = await client.blockchain.getBlock(blockHash, 2) + + const txid = block.tx[0].txid + const vout = await controller.getVouts(txid, { size: 30 }) + + expect(vout.data.length).toBeGreaterThanOrEqual(1) + }) + + it.skip('should return list of vout when next is out of range', async () => { + const blockHash = await app.call('getblockhash', [37]) + const block = await client.blockchain.getBlock(blockHash, 2) + + const txid = block.tx[0].txid + const vout = await controller.getVouts(txid, { size: 30, next: '100' }) + + expect(vout.data.length).toBeGreaterThanOrEqual(1) + }) + + it('should return empty page if txid is not valid', async () => { + const vout = await controller.getVouts('9d87a6b6b77323b6dab9d8971fff0bc7a6c341639ebae39891024f4800528532', { size: 30 }) + + expect(vout.data.length).toStrictEqual(0) + expect(vout.page).toBeUndefined() + }) +}) diff --git a/jest.config.js b/jest.config.js index 9aa246179f..80da2bce34 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,7 +13,8 @@ module.exports = { clearMocks: true, testTimeout: 300000, testPathIgnorePatterns: [ - '__sanity__' + '__sanity__', + '__defid__' ], coveragePathIgnorePatterns: [ '/node_modules/', diff --git a/jest.defid.js b/jest.defid.js new file mode 100644 index 0000000000..87b8f48637 --- /dev/null +++ b/jest.defid.js @@ -0,0 +1,8 @@ +const config = require('./jest.config.js') + +module.exports = { + ...config, + testRegex: '((\\.|/)(defid))\\.ts$', + testPathIgnorePatterns: [], + testTimeout: 300000 +} diff --git a/package.json b/package.json index 68d427b890..d6a9c3ffeb 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "compile": "hardhat compile", "test": "jest --maxWorkers=100%", "sanity": "jest --maxWorkers=100% --config=jest.sanity.js", + "defid": "jest --maxWorkers=100% --maxParallel=4 --config=jest.defid.js", "ci:test": "jest --ci --coverage --forceExit --maxWorkers=4", "all:clean": "rm -rf ./packages/**/dist && rm -rf ./apps/dist && rm -rf ./packages/**/tsconfig.build.tsbuildinfo", "all:build": "lerna run build", diff --git a/packages/jellyfish-testing/src/poolpair.ts b/packages/jellyfish-testing/src/poolpair.ts index accad1963a..ee3deea3a0 100644 --- a/packages/jellyfish-testing/src/poolpair.ts +++ b/packages/jellyfish-testing/src/poolpair.ts @@ -43,7 +43,7 @@ export class TestingPoolPair { } } -interface TestingPoolPairCreate { +export interface TestingPoolPairCreate { tokenA: string tokenB: string commission?: number @@ -53,7 +53,7 @@ interface TestingPoolPairCreate { pairSymbol?: string } -interface TestingPoolPairAdd { +export interface TestingPoolPairAdd { a: { symbol: string amount: number | string @@ -65,7 +65,7 @@ interface TestingPoolPairAdd { address?: string } -interface TestingPoolPairRemove { +export interface TestingPoolPairRemove { address: string symbol: string amount: number | string diff --git a/packages/jellyfish-testing/src/token.ts b/packages/jellyfish-testing/src/token.ts index 9cbfc205cd..adeec4c236 100644 --- a/packages/jellyfish-testing/src/token.ts +++ b/packages/jellyfish-testing/src/token.ts @@ -56,7 +56,7 @@ export class TestingToken { } } -interface TestingTokenCreate { +export interface TestingTokenCreate { symbol: string name?: string isDAT?: boolean @@ -65,23 +65,23 @@ interface TestingTokenCreate { collateralAddress?: string } -interface TestingTokenDFI { +export interface TestingTokenDFI { address?: string amount: number | string } -interface TestingTokenMint { +export interface TestingTokenMint { amount: number | string symbol: string } -interface TestingTokenSend { +export interface TestingTokenSend { address: string amount: number | string symbol: string } -interface TestingTokenBurn { +export interface TestingTokenBurn { amount: number | string symbol: string from: string diff --git a/packages/testing/src/token.ts b/packages/testing/src/token.ts index 0900c4791c..9b2a9af5de 100644 --- a/packages/testing/src/token.ts +++ b/packages/testing/src/token.ts @@ -74,7 +74,7 @@ export interface MintTokensOptions { mintAmount?: number } -interface CreateTokenOptions { +export interface CreateTokenOptions { name?: string isDAT?: boolean mintable?: boolean diff --git a/packages/whale-api-client/__tests__/api/prices.test.ts b/packages/whale-api-client/__tests__/api/prices.test.ts index 82c9dd9c71..305d470714 100644 --- a/packages/whale-api-client/__tests__/api/prices.test.ts +++ b/packages/whale-api-client/__tests__/api/prices.test.ts @@ -55,19 +55,26 @@ describe('oracles', () => { { token: 'TA', currency: 'USD' }, { token: 'TB', currency: 'USD' }, { token: 'TC', currency: 'USD' }, - { token: 'TD', currency: 'USD' } + { token: 'TD', currency: 'USD' }, + { token: 'TE', currency: 'USD' }, + { token: 'TF', currency: 'USD' } ], prices: [ [ { tokenAmount: '1.1@TA', currency: 'USD' }, { tokenAmount: '2.1@TB', currency: 'USD' }, { tokenAmount: '3.1@TC', currency: 'USD' }, - { tokenAmount: '4.1@TD', currency: 'USD' } + { tokenAmount: '4.1@TD', currency: 'USD' }, + { tokenAmount: '5.1@TE', currency: 'USD' }, + { tokenAmount: '6.1@TF', currency: 'USD' } ], [ { tokenAmount: '1@TA', currency: 'USD' }, { tokenAmount: '2@TB', currency: 'USD' }, - { tokenAmount: '3@TC', currency: 'USD' } + { tokenAmount: '3@TC', currency: 'USD' }, + { tokenAmount: '4@TD', currency: 'USD' }, + { tokenAmount: '5@TE', currency: 'USD' }, + { tokenAmount: '6@TF', currency: 'USD' } ], [ { tokenAmount: '0.9@TA', currency: 'USD' }, @@ -82,18 +89,21 @@ describe('oracles', () => { feed: [ { token: 'TA', currency: 'USD' }, { token: 'TB', currency: 'USD' }, - { token: 'TD', currency: 'USD' } + { token: 'TD', currency: 'USD' }, + { token: 'TG', currency: 'USD' } ], prices: [ [ { tokenAmount: '1.5@TA', currency: 'USD' }, { tokenAmount: '2.5@TB', currency: 'USD' }, - { tokenAmount: '4.5@TD', currency: 'USD' } + { tokenAmount: '4.5@TD', currency: 'USD' }, + { tokenAmount: '6.5@TG', currency: 'USD' } ], [ { tokenAmount: '1.5@TA', currency: 'USD' }, { tokenAmount: '2.5@TB', currency: 'USD' }, - { tokenAmount: '4.5@TD', currency: 'USD' } + { tokenAmount: '4.5@TD', currency: 'USD' }, + { tokenAmount: '6.5@TG', currency: 'USD' } ] ] }, @@ -104,13 +114,15 @@ describe('oracles', () => { feed: [ { token: 'TA', currency: 'USD' }, { token: 'TB', currency: 'USD' }, - { token: 'TC', currency: 'USD' } + { token: 'TC', currency: 'USD' }, + { token: 'TH', currency: 'USD' } ], prices: [ [ { tokenAmount: '1.25@TA', currency: 'USD' }, { tokenAmount: '2.25@TB', currency: 'USD' }, - { tokenAmount: '4.25@TC', currency: 'USD' } + { tokenAmount: '4.25@TC', currency: 'USD' }, + { tokenAmount: '8.25@TH', currency: 'USD' } ] ] } @@ -141,7 +153,7 @@ describe('oracles', () => { it('should list', async () => { const prices = await apiClient.prices.list() - expect(prices.length).toStrictEqual(4) + expect(prices.length).toStrictEqual(7) expect(prices[0]).toStrictEqual({ id: 'TB-USD', sort: '000000030000006eTB-USD', @@ -167,6 +179,20 @@ describe('oracles', () => { } } }) + + const expectOrders = [ + 'TB-USD', + 'TA-USD', + 'TC-USD', + 'TD-USD', + 'TG-USD', + 'TF-USD', + 'TE-USD' + ] + const ids = prices.map(p => p.id) + for (let i = 0; i < ids.length; i += 1) { + expect(ids[i]).toStrictEqual(expectOrders[i]) + } }) describe('TA-USD', () => {