diff --git a/packages/whale-api-client/__tests__/api/account.history.test.ts b/packages/whale-api-client/__tests__/api/account.history.test.ts new file mode 100644 index 0000000000..6a66fb3bbd --- /dev/null +++ b/packages/whale-api-client/__tests__/api/account.history.test.ts @@ -0,0 +1,215 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { Testing } from '@defichain/jellyfish-testing' +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiClient, WhaleApiException } from '../../src' +import { RegTestFoundationKeys } from '@defichain/jellyfish-network' + +let container: MasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient +let testing: Testing + +let colAddr: string +let usdcAddr: string +let poolAddr: string +let emptyAddr: string + +beforeAll(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + testing = Testing.create(container) + + await testing.container.start() + await testing.container.waitForWalletCoinbaseMaturity() + await service.start() + + colAddr = await testing.generateAddress() + usdcAddr = await testing.generateAddress() + poolAddr = await testing.generateAddress() + emptyAddr = await testing.generateAddress() + + await testing.token.dfi({ address: colAddr, amount: 20000 }) + await testing.generate(1) + + await testing.token.create({ symbol: 'USDC', collateralAddress: colAddr }) + await testing.generate(1) + + await testing.token.mint({ symbol: 'USDC', amount: 10000 }) + await testing.generate(1) + + await testing.rpc.account.accountToAccount(colAddr, { [usdcAddr]: '10000@USDC' }) + await testing.generate(1) + + await testing.rpc.poolpair.createPoolPair({ + tokenA: 'DFI', + tokenB: 'USDC', + commission: 0, + status: true, + ownerAddress: poolAddr + }) + await testing.generate(1) + + await testing.rpc.poolpair.addPoolLiquidity({ + [colAddr]: '5000@DFI', + [usdcAddr]: '5000@USDC' + }, poolAddr) + await testing.generate(1) + + await testing.rpc.poolpair.poolSwap({ + from: colAddr, + tokenFrom: 'DFI', + amountFrom: 555, + to: usdcAddr, + tokenTo: 'USDC' + }) + await testing.generate(1) + + await testing.rpc.poolpair.removePoolLiquidity(poolAddr, '2@DFI-USDC') + await testing.generate(1) + + // for testing same block pagination + await testing.token.create({ symbol: 'APE', collateralAddress: colAddr }) + await testing.generate(1) + + await testing.token.create({ symbol: 'CAT', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'DOG', collateralAddress: colAddr }) + await testing.generate(1) + + await testing.token.create({ symbol: 'ELF', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'FOX', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'RAT', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'BEE', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'COW', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'OWL', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'ELK', collateralAddress: colAddr }) + await testing.generate(1) + + await testing.token.create({ symbol: 'PIG', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'KOI', collateralAddress: colAddr }) + await testing.token.create({ symbol: 'FLY', collateralAddress: colAddr }) + await testing.generate(1) + + const height = await testing.container.getBlockCount() + await testing.generate(1) + await service.waitForIndexedHeight(height - 1) +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await testing.container.stop() + } +}) + +describe('listAccountHistory', () => { + it('should not listAccountHistory with mine filter', async () => { + const promise = client.address.listAccountHistory('mine') + await expect(promise).rejects.toThrow(WhaleApiException) + await expect(promise).rejects.toThrow('mine is not allowed') + }) + + it('should list empty account history', async () => { + const history = await client.address.listAccountHistory(emptyAddr) + expect(history.length).toStrictEqual(0) + }) + + it('should listAccountHistory', async () => { + const history = await client.address.listAccountHistory(colAddr) + + expect(history.length).toStrictEqual(30) + for (let i = 0; i < history.length; i += 1) { + const accountHistory = history[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 client.address.listAccountHistory(colAddr, 10) + expect(history.length).toStrictEqual(10) + }) + + it('test listAccountHistory pagination', async () => { + const full = await client.address.listAccountHistory(colAddr, 12) + + const first = await client.address.listAccountHistory(colAddr, 3) + expect(first[0]).toStrictEqual(full[0]) + expect(first[1]).toStrictEqual(full[1]) + expect(first[2]).toStrictEqual(full[2]) + + const firstLast = first[first.length - 1] + const secondToken = `${firstLast.txid}-${firstLast.type}-${firstLast.block.height}` + const second = await client.address.listAccountHistory(colAddr, 3, secondToken) + expect(second[0]).toStrictEqual(full[3]) + expect(second[1]).toStrictEqual(full[4]) + expect(second[2]).toStrictEqual(full[5]) + + const secondLast = second[second.length - 1] + const thirdToken = `${secondLast.txid}-${secondLast.type}-${secondLast.block.height}` + const third = await client.address.listAccountHistory(colAddr, 3, thirdToken) + expect(third[0]).toStrictEqual(full[6]) + expect(third[1]).toStrictEqual(full[7]) + expect(third[2]).toStrictEqual(full[8]) + + const thirdLast = third[third.length - 1] + const forthToken = `${thirdLast.txid}-${thirdLast.type}-${thirdLast.block.height}` + const forth = await client.address.listAccountHistory(colAddr, 3, forthToken) + expect(forth[0]).toStrictEqual(full[9]) + expect(forth[1]).toStrictEqual(full[10]) + expect(forth[2]).toStrictEqual(full[11]) + }) +}) + +describe('getAccountHistory', () => { + it('should getAccountHistory', async () => { + const history = await client.address.listAccountHistory(colAddr, 30) + for (const h of history) { + if (['sent', 'receive'].includes(h.type)) { + continue + } + const acc = await client.address.getAccountHistory(colAddr, h.block.height, h.txn) + expect(acc?.owner).toStrictEqual(h.owner) + expect(acc?.txid).toStrictEqual(h.txid) + expect(acc?.txn).toStrictEqual(h.txn) + } + + const poolHistory = await client.address.listAccountHistory(poolAddr, 30) + for (const h of poolHistory) { + if (['sent', 'receive'].includes(h.type)) { + continue + } + const acc = await client.address.getAccountHistory(poolAddr, h.block.height, h.txn) + expect(acc?.owner).toStrictEqual(h.owner) + expect(acc?.txid).toStrictEqual(h.txid) + expect(acc?.txn).toStrictEqual(h.txn) + } + }) + + it('should be failed as getting unsupport tx type - sent, received, blockReward', async () => { + const history = await client.address.listAccountHistory(colAddr, 30) + for (const h of history) { + if (['sent', 'receive'].includes(h.type)) { + const promise = client.address.getAccountHistory(colAddr, h.block.height, h.txn) + await expect(promise).rejects.toThrow('Record not found') + } + } + + const operatorAccHistory = await container.call('listaccounthistory', [RegTestFoundationKeys[0].operator.address]) + for (const h of operatorAccHistory) { + if (['blockReward'].includes(h.type)) { + const promise = client.address.getAccountHistory(RegTestFoundationKeys[0].operator.address, h.blockHeight, h.txn) + await expect(promise).rejects.toThrow('Record not found') + } + } + }) +}) diff --git a/packages/whale-api-client/__tests__/api/address.test.ts b/packages/whale-api-client/__tests__/api/address.test.ts new file mode 100644 index 0000000000..2654398eec --- /dev/null +++ b/packages/whale-api-client/__tests__/api/address.test.ts @@ -0,0 +1,361 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiClient } from '../../src' +import { createSignedTxnHex, createToken, mintTokens, utxosToAccount } from '@defichain/testing' +import { WIF } from '@defichain/jellyfish-crypto' +import { Testing } from '@defichain/jellyfish-testing' +import BigNumber from 'bignumber.js' + +let container: MasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient + +let address: string +const tokens = ['A', 'B', 'C', 'D'] + +beforeAll(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + + await container.start() + await container.waitForReady() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + address = await container.getNewAddress('', 'bech32') + await container.waitForWalletBalanceGTE(20) + + await utxosToAccount(container, 15.5, { address: address }) + await container.generate(1) + + for (const token of tokens) { + await container.waitForWalletBalanceGTE(101) + await createToken(container, token) + await mintTokens(container, token, { mintAmount: 1000 }) + await container.call('sendtokenstoaddress', [{}, { [address]: [`10@${token}`] }]) + } + + await container.generate(1) + + // setup a loan token + const testing = Testing.create(container) + const oracleId = await testing.rpc.oracle.appointOracle(await testing.generateAddress(), [ + { token: 'DFI', currency: 'USD' }, + { token: 'LOAN', currency: 'USD' } + ], { weightage: 1 }) + await testing.generate(1) + + await testing.rpc.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.rpc.loan.setCollateralToken({ + token: 'DFI', + factor: new BigNumber(1), + fixedIntervalPriceId: 'DFI/USD' + }) + await testing.rpc.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.rpc.loan.createLoanScheme({ + id: 'scheme', + minColRatio: 110, + interestRate: new BigNumber(1) + }) + await testing.generate(1) + + const vaultId = await testing.rpc.loan.createVault({ + ownerAddress: await testing.address('VAULT'), + loanSchemeId: 'scheme' + }) + await testing.generate(1) + + await testing.rpc.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.rpc.loan.depositToVault({ + vaultId: vaultId, + from: await testing.address('DFI'), + amount: '100@DFI' + }) + await testing.generate(1) + await testing.rpc.loan.takeLoan({ + vaultId: vaultId, + amounts: '10@LOAN', + to: address + }) + await testing.generate(1) +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +describe('getBalance', () => { + beforeAll(async () => { + await container.waitForWalletBalanceGTE(100) + }) + + it('should getBalance zero for bech32', async () => { + const address = await container.getNewAddress() + const balance = await client.address.getBalance(address) + expect(balance).toStrictEqual('0.00000000') + }) + + it('should getBalance for bech32', async () => { + const address = 'bcrt1qf5v8n3kfe6v5mharuvj0qnr7g74xnu9leut39r' + + await container.fundAddress(address, 1.23) + await service.waitForAddressTxCount(address, 1) + + const balance = await client.address.getBalance(address) + expect(balance).toStrictEqual('1.23000000') + }) + + it('should getBalance for legacy', async () => { + const address = await container.getNewAddress('', 'legacy') + + await container.fundAddress(address, 1.92822343) + await service.waitForAddressTxCount(address, 1) + + const balance = await client.address.getBalance(address) + expect(balance).toStrictEqual('1.92822343') + }) + + it('should getBalance for p2sh', async () => { + const address = await container.getNewAddress('', 'p2sh-segwit') + + await container.fundAddress(address, 1.23419341) + await service.waitForAddressTxCount(address, 1) + + const balance = await client.address.getBalance(address) + expect(balance).toStrictEqual('1.23419341') + }) +}) + +it('should getAggregation', async () => { + const address = 'bcrt1qxvvp3tz5u8t90nwwjzsalha66zk9em95tgn3fk' + + await container.waitForWalletBalanceGTE(30) + await container.fundAddress(address, 0.12340002) + await container.fundAddress(address, 4.32412313) + await container.fundAddress(address, 19.93719381) + await service.waitForAddressTxCount(address, 3) + + const agg = await client.address.getAggregation(address) + expect(agg).toStrictEqual({ + amount: { + txIn: '24.38471696', + txOut: '0.00000000', + unspent: '24.38471696' + }, + 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 + } + }) +}) + +describe('tokens', () => { + it('should listToken', async () => { + const response = await client.address.listToken(address) + expect(response.length).toStrictEqual(6) + expect(response.hasNext).toStrictEqual(false) + + expect(response[0]).toStrictEqual(expect.objectContaining({ id: '0', amount: '15.50000000', symbol: 'DFI', isLoanToken: false })) + expect(response[4]).toStrictEqual(expect.objectContaining({ id: '4', amount: '10.00000000', symbol: 'D', isLoanToken: false })) + expect(response[5]).toStrictEqual(expect.objectContaining({ id: '5', amount: '10.00000000', symbol: 'LOAN', isLoanToken: true })) + }) + + it('should paginate listToken', async () => { + const first = await client.address.listToken(address, 2) + expect(first.length).toStrictEqual(2) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken).toStrictEqual('1') + + expect(first[0]).toStrictEqual(expect.objectContaining({ id: '0', amount: '15.50000000', symbol: 'DFI' })) + expect(first[1]).toStrictEqual(expect.objectContaining({ id: '1', amount: '10.00000000', symbol: 'A' })) + + const next = await client.paginate(first) + expect(next.length).toStrictEqual(2) + expect(next.hasNext).toStrictEqual(true) + expect(next.nextToken).toStrictEqual('3') + + expect(next[0]).toStrictEqual(expect.objectContaining({ id: '2', amount: '10.00000000', symbol: 'B' })) + expect(next[1]).toStrictEqual(expect.objectContaining({ id: '3', amount: '10.00000000', symbol: 'C' })) + + const last = await client.paginate(next) + expect(last.length).toStrictEqual(2) + expect(last.hasNext).toStrictEqual(true) + expect(last.nextToken).toStrictEqual('5') + + expect(last[0]).toStrictEqual(expect.objectContaining({ id: '4', amount: '10.00000000', symbol: 'D', isLoanToken: false })) + expect(last[1]).toStrictEqual(expect.objectContaining({ id: '5', amount: '10.00000000', symbol: 'LOAN', isLoanToken: true })) + + const emptyLast = await client.paginate(last) + expect(emptyLast.length).toStrictEqual(0) + expect(emptyLast.hasNext).toStrictEqual(false) + }) +}) + +describe('transactions', () => { + const addressA = { + bech32: 'bcrt1qykj5fsrne09yazx4n72ue4fwtpx8u65zac9zhn', + privKey: 'cQSsfYvYkK5tx3u1ByK2ywTTc9xJrREc1dd67ZrJqJUEMwgktPWN' + } + const addressB = { + bech32: 'bcrt1qf26rj8895uewxcfeuukhng5wqxmmpqp555z5a7', + privKey: 'cQbfHFbdJNhg3UGaBczir2m5D4hiFRVRKgoU8GJoxmu2gEhzqHtV' + } + const options = { + aEllipticPair: WIF.asEllipticPair(addressA.privKey), + bEllipticPair: WIF.asEllipticPair(addressB.privKey) + } + + beforeAll(async () => { + await container.waitForWalletBalanceGTE(100) + await container.fundAddress(addressA.bech32, 34) + await container.fundAddress(addressB.bech32, 2.93719381) + + await container.call('sendrawtransaction', [ + // This create vin & vout with 9.5 + await createSignedTxnHex(container, 9.5, 9.4999, options) + ]) + await container.generate(1) + await service.waitForAddressTxCount(addressA.bech32, 3) + await service.waitForAddressTxCount(addressB.bech32, 2) + }) + + it('should listTransaction', async () => { + const transactions = await client.address.listTransaction(addressA.bech32) + + expect(transactions.length).toStrictEqual(3) + expect(transactions.hasNext).toStrictEqual(false) + + expect(transactions[2]).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: '34.00000000', + vout: { + n: expect.any(Number), + txid: expect.stringMatching(/[0-f]{64}/) + } + }) + }) + + it('should paginate listTransaction', async () => { + const first = await client.address.listTransaction(addressA.bech32, 2) + expect(first.length).toStrictEqual(2) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken).toMatch(/[0-f]{80}/) + + expect(first[0]).toStrictEqual(expect.objectContaining({ value: '9.50000000', type: 'vin' })) + expect(first[1]).toStrictEqual(expect.objectContaining({ value: '9.50000000', type: 'vout' })) + + const next = await client.paginate(first) + expect(next.length).toStrictEqual(1) + expect(next.hasNext).toStrictEqual(false) + + expect(next[0]).toStrictEqual(expect.objectContaining({ value: '34.00000000', type: 'vout' })) + }) + + it('should listTransactionUnspent', async () => { + const unspent = await client.address.listTransactionUnspent(addressA.bech32) + + expect(unspent.length).toStrictEqual(1) + expect(unspent.hasNext).toStrictEqual(false) + + expect(unspent[0]).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: '34.00000000' + } + }) + }) + + it('should paginate listTransactionUnspent', async () => { + const first = await client.address.listTransactionUnspent(addressB.bech32, 1) + expect(first.length).toStrictEqual(1) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken).toMatch(/[0-f]{80}/) + + expect(first[0].vout.value).toStrictEqual('2.93719381') + + const next = await client.paginate(first) + expect(next.length).toStrictEqual(1) + expect(next.hasNext).toStrictEqual(true) + + expect(next[0].vout.value).toStrictEqual('9.49990000') + + const empty = await client.paginate(next) + expect(empty.length).toStrictEqual(0) + }) +}) diff --git a/packages/whale-api-client/__tests__/api/address.vault.test.ts b/packages/whale-api-client/__tests__/api/address.vault.test.ts new file mode 100644 index 0000000000..ceb9a3bfa1 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/address.vault.test.ts @@ -0,0 +1,123 @@ +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiClient } from '../../src' +import BigNumber from 'bignumber.js' +import { Testing } from '@defichain/jellyfish-testing' +import { LoanMasterNodeRegTestContainer } from '@defichain/testcontainers' +import { LoanVaultState } from '../../src/api/Loan' + +let container: LoanMasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient +let testing: Testing + +beforeAll(async () => { + container = new LoanMasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + testing = Testing.create(container) + + await testing.rpc.loan.createLoanScheme({ + minColRatio: 100, + interestRate: new BigNumber(2.5), + id: 'default' + }) + await testing.generate(1) + + await testing.rpc.loan.createVault({ + ownerAddress: await testing.address('vault'), + loanSchemeId: 'default' + }) + await testing.generate(1) + + await testing.rpc.loan.createVault({ + ownerAddress: await testing.address('vault'), + loanSchemeId: 'default' + }) + await testing.generate(1) + + await testing.rpc.loan.createVault({ + ownerAddress: await testing.address('vault'), + loanSchemeId: 'default' + }) + await testing.generate(1) + + await testing.rpc.loan.createVault({ + ownerAddress: await testing.address('vault'), + loanSchemeId: 'default' + }) + await testing.generate(1) +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +describe('list', () => { + it('should listVault with size only', async () => { + const address = await testing.address('vault') + + const result = await client.address.listVault(address) + expect(result.length).toStrictEqual(4) + + result.forEach(e => + expect(e).toStrictEqual({ + vaultId: expect.any(String), + loanScheme: { + id: 'default', + interestRate: '2.5', + minColRatio: '100' + }, + ownerAddress: address, + state: LoanVaultState.ACTIVE, + informativeRatio: '-1', + collateralRatio: '-1', + collateralValue: '0', + loanValue: '0', + interestValue: '0', + collateralAmounts: [], + loanAmounts: [], + interestAmounts: [] + }) + ) + }) + + it('should listVault with size and pagination', async () => { + const address = await testing.address('vault') + + const vaultIds = (await client.address.listVault(address, 20)) + .map(value => value.vaultId) + + const first = await client.address.listVault(address, 2) + expect(first.length).toStrictEqual(2) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken).toStrictEqual(vaultIds[1]) + + expect(first[0].vaultId).toStrictEqual(vaultIds[0]) + expect(first[1].vaultId).toStrictEqual(vaultIds[1]) + + const next = await client.paginate(first) + + expect(next.length).toStrictEqual(2) + expect(next.hasNext).toStrictEqual(true) + expect(next.nextToken).toStrictEqual(vaultIds[3]) + + expect(next[0].vaultId).toStrictEqual(vaultIds[2]) + expect(next[1].vaultId).toStrictEqual(vaultIds[3]) + + const last = await client.paginate(next) + + expect(last.length).toStrictEqual(0) + expect(last.hasNext).toStrictEqual(false) + expect(last.nextToken).toBeUndefined() + }) +}) diff --git a/packages/whale-api-client/__tests__/api/blocks.test.ts b/packages/whale-api-client/__tests__/api/blocks.test.ts new file mode 100644 index 0000000000..ef1c37d07b --- /dev/null +++ b/packages/whale-api-client/__tests__/api/blocks.test.ts @@ -0,0 +1,172 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiClient } from '../../src' + +let container: MasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient + +beforeAll(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + await container.waitForBlockHeight(201) + await service.waitForIndexedHeight(201) +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +const ExpectedBlock = { + id: expect.stringMatching(/[0-f]{64}/), + hash: expect.stringMatching(/[0-f]{64}/), + previousHash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + version: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number), + transactionCount: expect.any(Number), + difficulty: expect.any(Number), + masternode: expect.stringMatching(/[0-f]{64}/), + minter: expect.stringMatching(/[a-zA-Z0-9]+/), + minterBlockCount: expect.any(Number), + stakeModifier: expect.stringMatching(/[0-f]{64}/), + merkleroot: expect.stringMatching(/[0-f]{64}/), + size: expect.any(Number), + sizeStripped: expect.any(Number), + weight: expect.any(Number), + reward: expect.any(String) +} + +describe('list', () => { + it('should get paginated list of blocks', async () => { + const first = await client.blocks.list(100) + + expect(first.length).toStrictEqual(100) + expect(first[0]).toStrictEqual(ExpectedBlock) + expect(first[0].height).toBeGreaterThanOrEqual(200 - 100) + expect(first[34].height).toBeGreaterThanOrEqual(first[0].height - 100) + + const second = await client.paginate(first) + expect(second.length).toStrictEqual(100) + expect(second[0].height).toStrictEqual(first[99].height - 1) + + const third = await client.paginate(second) + expect(third.length).toBeGreaterThanOrEqual(1) + expect(third[0].height).toStrictEqual(second[99].height - 1) + + const last = await client.paginate(third) + expect(last.hasNext).toStrictEqual(false) + }) + + it('should get paginated list of 200 even if request page size is more than 200', async () => { + const first = await client.blocks.list(250) + + expect(first.length).toStrictEqual(200) + expect(first[0]).toStrictEqual(ExpectedBlock) + }) + + it('should get paginated list of blocks when next is out of range', async () => { + const blocks = await client.blocks.list(15, '1000000') + expect(blocks.length).toStrictEqual(15) + + expect(blocks[0]).toStrictEqual(ExpectedBlock) + expect(blocks[0].height).toBeGreaterThanOrEqual(40) + }) + + it('should get paginated list of blocks when next is 0', async () => { + const blocks = await client.blocks.list(15, '0') + expect(blocks.length).toStrictEqual(0) + expect(blocks.hasNext).toStrictEqual(false) + }) + + it('should fetch the whole list of blocks when size is out of range', async () => { + const blocks = await client.blocks.list(60) + expect(blocks.length).toBeGreaterThanOrEqual(40) + + expect(blocks[0]).toStrictEqual(ExpectedBlock) + expect(blocks[0].height).toBeGreaterThanOrEqual(40) + }) +}) + +describe('get', () => { + it('should get block through height', async () => { + const block = await client.blocks.get('37') + + expect(block).toStrictEqual(ExpectedBlock) + expect(block?.height).toStrictEqual(37) + }) + + it('should get block through hash', async () => { + const blockHash = await container.call('getblockhash', [37]) + const block = await client.blocks.get(blockHash) + expect(block).toStrictEqual(ExpectedBlock) + expect(block?.height).toStrictEqual(37) + }) + + it('should get undefined through invalid hash', async () => { + const block = await client.blocks.get('d78167c999ed24b999de6530d6b7d9d723096e49baf191bd2706ddb8eaf452ae') + expect(block).toBeUndefined() + }) + + it('should get undefined through invalid height', async () => { + const block = await client.blocks.get('1000000000') + expect(block).toBeUndefined() + }) +}) + +describe('getTransactions', () => { + const ExpectedTransaction = { + id: expect.stringMatching(/[0-f]{64}/), + order: expect.any(Number), + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + }, + txid: expect.stringMatching(/[0-f]{64}/), + hash: expect.stringMatching(/[0-f]{64}/), + 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 getTransactions through hash', async () => { + const blockHash = await container.call('getblockhash', [37]) + const transactions = await client.blocks.getTransactions(blockHash) + expect(transactions[0]).toStrictEqual(ExpectedTransaction) + expect(transactions[0].block.height).toStrictEqual(37) + }) + + it('should not getTransactions with height', async () => { + const transactions = await client.blocks.getTransactions('0') + expect(transactions.length).toStrictEqual(0) + }) + + it('should getTransactions through invalid hash', async () => { + const transactions = await client.blocks.getTransactions('b33320d63574690eb549ee4867c0119efdb69b396d3452bf9a09132eaa76b4a5') + expect(transactions.length).toStrictEqual(0) + }) + + it('should getTransactions through invalid height', async () => { + const transactions = await client.blocks.getTransactions('1000000000') + expect(transactions.length).toStrictEqual(0) + }) +}) diff --git a/packages/whale-api-client/__tests__/api/fee.test.ts b/packages/whale-api-client/__tests__/api/fee.test.ts new file mode 100644 index 0000000000..b7ca7f80bd --- /dev/null +++ b/packages/whale-api-client/__tests__/api/fee.test.ts @@ -0,0 +1,37 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiClient } from '../../src' + +let container: MasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient + +beforeAll(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +beforeEach(async () => { + await container.waitForWalletBalanceGTE(15) +}) + +describe('estimate', () => { + it('should be fixed fee of 0.00005000 when there are no transactions', async () => { + const feeRate = await client.fee.estimate(10) + expect(feeRate).toStrictEqual(0.00005000) + }) +}) diff --git a/packages/whale-api-client/__tests__/api/loan.auction.history.test.ts b/packages/whale-api-client/__tests__/api/loan.auction.history.test.ts new file mode 100644 index 0000000000..d9be4ae5bc --- /dev/null +++ b/packages/whale-api-client/__tests__/api/loan.auction.history.test.ts @@ -0,0 +1,352 @@ +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import BigNumber from 'bignumber.js' +import { TestingGroup, Testing } from '@defichain/jellyfish-testing' +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { RegTestFoundationKeys } from '@defichain/jellyfish-network' +import { VaultLiquidation } from '@defichain/jellyfish-api-core/dist/category/loan' +import { HexEncoder } from '@defichain-apps/nest-apps/whale/src/module.model/_hex.encoder' + +const tGroup = TestingGroup.create(2, i => new MasterNodeRegTestContainer(RegTestFoundationKeys[i])) +const alice = tGroup.get(0) +const bob = tGroup.get(1) +let colAddr: string +let bobColAddr: string +let vaultId: string +let batch: number +let batch1: number + +const service = new StubService(alice.container) +const client = new StubWhaleApiClient(service) + +beforeAll(async () => { + await tGroup.start() + await alice.container.waitForWalletCoinbaseMaturity() + await service.start() + + colAddr = await alice.generateAddress() + bobColAddr = await bob.generateAddress() + + await dfi(alice, colAddr, 300000) + await createToken(alice, 'BTC', colAddr) + await mintTokens(alice, 'BTC', 50) + await alice.rpc.account.sendTokensToAddress({}, { [colAddr]: ['25@BTC'] }) + await alice.container.call('createloanscheme', [100, 1, 'default']) + await alice.generate(1) + + 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 alice.rpc.oracle.appointOracle(await alice.generateAddress(), priceFeeds, { weightage: 1 }) + await alice.generate(1) + await alice.rpc.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 alice.generate(1) + + await setCollateralToken(alice, 'DFI') + await setCollateralToken(alice, 'BTC') + + await setLoanToken(alice, 'AAPL') + await setLoanToken(alice, 'TSLA') + await setLoanToken(alice, 'MSFT') + + const mVaultId = await createVault(alice, 'default') + await depositToVault(alice, mVaultId, colAddr, '200001@DFI') + await depositToVault(alice, mVaultId, colAddr, '20@BTC') + await takeLoan(alice, mVaultId, ['60000@TSLA', '60000@AAPL', '60000@MSFT']) + + await alice.rpc.account.sendTokensToAddress({}, { [colAddr]: ['30000@TSLA', '30000@AAPL', '30000@MSFT'] }) + await alice.rpc.account.sendTokensToAddress({}, { [bobColAddr]: ['30000@TSLA', '30000@AAPL', '30000@MSFT'] }) + await alice.generate(1) + await tGroup.waitForSync() + + vaultId = await createVault(alice, 'default') + await depositToVault(alice, vaultId, colAddr, '10001@DFI') + await depositToVault(alice, vaultId, colAddr, '1@BTC') + await takeLoan(alice, vaultId, '7500@AAPL') + await takeLoan(alice, vaultId, '2500@TSLA') + + { + const data = await alice.container.call('listauctions', []) + expect(data).toStrictEqual([]) + + const list = await alice.container.call('listauctions') + expect(list.every((each: any) => each.state === 'active')) + } + + // liquidated + await alice.rpc.oracle.setOracleData(oracleId, now(), { + prices: [ + { tokenAmount: '2.2@AAPL', currency: 'USD' }, + { tokenAmount: '2.2@TSLA', currency: 'USD' } + ] + }) + await alice.container.generate(13) + + { + const list = await alice.container.call('listauctions') + expect(list.every((each: any) => each.state === 'inLiquidation')) + } + + let vault = await alice.rpc.loan.getVault(vaultId) as VaultLiquidation + batch = vault.liquidationHeight + + // BID WAR!! + // vaultId[0] + await placeAuctionBid(alice, vaultId, 0, colAddr, '5300@AAPL') + await tGroup.waitForSync() + await placeAuctionBid(bob, vaultId, 0, bobColAddr, '5355@AAPL') + await tGroup.waitForSync() + await placeAuctionBid(alice, vaultId, 0, colAddr, '5408.55@AAPL') + await tGroup.waitForSync() + + // vaultId[1] + await placeAuctionBid(alice, vaultId, 1, colAddr, '2700.00012@AAPL') + await tGroup.waitForSync() + await placeAuctionBid(bob, vaultId, 1, bobColAddr, '2730@AAPL') + await tGroup.waitForSync() + await placeAuctionBid(alice, vaultId, 1, colAddr, '2760.0666069@AAPL') + await tGroup.waitForSync() + + // vaultId[2] + await placeAuctionBid(alice, vaultId, 2, colAddr, '2625.01499422@TSLA') + await tGroup.waitForSync() + + // do another batch + await alice.generate(40) + await tGroup.waitForSync() + + await depositToVault(alice, vaultId, colAddr, '10001@DFI') + await depositToVault(alice, vaultId, colAddr, '1@BTC') + await takeLoan(alice, vaultId, '10000@MSFT') + + // liquidated #2 + await alice.rpc.oracle.setOracleData(oracleId, now(), { + prices: [ + { tokenAmount: '2.2@MSFT', currency: 'USD' } + ] + }) + await alice.container.generate(13) + + vault = await alice.rpc.loan.getVault(vaultId) as VaultLiquidation + batch1 = vault.liquidationHeight + + // BID WAR #2!! + await placeAuctionBid(alice, vaultId, 0, colAddr, '5300.123@MSFT') + await tGroup.waitForSync() + await placeAuctionBid(bob, vaultId, 0, bobColAddr, '5355.123@MSFT') + await tGroup.waitForSync() + + const height = await alice.container.call('getblockcount') + await alice.generate(1) + await service.waitForIndexedHeight(height) +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await tGroup.stop() + } +}) + +it('should listVaultAuctionHistory', async () => { + { + const list = await client.loan.listVaultAuctionHistory(vaultId, batch, 0) + expect(list.length).toStrictEqual(3) + expect([...list]).toStrictEqual([ + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(list[0].block.height)}-${list[0].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + amount: '5408.55', + tokenId: 2, + block: expect.any(Object) + }, + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(list[1].block.height)}-${list[1].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + amount: '5355', + tokenId: 2, + block: expect.any(Object) + }, + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(list[2].block.height)}-${list[2].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + amount: '5300', + tokenId: 2, + block: expect.any(Object) + } + ]) + } + + { + const list = await client.loan.listVaultAuctionHistory(vaultId, batch1, 0) + expect(list.length).toStrictEqual(2) + expect([...list]).toStrictEqual([ + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(list[0].block.height)}-${list[0].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + amount: '5355.123', + tokenId: 4, + block: expect.any(Object) + }, + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(list[1].block.height)}-${list[1].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + amount: '5300.123', + tokenId: 4, + block: expect.any(Object) + } + ]) + } +}) + +it('should listVaultAuctionHistory with pagination', async () => { + const first = await client.loan.listVaultAuctionHistory(vaultId, batch, 0, 1) + expect(first.length).toStrictEqual(1) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken).toStrictEqual(first[0].sort) + expect([...first]).toStrictEqual([ + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(first[0].block.height)}-${first[0].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + amount: '5408.55', + tokenId: 2, + block: expect.any(Object) + } + ]) + + const next = await client.paginate(first) + expect(next.length).toStrictEqual(1) + expect(next.hasNext).toStrictEqual(true) + expect(next.nextToken).toStrictEqual(next[0].sort) + expect([...next]).toStrictEqual([ + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(next[0].block.height)}-${next[0].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + amount: '5355', + tokenId: 2, + block: expect.any(Object) + } + ]) + + const last = await client.paginate(next) + expect(last.length).toStrictEqual(1) + expect(last.hasNext).toStrictEqual(true) + expect(last.nextToken).toStrictEqual(last[0].sort) + expect([...last]).toStrictEqual([ + { + id: expect.any(String), + key: `${vaultId}-0`, + sort: `${HexEncoder.encodeHeight(last[0].block.height)}-${last[0].id.split('-')[2]}`, + vaultId: vaultId, + index: 0, + from: expect.any(String), + amount: '5300', + tokenId: 2, + block: expect.any(Object) + } + ]) + + const none = await client.paginate(last) + expect(none.length).toStrictEqual(0) + expect(none.hasNext).toStrictEqual(false) +}) + +function now (): number { + return Math.floor(new Date().getTime() / 1000) +} +async function dfi (testing: Testing, address: string, amount: number): Promise { + await testing.token.dfi({ + address: address, + amount: amount + }) + await testing.generate(1) +} +async function createToken (testing: Testing, symbol: string, address: string): Promise { + await testing.token.create({ + symbol: symbol, + collateralAddress: address + }) + await testing.generate(1) +} +async function mintTokens (testing: Testing, symbol: string, amount: number): Promise { + await testing.token.mint({ + symbol: symbol, + amount: amount + }) + await testing.generate(1) +} +async function setCollateralToken (testing: Testing, symbol: string): Promise { + await testing.rpc.loan.setCollateralToken({ + token: symbol, + factor: new BigNumber(1), + fixedIntervalPriceId: `${symbol}/USD` + }) + await testing.generate(1) +} +async function setLoanToken (testing: Testing, symbol: string): Promise { + await testing.rpc.loan.setLoanToken({ + symbol: symbol, + fixedIntervalPriceId: `${symbol}/USD` + }) + await testing.generate(1) +} +async function createVault (testing: Testing, schemeId: string, address?: string): Promise { + const vaultId = await testing.rpc.container.call( + 'createvault', [address ?? await testing.generateAddress(), schemeId] + ) + await testing.generate(1) + return vaultId +} +async function depositToVault (testing: Testing, vaultId: string, address: string, tokenAmt: string): Promise { + await testing.rpc.container.call('deposittovault', [vaultId, address, tokenAmt]) + await testing.generate(1) +} +async function takeLoan (testing: Testing, vaultId: string, amounts: string | string[]): Promise { + await testing.rpc.container.call('takeloan', [{ vaultId, amounts }]) + await testing.generate(1) +} +async function placeAuctionBid (testing: Testing, vaultId: string, index: number, addr: string, tokenAmt: string): Promise { + await testing.container.call('placeauctionbid', [vaultId, index, addr, tokenAmt]) + await testing.generate(1) +} diff --git a/packages/whale-api-client/__tests__/api/loan.auction.test.ts b/packages/whale-api-client/__tests__/api/loan.auction.test.ts new file mode 100644 index 0000000000..d86d0b0dd7 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/loan.auction.test.ts @@ -0,0 +1,380 @@ +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import BigNumber from 'bignumber.js' +import { Testing } from '@defichain/jellyfish-testing' +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' + +const container = new MasterNodeRegTestContainer() +const service = new StubService(container) +const client = new StubWhaleApiClient(service) +const testing = Testing.create(container) + +let vaultId1: string + +beforeAll(async () => { + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + const collateralAddress = await testing.generateAddress() + await testing.token.dfi({ + address: collateralAddress, + amount: 300000 + }) + await testing.token.create({ + symbol: 'BTC', + collateralAddress + }) + await testing.generate(1) + await testing.token.mint({ + symbol: 'BTC', + amount: 11 + }) + await testing.generate(1) + + // Loan scheme + await testing.container.call('createloanscheme', [100, 1, 'default']) + await testing.generate(1) + + // Price oracle + const addr = await testing.generateAddress() + const priceFeeds = [ + { + token: 'DFI', + currency: 'USD' + }, + { + token: 'BTC', + currency: 'USD' + }, + { + token: 'AAPL', + currency: 'USD' + }, + { + token: 'TSLA', + currency: 'USD' + }, + { + token: 'MSFT', + currency: 'USD' + }, + { + token: 'FB', + currency: 'USD' + } + ] + const oracleId = await testing.rpc.oracle.appointOracle(addr, priceFeeds, { weightage: 1 }) + await testing.generate(1) + + const timestamp = Math.floor(new Date().getTime() / 1000) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '1@DFI', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '10000@BTC', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '2@AAPL', + currency: 'USD' + }] + }) + await testing.generate(1) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '2@TSLA', + currency: 'USD' + }] + }) + await testing.generate(1) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '2@MSFT', + currency: 'USD' + }] + }) + await testing.generate(1) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '2@FB', + currency: 'USD' + }] + }) + await testing.generate(1) + + // Collateral tokens + await testing.rpc.loan.setCollateralToken({ + token: 'DFI', + factor: new BigNumber(1), + fixedIntervalPriceId: 'DFI/USD' + }) + await testing.rpc.loan.setCollateralToken({ + token: 'BTC', + factor: new BigNumber(1), + fixedIntervalPriceId: 'BTC/USD' + }) + await testing.generate(1) + + // Loan token + await testing.rpc.loan.setLoanToken({ + symbol: 'AAPL', + fixedIntervalPriceId: 'AAPL/USD' + }) + await testing.generate(1) + await testing.token.mint({ + symbol: 'AAPL', + amount: 10000 + }) + await testing.generate(1) + await testing.rpc.loan.setLoanToken({ + symbol: 'TSLA', + fixedIntervalPriceId: 'TSLA/USD' + }) + await testing.generate(1) + await testing.rpc.loan.setLoanToken({ + symbol: 'MSFT', + fixedIntervalPriceId: 'MSFT/USD' + }) + await testing.generate(1) + await testing.rpc.loan.setLoanToken({ + symbol: 'FB', + fixedIntervalPriceId: 'FB/USD' + }) + await testing.generate(1) + + // Vault 1 + vaultId1 = await testing.rpc.container.call('createvault', [await testing.generateAddress(), 'default']) + await testing.generate(1) + + await testing.container.call('deposittovault', [vaultId1, collateralAddress, '10000@DFI']) + await testing.generate(1) + await testing.container.call('deposittovault', [vaultId1, collateralAddress, '0.5@BTC']) + await testing.generate(1) + + await testing.container.call('takeloan', [{ + vaultId: vaultId1, + amounts: '7500@AAPL' + }]) + await testing.generate(1) + + // Vault 2 + const vaultId2 = await testing.rpc.container.call('createvault', [await testing.generateAddress(), 'default']) + await testing.generate(1) + + await testing.container.call('deposittovault', [vaultId2, collateralAddress, '20000@0DFI']) + await testing.generate(1) + await testing.container.call('deposittovault', [vaultId2, collateralAddress, '1@BTC']) + await testing.generate(1) + + await testing.container.call('takeloan', [{ + vaultId: vaultId2, + amounts: '15000@TSLA' + }]) + await testing.generate(1) + + // Vault 3 + const vaultId3 = await testing.rpc.container.call('createvault', [await testing.generateAddress(), 'default']) + await testing.generate(1) + + await testing.container.call('deposittovault', [vaultId3, collateralAddress, '30000@DFI']) + await testing.generate(1) + await testing.container.call('deposittovault', [vaultId3, collateralAddress, '1.5@BTC']) + await testing.generate(1) + + await testing.container.call('takeloan', [{ + vaultId: vaultId3, + amounts: '22500@MSFT' + }]) + await testing.generate(1) + + // Vault 4 + const vaultId4 = await testing.rpc.container.call('createvault', [await testing.generateAddress(), 'default']) + await testing.generate(1) + + await testing.container.call('deposittovault', [vaultId4, collateralAddress, '40000@DFI']) + await testing.generate(1) + await testing.container.call('deposittovault', [vaultId4, collateralAddress, '2@BTC']) + await testing.generate(1) + + await testing.container.call('takeloan', [{ + vaultId: vaultId4, + amounts: '30000@FB' + }]) + await testing.generate(1) + + { + // If there is no liquidation, return an empty array object + const data = await testing.rpc.loan.listAuctions() + expect(data).toStrictEqual([]) + } + + { + const vault1 = await testing.rpc.loan.getVault(vaultId1) + expect(vault1.state).toStrictEqual('active') + + const vault2 = await testing.rpc.loan.getVault(vaultId2) + expect(vault2.state).toStrictEqual('active') + + const vault3 = await testing.rpc.loan.getVault(vaultId3) + expect(vault3.state).toStrictEqual('active') + + const vault4 = await testing.rpc.loan.getVault(vaultId4) + expect(vault4.state).toStrictEqual('active') + } + + // Going to liquidate the vaults by price increase of the loan tokens + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '2.2@AAPL', + currency: 'USD' + }] + }) + await testing.generate(1) + await testing.container.waitForActivePrice('AAPL/USD', '2.2') + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '2.2@TSLA', + currency: 'USD' + }] + }) + await testing.container.waitForActivePrice('TSLA/USD', '2.2') + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '2.2@MSFT', + currency: 'USD' + }] + }) + await testing.generate(1) + await testing.container.waitForActivePrice('MSFT/USD', '2.2') + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '2.2@FB', + currency: 'USD' + }] + }) + await testing.generate(1) + await testing.container.waitForActivePrice('FB/USD', '2.2') + await testing.generate(1) + + const [auction1, auction2, auction3, auction4] = await testing.rpc.loan.listAuctions() + { + const vault1 = await testing.rpc.loan.getVault(auction1.vaultId) + expect(vault1.state).toStrictEqual('inLiquidation') + + const vault2 = await testing.rpc.loan.getVault(auction2.vaultId) + expect(vault2.state).toStrictEqual('inLiquidation') + + const vault3 = await testing.rpc.loan.getVault(auction3.vaultId) + expect(vault3.state).toStrictEqual('inLiquidation') + + const vault4 = await testing.rpc.loan.getVault(auction4.vaultId) + expect(vault4.state).toStrictEqual('inLiquidation') + } + + await testing.rpc.account.sendTokensToAddress({}, { [collateralAddress]: ['8000@AAPL'] }) + await testing.generate(1) + + const txid = await testing.container.call('placeauctionbid', [vaultId1, 0, collateralAddress, '8000@AAPL']) + expect(typeof txid).toStrictEqual('string') + expect(txid.length).toStrictEqual(64) + await testing.generate(1) +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +describe('list', () => { + it('should listAuction with size only', async () => { + const result = await client.loan.listAuction(20) + expect(result.length).toStrictEqual(4) + result.forEach((vault) => { + expect(vault).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) + }) + + if (vaultId1 === vault.vaultId) { + expect(vault.batches).toStrictEqual([ + { + collaterals: expect.any(Array), + froms: [], + highestBid: { + amount: { + activePrice: expect.any(Object), + amount: '8000.00000000', + displaySymbol: 'dAAPL', + id: expect.any(String), + name: expect.any(String), + symbol: 'AAPL', + symbolKey: 'AAPL' + }, + owner: expect.any(String) + }, + index: 0, + loan: expect.any(Object) + }, + { + collaterals: expect.any(Array), + froms: [], + index: 1, + loan: expect.any(Object) + } + ]) + } + }) + + result.forEach((e) => { + e.batches.forEach((f) => { + expect(typeof f.index).toBe('number') + expect(typeof f.collaterals).toBe('object') + expect(typeof f.loan).toBe('object') + expect(typeof f.highestBid === 'object' || f.highestBid === undefined).toBe(true) + }) + }) + }) + + it('should listAuction with size and pagination', async () => { + const auctionList = await client.loan.listAuction() + const first = await client.loan.listAuction(2) + + expect(first.length).toStrictEqual(2) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken).toStrictEqual(`${first[1].vaultId}${first[1].liquidationHeight}`) + + expect(first[0].vaultId).toStrictEqual(auctionList[0].vaultId) + expect(first[1].vaultId).toStrictEqual(auctionList[1].vaultId) + + const next = await client.paginate(first) + + expect(next.length).toStrictEqual(2) + expect(next.hasNext).toStrictEqual(true) + expect(next.nextToken).toStrictEqual(`${next[1].vaultId}${next[1].liquidationHeight}`) + + expect(next[0].vaultId).toStrictEqual(auctionList[2].vaultId) + expect(next[1].vaultId).toStrictEqual(auctionList[3].vaultId) + + const last = await client.paginate(next) + + expect(last.length).toStrictEqual(0) + expect(last.hasNext).toStrictEqual(false) + expect(last.nextToken).toBeUndefined() + }) +}) diff --git a/packages/whale-api-client/__tests__/api/loan.collateral.test.ts b/packages/whale-api-client/__tests__/api/loan.collateral.test.ts new file mode 100644 index 0000000000..e854d2485c --- /dev/null +++ b/packages/whale-api-client/__tests__/api/loan.collateral.test.ts @@ -0,0 +1,335 @@ +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiException } from '../../src' +import BigNumber from 'bignumber.js' +import { Testing } from '@defichain/jellyfish-testing' +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' + +const container = new MasterNodeRegTestContainer() +const service = new StubService(container) +const client = new StubWhaleApiClient(service) +let collateralTokenId1: string + +/* eslint-disable no-lone-blocks */ + +beforeAll(async () => { + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + const testing = Testing.create(container) + + { + 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.rpc.oracle.appointOracle(await container.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.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '1.5@AAPL', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '2.5@TSLA', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '3.5@MSFT', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '4.5@FB', + currency: 'USD' + }] + }) + await testing.generate(1) + } + + { + const oracleId = await testing.rpc.oracle.appointOracle(await testing.generateAddress(), [ + { token: 'AAPL', currency: 'USD' }, + { token: 'TSLA', currency: 'USD' }, + { token: 'MSFT', currency: 'USD' }, + { token: 'FB', currency: 'USD' } + ], { weightage: 1 }) + await testing.generate(1) + + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '2@AAPL', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '2@TSLA', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '2@MSFT', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '2@FB', + currency: 'USD' + }] + }) + await testing.generate(1) + } + + { + collateralTokenId1 = await testing.rpc.loan.setCollateralToken({ + token: 'AAPL', + factor: new BigNumber(0.1), + fixedIntervalPriceId: 'AAPL/USD' + }) + await testing.generate(1) + + await testing.rpc.loan.setCollateralToken({ + token: 'TSLA', + factor: new BigNumber(0.2), + fixedIntervalPriceId: 'TSLA/USD' + }) + await testing.generate(1) + + await testing.rpc.loan.setCollateralToken({ + token: 'MSFT', + factor: new BigNumber(0.3), + fixedIntervalPriceId: 'MSFT/USD' + }) + await testing.generate(1) + + await testing.rpc.loan.setCollateralToken({ + token: 'FB', + factor: new BigNumber(0.4), + fixedIntervalPriceId: 'FB/USD' + }) + await testing.generate(1) + } + + { + await testing.generate(12) + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + } +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +describe('list', () => { + it('should listCollateralTokens', async () => { + const result = await client.loan.listCollateralToken() + expect(result.length).toStrictEqual(4) + + // Not deterministic ordering due to use of id + expect(result[0]).toStrictEqual({ + tokenId: 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: false, + limit: '0', + mintable: true, + minted: '0', + name: expect.any(String), + symbol: expect.any(String), + symbolKey: expect.any(String), + tradeable: true + }, + factor: expect.any(String), + activateAfterBlock: expect.any(Number), + fixedIntervalPriceId: expect.any(String), + activePrice: { + active: { + amount: expect.any(String), + 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), + isLive: true, + key: expect.any(String), + next: { + amount: expect.any(String), + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + sort: expect.any(String) + } + }) + }) + + it('should listCollateral with pagination', async () => { + const first = await client.loan.listCollateralToken(2) + + expect(first.length).toStrictEqual(2) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken?.length).toStrictEqual(64) + + const next = await client.paginate(first) + + expect(next.length).toStrictEqual(2) + expect(next.hasNext).toStrictEqual(true) + expect(next.nextToken?.length).toStrictEqual(64) + + const last = await client.paginate(next) + + expect(last.length).toStrictEqual(0) + expect(last.hasNext).toStrictEqual(false) + expect(last.nextToken).toBeUndefined() + }) +}) + +describe('get', () => { + it('should get collateral token by symbol', async () => { + const data = await client.loan.getCollateralToken('AAPL') + expect(data).toStrictEqual({ + tokenId: collateralTokenId1, + factor: '0.1', + 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 + }, + activateAfterBlock: 110, + fixedIntervalPriceId: 'AAPL/USD', + activePrice: { + active: { + amount: '1.75000000', + 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), + isLive: true, + key: 'AAPL-USD', + next: { + amount: '1.75000000', + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + sort: expect.any(String) + } + }) + }) + + it('should fail due to getting non-existent or malformed collateral token id', async () => { + expect.assertions(4) + try { + await client.loan.getCollateralToken('999') + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find collateral token', + url: '/v0.0/regtest/loans/collaterals/999' + }) + } + + try { + await client.loan.getCollateralToken('$*@') + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find collateral token', + url: '/v0.0/regtest/loans/collaterals/$*@' + }) + } + }) +}) diff --git a/packages/whale-api-client/__tests__/api/loan.scheme.test.ts b/packages/whale-api-client/__tests__/api/loan.scheme.test.ts new file mode 100644 index 0000000000..7d267d4ab1 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/loan.scheme.test.ts @@ -0,0 +1,154 @@ +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiClient, WhaleApiException } from '../../src' +import BigNumber from 'bignumber.js' +import { Testing } from '@defichain/jellyfish-testing' +import { LoanMasterNodeRegTestContainer } from '@defichain/testcontainers' + +let container: LoanMasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient + +beforeAll(async () => { + container = new LoanMasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + const testing = Testing.create(container) + + // Default scheme + await testing.rpc.loan.createLoanScheme({ + minColRatio: 100, + interestRate: new BigNumber(6.5), + id: 'default' + }) + await testing.generate(1) + + await testing.rpc.loan.createLoanScheme({ + minColRatio: 150, + interestRate: new BigNumber(5.5), + id: 'scheme1' + }) + await testing.generate(1) + + await testing.rpc.loan.createLoanScheme({ + minColRatio: 200, + interestRate: new BigNumber(4.5), + id: 'scheme2' + }) + await testing.generate(1) + + await testing.rpc.loan.createLoanScheme({ + minColRatio: 250, + interestRate: new BigNumber(3.5), + id: 'scheme3' + }) + await testing.generate(1) +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +describe('list', () => { + it('should listScheme', async () => { + const list = await client.loan.listScheme() + expect(list.length).toStrictEqual(4) + expect([...list]).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 listScheme with pagination', async () => { + const first = await client.loan.listScheme(2) + + expect(first.length).toStrictEqual(2) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken).toStrictEqual('scheme1') + + expect(first[0].id).toStrictEqual('default') + expect(first[1].id).toStrictEqual('scheme1') + + const next = await client.paginate(first) + + expect(next.length).toStrictEqual(2) + expect(next.hasNext).toStrictEqual(true) + expect(next.nextToken).toStrictEqual('scheme3') + + expect(next[0].id).toStrictEqual('scheme2') + expect(next[1].id).toStrictEqual('scheme3') + + const last = await client.paginate(next) + + expect(last.length).toStrictEqual(0) + expect(last.hasNext).toStrictEqual(false) + expect(last.nextToken).toBeUndefined() + }) +}) + +describe('get', () => { + it('should get scheme by scheme id', async () => { + const data = await client.loan.getScheme('scheme1') + expect(data).toStrictEqual({ + id: 'scheme1', + minColRatio: '150', + interestRate: '5.5' + }) + }) + + it('should fail due to getting non-existent or malformed id', async () => { + expect.assertions(4) + try { + await client.loan.getScheme('999') + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find scheme', + url: '/v0.0/regtest/loans/schemes/999' + }) + } + + try { + await client.loan.getScheme('$*@') + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find scheme', + url: '/v0.0/regtest/loans/schemes/$*@' + }) + } + }) +}) diff --git a/packages/whale-api-client/__tests__/api/loan.token.test.ts b/packages/whale-api-client/__tests__/api/loan.token.test.ts new file mode 100644 index 0000000000..f25d4bbabc --- /dev/null +++ b/packages/whale-api-client/__tests__/api/loan.token.test.ts @@ -0,0 +1,323 @@ +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiException } from '../../src' +import BigNumber from 'bignumber.js' +import { Testing } from '@defichain/jellyfish-testing' +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' + +const container = new MasterNodeRegTestContainer() +const service = new StubService(container) +const client = new StubWhaleApiClient(service) + +/* eslint-disable no-lone-blocks */ + +beforeAll(async () => { + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + const testing = Testing.create(container) + + { + const oracleId = await testing.rpc.oracle.appointOracle(await testing.generateAddress(), [ + { token: 'AAPL', currency: 'USD' }, + { token: 'TSLA', currency: 'USD' }, + { token: 'MSFT', currency: 'USD' }, + { token: 'FB', currency: 'USD' } + ], { weightage: 1 }) + await testing.generate(1) + + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '1.5@AAPL', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '2.5@TSLA', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '3.5@MSFT', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '4.5@FB', + currency: 'USD' + }] + }) + await testing.generate(1) + } + + { + const oracleId = await testing.rpc.oracle.appointOracle(await testing.generateAddress(), [ + { token: 'AAPL', currency: 'USD' }, + { token: 'TSLA', currency: 'USD' }, + { token: 'MSFT', currency: 'USD' }, + { token: 'FB', currency: 'USD' } + ], { weightage: 1 }) + await testing.generate(1) + + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '2@AAPL', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '2@TSLA', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '2@MSFT', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, Math.floor(new Date().getTime() / 1000), { + prices: [{ + tokenAmount: '2@FB', + currency: 'USD' + }] + }) + await testing.generate(1) + } + + { + await testing.rpc.loan.setLoanToken({ + symbol: 'AAPL', + fixedIntervalPriceId: 'AAPL/USD', + mintable: false, + interest: new BigNumber(0.01) + }) + await testing.generate(1) + + await testing.rpc.loan.setLoanToken({ + symbol: 'TSLA', + fixedIntervalPriceId: 'TSLA/USD', + mintable: false, + interest: new BigNumber(0.02) + }) + await testing.generate(1) + + await testing.rpc.loan.setLoanToken({ + symbol: 'MSFT', + fixedIntervalPriceId: 'MSFT/USD', + mintable: false, + interest: new BigNumber(0.03) + }) + await testing.generate(1) + + await testing.rpc.loan.setLoanToken({ + symbol: 'FB', + fixedIntervalPriceId: 'FB/USD', + mintable: false, + interest: new BigNumber(0.04) + }) + await testing.generate(1) + } + + { + await testing.generate(12) + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + } +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +describe('list', () => { + it('should listLoanTokens', async () => { + const result = await client.loan.listLoanToken() + expect(result.length).toStrictEqual(4) + expect(result[0]).toStrictEqual({ + tokenId: expect.any(String), + interest: expect.any(String), + fixedIntervalPriceId: expect.any(String), + token: { + collateralAddress: expect.any(String), + creation: { + height: expect.any(Number), + tx: expect.any(String) + }, + decimal: 8, + destruction: { + height: -1, + tx: expect.any(String) + }, + displaySymbol: expect.any(String), + finalized: false, + id: expect.any(String), + isDAT: true, + isLPS: false, + isLoanToken: true, + limit: '0', + mintable: false, + minted: '0', + name: '', + symbol: expect.any(String), + symbolKey: expect.any(String), + tradeable: true + }, + activePrice: { + active: { + amount: expect.any(String), + 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), + isLive: true, + key: expect.any(String), + next: { + amount: expect.any(String), + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + sort: expect.any(String) + } + }) + + expect(result[1].tokenId.length).toStrictEqual(64) + expect(result[2].tokenId.length).toStrictEqual(64) + expect(result[3].tokenId.length).toStrictEqual(64) + }) + + it('should listLoanTokens with pagination', async () => { + const first = await client.loan.listLoanToken(2) + + expect(first.length).toStrictEqual(2) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken?.length).toStrictEqual(64) + + const next = await client.paginate(first) + + expect(next.length).toStrictEqual(2) + expect(next.hasNext).toStrictEqual(true) + expect(next.nextToken?.length).toStrictEqual(64) + + const last = await client.paginate(next) + + expect(last.length).toStrictEqual(0) + expect(last.hasNext).toStrictEqual(false) + expect(last.nextToken).toBeUndefined() + }) +}) + +describe('get', () => { + it('should get loan token by symbol', async () => { + const data = await client.loan.getLoanToken('AAPL') + expect(data).toStrictEqual({ + tokenId: expect.any(String), + fixedIntervalPriceId: 'AAPL/USD', + interest: '0.01', + token: { + collateralAddress: expect.any(String), + creation: { + height: expect.any(Number), + tx: expect.any(String) + }, + decimal: 8, + destruction: { + height: -1, + tx: expect.any(String) + }, + displaySymbol: 'dAAPL', + finalized: false, + id: '1', + isDAT: true, + isLPS: false, + isLoanToken: true, + limit: '0', + mintable: false, + minted: '0', + name: '', + symbol: 'AAPL', + symbolKey: 'AAPL', + tradeable: true + }, + activePrice: { + active: { + amount: '1.75000000', + 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), + isLive: true, + key: 'AAPL-USD', + next: { + amount: '1.75000000', + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + sort: expect.any(String) + } + }) + }) + + it('should fail due to getting non-existent or malformed loan token id', async () => { + expect.assertions(4) + try { + await client.loan.getLoanToken('999') + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find loan token', + url: '/v0.0/regtest/loans/tokens/999' + }) + } + + try { + await client.loan.getLoanToken('$*@') + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find loan token', + url: '/v0.0/regtest/loans/tokens/$*@' + }) + } + }) +}) diff --git a/packages/whale-api-client/__tests__/api/loan.vault.test.ts b/packages/whale-api-client/__tests__/api/loan.vault.test.ts new file mode 100644 index 0000000000..6397469d98 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/loan.vault.test.ts @@ -0,0 +1,504 @@ +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiException } from '../../src' +import BigNumber from 'bignumber.js' +import { Testing } from '@defichain/jellyfish-testing' +import { LoanMasterNodeRegTestContainer } from '@defichain/testcontainers' +import { LoanVaultState } from '../../src/api/Loan' + +const container = new LoanMasterNodeRegTestContainer() +const service = new StubService(container) +const client = new StubWhaleApiClient(service) +const testing = Testing.create(container) + +let johnEmptyVaultId: string +let bobDepositedVaultId: string +let johnLoanedVaultId: string +let adamLiquidatedVaultId: string + +/* eslint-disable no-lone-blocks */ + +beforeAll(async () => { + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + { // DFI setup + await testing.token.dfi({ + address: await testing.address('DFI'), + amount: 40000 + }) + } + + { // Loan Scheme + await testing.rpc.loan.createLoanScheme({ + id: 'default', + minColRatio: 100, + interestRate: new BigNumber(1) + }) + await testing.generate(1) + + await testing.rpc.loan.createLoanScheme({ + id: 'scheme', + minColRatio: 110, + interestRate: new BigNumber(1) + }) + await testing.generate(1) + } + + let oracleId: string + { // Oracle 1 + const oracleAddress = await testing.generateAddress() + const priceFeeds = [ + { token: 'DFI', currency: 'USD' }, + { token: 'TSLA', currency: 'USD' }, + { token: 'AAPL', currency: 'USD' }, + { token: 'GOOGL', currency: 'USD' } + ] + oracleId = await testing.rpc.oracle.appointOracle(oracleAddress, priceFeeds, { weightage: 1 }) + await testing.generate(1) + + const timestamp = Math.floor(new Date().getTime() / 1000) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ tokenAmount: '1@DFI', currency: 'USD' }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ tokenAmount: '2@TSLA', currency: 'USD' }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ tokenAmount: '2@AAPL', currency: 'USD' }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ tokenAmount: '4@GOOGL', currency: 'USD' }] + }) + await testing.generate(1) + } + + { // Oracle 2 + const priceFeeds = [ + { token: 'DFI', currency: 'USD' }, + { token: 'TSLA', currency: 'USD' }, + { token: 'AAPL', currency: 'USD' }, + { token: 'GOOGL', currency: 'USD' } + ] + const oracleId = await testing.rpc.oracle.appointOracle(await testing.generateAddress(), priceFeeds, { weightage: 1 }) + await testing.generate(1) + + const timestamp = Math.floor(new Date().getTime() / 1000) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ tokenAmount: '1@DFI', currency: 'USD' }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ tokenAmount: '2@TSLA', currency: 'USD' }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ tokenAmount: '2@AAPL', currency: 'USD' }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ tokenAmount: '4@GOOGL', currency: 'USD' }] + }) + await testing.generate(1) + } + + { // Collateral Tokens + await testing.rpc.loan.setCollateralToken({ + token: 'DFI', + factor: new BigNumber(1), + fixedIntervalPriceId: 'DFI/USD' + }) + } + + { // Loan Tokens + await testing.rpc.loan.setLoanToken({ + symbol: 'TSLA', + fixedIntervalPriceId: 'TSLA/USD' + }) + await testing.generate(1) + + await testing.rpc.loan.setLoanToken({ + symbol: 'AAPL', + fixedIntervalPriceId: 'AAPL/USD' + }) + await testing.generate(1) + + await testing.rpc.loan.setLoanToken({ + symbol: 'GOOGL', + fixedIntervalPriceId: 'GOOGL/USD' + }) + await testing.generate(1) + } + + { // Vault Empty (John) + johnEmptyVaultId = await testing.rpc.loan.createVault({ + ownerAddress: await testing.address('John'), + loanSchemeId: 'default' + }) + await testing.generate(1) + } + + { // Vault Deposit Collateral (Bob) + bobDepositedVaultId = await testing.rpc.loan.createVault({ + ownerAddress: await testing.address('Bob'), + loanSchemeId: 'default' + }) + await testing.generate(1) + await testing.rpc.loan.depositToVault({ + vaultId: bobDepositedVaultId, + from: await testing.address('DFI'), + amount: '10000@DFI' + }) + await testing.generate(1) + } + + { // Vault Deposited & Loaned (John) + johnLoanedVaultId = await testing.rpc.loan.createVault({ + ownerAddress: await testing.address('John'), + loanSchemeId: 'scheme' + }) + await testing.generate(1) + await testing.rpc.loan.depositToVault({ + vaultId: johnLoanedVaultId, + from: await testing.address('DFI'), + amount: '10000@DFI' + }) + await testing.generate(1) + await testing.rpc.loan.takeLoan({ + vaultId: johnLoanedVaultId, + amounts: '30@TSLA' + }) + await testing.generate(1) + } + + { // Vault Deposited, Loaned, Liquidated (Adam) + adamLiquidatedVaultId = await testing.rpc.loan.createVault({ + ownerAddress: await testing.address('Adam'), + loanSchemeId: 'default' + }) + await testing.generate(1) + await testing.rpc.loan.depositToVault({ + vaultId: adamLiquidatedVaultId, + from: await testing.address('DFI'), + amount: '10000@DFI' + }) + await testing.generate(1) + await testing.rpc.loan.takeLoan({ + vaultId: adamLiquidatedVaultId, + amounts: '30@AAPL' + }) + await testing.generate(1) + + // Make vault enter under liquidation state by a price hike of the loan token + const timestamp2 = Math.floor(new Date().getTime() / 1000) + await testing.rpc.oracle.setOracleData(oracleId, timestamp2, { + prices: [{ tokenAmount: '1000@AAPL', currency: 'USD' }] + }) + + // Wait for 12 blocks which are equivalent to 2 hours (1 block = 10 minutes in regtest) in order to liquidate the vault + await testing.generate(12) + } + + { + const height = await container.getBlockCount() + await service.waitForIndexedHeight(height - 1) + } +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +describe('list', () => { + it('should listVault with size only', async () => { + const result = await client.loan.listVault(20) + expect(result.length).toStrictEqual(4) + result.forEach(e => + expect(e).toStrictEqual(expect.objectContaining({ + vaultId: expect.any(String), + loanScheme: { + id: expect.any(String), + interestRate: expect.any(String), + minColRatio: expect.any(String) + }, + ownerAddress: expect.any(String), + state: expect.any(String) + })) + ) + }) + + it('should listVault with size and pagination', async () => { + const vaultIds = (await client.loan.listVault()) + .map(value => value.vaultId) + + const first = await client.loan.listVault(2) + + expect(first.length).toStrictEqual(2) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken).toStrictEqual(vaultIds[1]) + + expect(first[0].vaultId).toStrictEqual(vaultIds[0]) + expect(first[1].vaultId).toStrictEqual(vaultIds[1]) + + const next = await client.paginate(first) + + expect(next.length).toStrictEqual(2) + expect(next.hasNext).toStrictEqual(true) + expect(next.nextToken).toStrictEqual(vaultIds[3]) + + expect(next[0].vaultId).toStrictEqual(vaultIds[2]) + expect(next[1].vaultId).toStrictEqual(vaultIds[3]) + + const last = await client.paginate(next) + + expect(last.length).toStrictEqual(0) + expect(last.hasNext).toStrictEqual(false) + expect(last.nextToken).toBeUndefined() + }) +}) + +describe('get', () => { + it('should get johnEmptyVaultId', async () => { + const vault = await client.loan.getVault(johnEmptyVaultId) + expect(vault).toStrictEqual({ + vaultId: johnEmptyVaultId, + loanScheme: { + id: 'default', + interestRate: '1', + minColRatio: '100' + }, + ownerAddress: expect.any(String), + state: LoanVaultState.ACTIVE, + informativeRatio: '-1', + collateralRatio: '-1', + collateralValue: '0', + loanValue: '0', + interestValue: '0', + collateralAmounts: [], + loanAmounts: [], + interestAmounts: [] + }) + }) + + it('should get bobDepositedVaultId', async () => { + const vault = await client.loan.getVault(bobDepositedVaultId) + expect(vault).toStrictEqual({ + vaultId: bobDepositedVaultId, + loanScheme: { + id: 'default', + interestRate: '1', + minColRatio: '100' + }, + ownerAddress: expect.any(String), + state: LoanVaultState.ACTIVE, + informativeRatio: '-1', + collateralRatio: '-1', + collateralValue: '10000', + loanValue: '0', + interestValue: '0', + collateralAmounts: [ + { + amount: '10000.00000000', + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI', + symbolKey: 'DFI', + activePrice: { + active: expect.any(Object), + block: expect.any(Object), + id: expect.any(String), + isLive: expect.any(Boolean), + key: 'DFI-USD', + next: expect.any(Object), + sort: expect.any(String) + } + } + ], + loanAmounts: [], + interestAmounts: [] + }) + }) + + it('should get johnLoanedVaultId', async () => { + const vault = await client.loan.getVault(johnLoanedVaultId) + expect(vault).toStrictEqual({ + vaultId: johnLoanedVaultId, + loanScheme: { + id: 'scheme', + interestRate: '1', + minColRatio: '110' + }, + ownerAddress: expect.any(String), + state: LoanVaultState.ACTIVE, + collateralRatio: '16667', + collateralValue: '10000', + informativeRatio: '16666.61592793', + loanValue: '60.00018266', + interestValue: '0.00018266', + collateralAmounts: [ + { + amount: '10000.00000000', + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI', + symbolKey: 'DFI', + activePrice: { + active: expect.any(Object), + block: expect.any(Object), + id: expect.any(String), + isLive: expect.any(Boolean), + key: 'DFI-USD', + next: expect.any(Object), + sort: expect.any(String) + } + } + ], + loanAmounts: [ + { + amount: '30.00009133', + displaySymbol: 'dTSLA', + id: '1', + name: '', + symbol: 'TSLA', + symbolKey: 'TSLA', + activePrice: { + active: expect.any(Object), + block: expect.any(Object), + id: expect.any(String), + isLive: expect.any(Boolean), + key: 'TSLA-USD', + next: expect.any(Object), + sort: expect.any(String) + } + } + ], + interestAmounts: [ + { + amount: '0.00009133', + displaySymbol: 'dTSLA', + id: '1', + name: '', + symbol: 'TSLA', + symbolKey: 'TSLA', + activePrice: { + active: expect.any(Object), + block: expect.any(Object), + id: expect.any(String), + isLive: expect.any(Boolean), + key: 'TSLA-USD', + next: expect.any(Object), + sort: expect.any(String) + } + } + ] + }) + }) + + it('should get adamLiquidatedVaultId', async () => { + const vault = await client.loan.getVault(adamLiquidatedVaultId) + expect(vault).toStrictEqual({ + vaultId: adamLiquidatedVaultId, + loanScheme: { + id: 'default', + interestRate: '1', + minColRatio: '100' + }, + ownerAddress: expect.any(String), + state: LoanVaultState.IN_LIQUIDATION, + batchCount: 1, + liquidationHeight: 162, + liquidationPenalty: 5, + batches: [ + { + index: 0, + froms: [], + collaterals: [ + { + amount: '10000.00000000', + displaySymbol: 'DFI', + id: '0', + name: 'Default Defi token', + symbol: 'DFI', + symbolKey: 'DFI', + activePrice: { + active: expect.any(Object), + block: expect.any(Object), + id: expect.any(String), + isLive: expect.any(Boolean), + key: 'DFI-USD', + next: expect.any(Object), + sort: expect.any(String) + } + } + ], + loan: { + amount: expect.any(String), + displaySymbol: 'dAAPL', + id: '2', + name: '', + symbol: 'AAPL', + symbolKey: 'AAPL', + activePrice: { + active: expect.any(Object), + block: expect.any(Object), + id: expect.any(String), + isLive: expect.any(Boolean), + key: 'AAPL-USD', + next: expect.any(Object), + sort: expect.any(String) + } + } + } + ] + }) + }) + + it('should fail due to getting non-existent vault', async () => { + expect.assertions(4) + try { + await client.loan.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.0/regtest/loans/vaults/0530ab29a9f09416a014a4219f186f1d5d530e9a270a9f941275b3972b43ebb7' + }) + } + + try { + await client.loan.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.0/regtest/loans/vaults/999' + }) + } + }) + + it('should fail due to id is malformed', async () => { + expect.assertions(2) + try { + await client.loan.getVault('$*@') + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + at: expect.any(Number), + code: 400, + message: "RpcApiError: 'vaultId must be of length 64 (not 3, for '$*@')', code: -8, method: getvault", + type: 'BadRequest', + url: '/v0.0/regtest/loans/vaults/$*@' + }) + } + }) +}) diff --git a/packages/whale-api-client/__tests__/api/masternodes.test.ts b/packages/whale-api-client/__tests__/api/masternodes.test.ts new file mode 100644 index 0000000000..8b4b68b504 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/masternodes.test.ts @@ -0,0 +1,104 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiClient, WhaleApiException } from '../../src' + +let container: MasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient + +beforeAll(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + + await container.start() + await service.start() + + await container.generate(1) + const height: number = (await client.rpc.call('getblockcount', [], 'number')) + await container.generate(1) + await service.waitForIndexedHeight(height) +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +describe('list', () => { + it('should list masternodes', async () => { + const data = await client.masternodes.list() + expect(Object.keys(data[0]).length).toStrictEqual(8) + expect(data.hasNext).toStrictEqual(false) + expect(data.nextToken).toStrictEqual(undefined) + + expect(data[0]).toStrictEqual({ + id: 'e86c027861cc0af423313f4152a44a83296a388eb51bf1a6dde9bd75bed55fb4', + sort: '00000000e86c027861cc0af423313f4152a44a83296a388eb51bf1a6dde9bd75bed55fb4', + state: 'ENABLED', + mintedBlocks: expect.any(Number), + owner: { address: 'mwsZw8nF7pKxWH8eoKL9tPxTpaFkz7QeLU' }, + operator: { address: 'mswsMVsyGMj1FzDMbbxw2QW3KvQAv2FKiy' }, + creation: { height: 0 }, + timelock: 0 + }) + }) + + it('should list masternodes with pagination', async () => { + const first = await client.masternodes.list(4) + expect(first.length).toStrictEqual(4) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken).toStrictEqual(`00000000${first[3].id}`) + + const next = await client.paginate(first) + expect(next.length).toStrictEqual(4) + expect(next.hasNext).toStrictEqual(true) + expect(next.nextToken).toStrictEqual(`00000000${next[3].id}`) + + const last = await client.paginate(next) + expect(last.length).toStrictEqual(0) + expect(last.hasNext).toStrictEqual(false) + expect(last.nextToken).toStrictEqual(undefined) + }) +}) + +describe('get', () => { + it('should get masternode', async () => { + // get a masternode from list + const masternode = (await client.masternodes.list(1))[0] + + const data = await client.masternodes.get(masternode.id) + expect(Object.keys(data).length).toStrictEqual(8) + expect(data).toStrictEqual({ + id: masternode.id, + sort: expect.any(String), + state: masternode.state, + mintedBlocks: expect.any(Number), + owner: { address: masternode.owner.address }, + operator: { address: masternode.operator.address }, + creation: { height: masternode.creation.height }, + timelock: 0 + }) + }) + + it('should fail due to non-existent masternode', async () => { + expect.assertions(2) + const id = '8d4d987dee688e400a0cdc899386f243250d3656d802231755ab4d28178c9816' + try { + await client.masternodes.get(id) + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find masternode', + url: `/v0.0/regtest/masternodes/${id}` + }) + } + }) +}) diff --git a/packages/whale-api-client/__tests__/api/oracles.test.ts b/packages/whale-api-client/__tests__/api/oracles.test.ts new file mode 100644 index 0000000000..947360da85 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/oracles.test.ts @@ -0,0 +1,231 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { StubService } from '../stub.service' +import { WhaleApiClient } from '../../src' +import { StubWhaleApiClient } from '../stub.client' +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' + +let container: MasterNodeRegTestContainer +let service: StubService +let rpcClient: JsonRpcClient +let apiClient: WhaleApiClient + +beforeAll(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + apiClient = new StubWhaleApiClient(service) + + await container.start() + await container.waitForReady() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + rpcClient = new JsonRpcClient(await container.getCachedRpcUrl()) +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +describe('oracles', () => { + 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 () => { + for (const setup of Object.values(oracles)) { + setup.address = await container.getNewAddress() + setup.id = await rpcClient.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 rpcClient.oracle.setOracleData(setup.id, timestamp, { + prices: price + }) + await container.generate(1) + } + } + + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + }) + + it('should list', async () => { + const oracles = await apiClient.oracles.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 feed = await apiClient.oracles.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 feed = await apiClient.oracles.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 feed = await apiClient.oracles.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 oracles = await apiClient.oracles.list() + + for (const oracle of oracles) { + const toCompare = await apiClient.oracles.getOracleByAddress(oracle.ownerAddress) + expect(toCompare).toStrictEqual(oracle) + } + }) +}) diff --git a/packages/whale-api-client/__tests__/api/poolpairs.swaps.test.ts b/packages/whale-api-client/__tests__/api/poolpairs.swaps.test.ts new file mode 100644 index 0000000000..29f355dc89 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/poolpairs.swaps.test.ts @@ -0,0 +1,467 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { StubService } from '../stub.service' +import { StubWhaleApiClient } from '../stub.client' +import { Testing } from '@defichain/jellyfish-testing' +import { ApiPagedResponse, WhaleApiClient } from '../../src' +import { PoolSwapData } from '../../src/api/PoolPairs' + +let container: MasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient +let testing: Testing + +beforeEach(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + testing = Testing.create(container) + + await testing.container.start() + await testing.container.waitForWalletCoinbaseMaturity() + await service.start() + + const tokens = ['A', 'B', 'C'] + + await testing.token.dfi({ address: await testing.address('swap'), amount: 10000 }) + await container.generate(1) + + for (const token of tokens) { + await container.waitForWalletBalanceGTE(110) + await testing.token.create({ symbol: token }) + await container.generate(1) + await testing.token.mint({ amount: 10000, symbol: token }) + await container.generate(1) + await testing.token.send({ address: await testing.address('swap'), symbol: token, amount: 1000 }) + } + + await testing.poolpair.create({ tokenA: 'A', tokenB: 'B' }) + await container.generate(1) + await testing.poolpair.add({ a: { symbol: 'A', amount: 100 }, b: { symbol: 'B', amount: 200 } }) + + await testing.poolpair.create({ tokenA: 'C', tokenB: 'B' }) + await container.generate(1) + await testing.poolpair.add({ a: { symbol: 'C', amount: 100 }, b: { symbol: 'B', amount: 200 } }) + + await testing.poolpair.create({ tokenA: 'DFI', tokenB: 'C' }) + await container.generate(1) + await testing.poolpair.add({ a: { symbol: 'DFI', amount: 100 }, b: { symbol: 'C', amount: 200 } }) + await testing.generate(1) +}) + +afterEach(async () => { + try { + await service.stop() + } finally { + await testing.container.stop() + } +}) + +describe('poolswap buy-sell indicator', () => { + it('should get pool swap details', async () => { + await testing.rpc.poolpair.poolSwap({ + from: await testing.address('swap'), + tokenFrom: 'C', + amountFrom: 15, + to: await testing.address('swap'), + tokenTo: 'DFI' + }) + + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + + const verbose: ApiPagedResponse = await client.poolpairs.listPoolSwapsVerbose('6') + expect(verbose.hasNext).toStrictEqual(false) + expect([...verbose]).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 testing.rpc.poolpair.compositeSwap({ + from: await testing.address('swap'), + tokenFrom: 'B', + amountFrom: 10, + to: await testing.address('swap'), + tokenTo: 'DFI' + }) + + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + + const verbose5: ApiPagedResponse = await client.poolpairs.listPoolSwapsVerbose('5') + expect(verbose5.hasNext).toStrictEqual(false) + expect([...verbose5]).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 verbose6: ApiPagedResponse = await client.poolpairs.listPoolSwapsVerbose('6') + expect(verbose6.hasNext).toStrictEqual(false) + expect([...verbose6]).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 testing.rpc.poolpair.compositeSwap({ + from: await testing.address('swap'), + tokenFrom: 'DFI', + amountFrom: 5, + to: await testing.address('swap'), + tokenTo: 'B' + }) + + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + + const verbose5: ApiPagedResponse = await client.poolpairs.listPoolSwapsVerbose('5') + expect(verbose5.hasNext).toStrictEqual(false) + expect([...verbose5]).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 verbose6: ApiPagedResponse = await client.poolpairs.listPoolSwapsVerbose('6') + expect(verbose6.hasNext).toStrictEqual(false) + expect([...verbose6]).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 testing.rpc.poolpair.compositeSwap({ + from: await testing.address('swap'), + tokenFrom: 'A', + amountFrom: 20, + to: await testing.address('swap'), + tokenTo: 'DFI' + }) + + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + + const verbose4: ApiPagedResponse = await client.poolpairs.listPoolSwapsVerbose('4') + expect(verbose4.hasNext).toStrictEqual(false) + expect([...verbose4]).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 verbose5: ApiPagedResponse = await client.poolpairs.listPoolSwapsVerbose('5') + expect(verbose5.hasNext).toStrictEqual(false) + expect([...verbose5]).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 verbose6: ApiPagedResponse = await client.poolpairs.listPoolSwapsVerbose('6') + expect(verbose6.hasNext).toStrictEqual(false) + expect([...verbose6]).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 testing.rpc.poolpair.compositeSwap({ + from: await testing.address('swap'), + tokenFrom: 'C', + amountFrom: 10, + to: await testing.address('swap'), + tokenTo: 'DFI' + }) + + await testing.rpc.poolpair.compositeSwap({ + from: await testing.address('swap'), + tokenFrom: 'A', + amountFrom: 10, + to: await testing.address('swap'), + tokenTo: 'B' + }) + + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + + const verbose6: ApiPagedResponse = await client.poolpairs.listPoolSwapsVerbose('6') + expect(verbose6.hasNext).toStrictEqual(false) + expect([...verbose6]).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 verbose4: ApiPagedResponse = await client.poolpairs.listPoolSwapsVerbose('4') + expect(verbose4.hasNext).toStrictEqual(false) + expect([...verbose4]).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/packages/whale-api-client/__tests__/api/poolpairs.test.ts b/packages/whale-api-client/__tests__/api/poolpairs.test.ts new file mode 100644 index 0000000000..8b176308c8 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/poolpairs.test.ts @@ -0,0 +1,1067 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { ApiPagedResponse, WhaleApiClient, WhaleApiException } from '../../src' +import { addPoolLiquidity, createPoolPair, createToken, getNewAddress, mintTokens, poolSwap } from '@defichain/testing' +import { PoolPairData, PoolSwapAggregatedData, PoolSwapAggregatedInterval, PoolSwapData } from '../../src/api/PoolPairs' +import { Testing } from '@defichain/jellyfish-testing' +import waitForExpect from 'wait-for-expect' + +let container: MasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient +let testing: Testing + +beforeEach(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + testing = Testing.create(container) + + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + await setup() +}) + +afterEach(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +async function setup (): Promise { + const tokens = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] + + for (const token of tokens) { + await container.waitForWalletBalanceGTE(110) + await createToken(container, token, { + collateralAddress: await testing.address('swap') + }) + await mintTokens(container, token, { + mintAmount: 10000 + }) + } + await createPoolPair(container, 'A', 'DFI') + await createPoolPair(container, 'B', 'DFI') + await createPoolPair(container, 'C', 'DFI') + await createPoolPair(container, 'D', 'DFI') + await createPoolPair(container, 'E', 'DFI') + await createPoolPair(container, 'F', 'DFI') + await createPoolPair(container, 'G', 'DFI') + await createPoolPair(container, 'H', 'DFI') + + await addPoolLiquidity(container, { + tokenA: 'A', + amountA: 100, + tokenB: 'DFI', + amountB: 200, + shareAddress: await getNewAddress(container) + }) + await addPoolLiquidity(container, { + tokenA: 'B', + amountA: 50, + tokenB: 'DFI', + amountB: 300, + shareAddress: await getNewAddress(container) + }) + await addPoolLiquidity(container, { + tokenA: 'C', + amountA: 90, + tokenB: 'DFI', + amountB: 360, + shareAddress: await getNewAddress(container) + }) + + // dexUsdtDfi setup + await createToken(container, 'USDT') + await createPoolPair(container, 'USDT', 'DFI') + await mintTokens(container, 'USDT') + await addPoolLiquidity(container, { + tokenA: 'USDT', + amountA: 1000, + tokenB: 'DFI', + amountB: 431.51288, + shareAddress: await getNewAddress(container) + }) + + await createToken(container, 'USDC') + await createPoolPair(container, 'USDC', 'H') + await mintTokens(container, 'USDC') + await addPoolLiquidity(container, { + tokenA: 'USDC', + amountA: 500, + tokenB: 'H', + amountB: 31.51288, + shareAddress: await getNewAddress(container) + }) + + await createToken(container, 'DUSD') + await createToken(container, 'TEST', { + collateralAddress: await testing.address('swap') + }) + await createPoolPair(container, 'TEST', 'DUSD', { + commission: 0.002 + }) + await mintTokens(container, 'DUSD') + await mintTokens(container, 'TEST') + await addPoolLiquidity(container, { + tokenA: 'TEST', + amountA: 20, + tokenB: 'DUSD', + amountB: 100, + shareAddress: await getNewAddress(container) + }) + + await testing.token.dfi({ + address: await testing.address('swap'), + amount: 20 + }) + + await createToken(container, 'BURN') + await createPoolPair(container, 'BURN', 'DFI', { status: false }) + await mintTokens(container, 'BURN', { mintAmount: 1 }) + await addPoolLiquidity(container, { + tokenA: 'BURN', + amountA: 1, + tokenB: 'DFI', + amountB: 1, + shareAddress: await getNewAddress(container) + }) +} + +describe('poolpair info', () => { + it('should list', async () => { + const response: ApiPagedResponse = await client.poolpairs.list(30) + + expect(response.length).toStrictEqual(11) + expect(response.hasNext).toStrictEqual(false) + + expect(response[1]).toStrictEqual({ + id: '10', + symbol: 'B-DFI', + displaySymbol: 'dB-DFI', + name: 'B-Default Defi token', + status: true, + tokenA: { + id: '2', + symbol: 'B', + reserve: '50', + blockCommission: '0', + displaySymbol: 'dB' + }, + tokenB: { + id: '0', + symbol: 'DFI', + reserve: '300', + blockCommission: '0', + displaySymbol: 'DFI' + }, + apr: { + reward: 0, + total: 0, + commission: 0 + }, + commission: '0', + totalLiquidity: { + token: '122.47448713', + usd: '1390.4567576291117892' + }, + tradeEnabled: true, + ownerAddress: expect.any(String), + priceRatio: { + ab: '0.16666666', + ba: '6' + }, + rewardPct: '0', + creation: { + tx: expect.any(String), + height: expect.any(Number) + }, + volume: { + d30: 0, + h24: 0 + } + }) + }) + + it('should list with pagination', async () => { + const first = await client.poolpairs.list(5) + expect(first.length).toStrictEqual(5) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken).toStrictEqual('13') + + expect(first[0].symbol).toStrictEqual('A-DFI') + expect(first[1].symbol).toStrictEqual('B-DFI') + expect(first[2].symbol).toStrictEqual('C-DFI') + expect(first[3].symbol).toStrictEqual('D-DFI') + expect(first[4].symbol).toStrictEqual('E-DFI') + + const next = await client.paginate(first) + expect(next.length).toStrictEqual(5) + expect(next.hasNext).toStrictEqual(true) + expect(next.nextToken).toStrictEqual('20') + + expect(next[0].symbol).toStrictEqual('F-DFI') + expect(next[1].symbol).toStrictEqual('G-DFI') + expect(next[2].symbol).toStrictEqual('H-DFI') + expect(next[3].symbol).toStrictEqual('USDT-DFI') + expect(next[4].symbol).toStrictEqual('USDC-H') + + const last = await client.paginate(next) + expect(last.length).toStrictEqual(1) + expect(last.hasNext).toStrictEqual(false) + expect(last.nextToken).toBeUndefined() + + expect(last[0].symbol).toStrictEqual('TEST-DUSD') + }) + + it('should get 9', async () => { + const response: PoolPairData = await client.poolpairs.get('9') + + expect(response).toStrictEqual({ + id: '9', + symbol: 'A-DFI', + displaySymbol: 'dA-DFI', + name: 'A-Default Defi token', + status: true, + tokenA: { + id: expect.any(String), + symbol: 'A', + reserve: '100', + blockCommission: '0', + displaySymbol: 'dA' + }, + tokenB: { + id: '0', + symbol: 'DFI', + reserve: '200', + blockCommission: '0', + displaySymbol: 'DFI' + }, + apr: { + reward: 0, + total: 0, + commission: 0 + }, + commission: '0', + totalLiquidity: { + token: '141.42135623', + usd: '926.9711717527411928' + }, + tradeEnabled: true, + ownerAddress: expect.any(String), + priceRatio: { + ab: '0.5', + ba: '2' + }, + rewardPct: '0', + creation: { + tx: expect.any(String), + height: expect.any(Number) + }, + volume: { + d30: 0, + h24: 0 + } + }) + }) + + it('should get 20', async () => { + const response: PoolPairData = await client.poolpairs.get('20') + + expect(response).toStrictEqual({ + id: '20', + symbol: 'USDC-H', + name: 'USDC-H', + displaySymbol: 'dUSDC-dH', + status: true, + tokenA: { + id: expect.any(String), + symbol: 'USDC', + reserve: '500', + blockCommission: '0', + displaySymbol: 'dUSDC' + }, + tokenB: { + id: '8', + symbol: 'H', + reserve: '31.51288', + blockCommission: '0', + displaySymbol: 'dH' + }, + apr: { + reward: 0, + total: 0, + commission: 0 + }, + commission: '0', + totalLiquidity: { + token: '125.52465893', + usd: '1000' + }, + tradeEnabled: true, + ownerAddress: expect.any(String), + priceRatio: { + ab: '15.86652822', + ba: '0.06302576' + }, + rewardPct: '0', + creation: { + tx: expect.any(String), + height: expect.any(Number) + }, + volume: { + d30: 0, + h24: 0 + } + }) + }) + + it('should throw error as numeric string is expected', async () => { + expect.assertions(2) + try { + await client.poolpairs.get('A-DFI') + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + message: 'Validation failed (numeric string is expected)', + url: '/v0.0/regtest/poolpairs/A-DFI' + }) + } + }) + + it('should throw error while getting non-existent poolpair', async () => { + expect.assertions(2) + try { + await client.poolpairs.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.0/regtest/poolpairs/999' + }) + } + }) +}) + +describe('poolswap', () => { + it('should show volume and swaps', async () => { + await poolSwap(container, { + from: await testing.address('swap'), + tokenFrom: 'A', + amountFrom: 25, + to: await testing.address('swap'), + tokenTo: 'DFI' + }) + + await poolSwap(container, { + from: await testing.address('swap'), + tokenFrom: 'A', + amountFrom: 50, + to: await testing.address('swap'), + tokenTo: 'DFI' + }) + + await poolSwap(container, { + from: await testing.address('swap'), + tokenFrom: 'TEST', + amountFrom: 10, + to: await testing.address('swap'), + tokenTo: 'DUSD' + }) + + await testing.rpc.poolpair.compositeSwap({ + from: await testing.address('swap'), + tokenFrom: 'A', + amountFrom: 123, + to: await testing.address('swap'), + tokenTo: 'C' + }) + + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + + const response: ApiPagedResponse = await client.poolpairs.listPoolSwaps('9') + expect(response.hasNext).toStrictEqual(false) + expect([...response]).toStrictEqual([ + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '9', + sort: expect.any(String), + fromAmount: '123.00000000', + fromTokenId: 1, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + } + }, + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '9', + sort: expect.any(String), + fromAmount: '50.00000000', + fromTokenId: 1, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + } + }, + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '9', + sort: expect.any(String), + fromAmount: '25.00000000', + fromTokenId: 1, + block: { + hash: expect.stringMatching(/[0-f]{64}/), + height: expect.any(Number), + time: expect.any(Number), + medianTime: expect.any(Number) + } + } + ]) + + const verbose: ApiPagedResponse = await client.poolpairs.listPoolSwapsVerbose('9') + expect(verbose.hasNext).toStrictEqual(false) + expect([...verbose]).toStrictEqual([ + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '9', + sort: expect.any(String), + fromAmount: '123.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: '123.00000000', + displaySymbol: 'dA' + }, + to: { + address: expect.any(String), + amount: '10.42667420', + symbol: 'C', + displaySymbol: 'dC' + }, + type: 'SELL' + }, + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '9', + sort: expect.any(String), + fromAmount: '50.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: '50.00000000', + displaySymbol: 'dA' + }, + to: { + address: expect.any(String), + amount: '45.71428571', + symbol: 'DFI', + displaySymbol: 'DFI' + }, + type: 'SELL' + }, + { + id: expect.any(String), + txid: expect.stringMatching(/[0-f]{64}/), + txno: expect.any(Number), + poolPairId: '9', + sort: expect.any(String), + fromAmount: '25.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: '25.00000000', + displaySymbol: 'dA' + }, + to: { + address: expect.any(String), + amount: '39.99999999', + symbol: 'DFI', + displaySymbol: 'DFI' + }, + type: 'SELL' + } + ]) + + const poolPair: PoolPairData = await client.poolpairs.get('9') + expect(poolPair).toStrictEqual({ + id: '9', + symbol: 'A-DFI', + displaySymbol: 'dA-DFI', + name: 'A-Default Defi token', + status: true, + tokenA: { + id: expect.any(String), + symbol: 'A', + reserve: '298', + blockCommission: '0', + displaySymbol: 'dA' + }, + tokenB: { + id: '0', + symbol: 'DFI', + reserve: '67.11409397', + blockCommission: '0', + displaySymbol: 'DFI' + }, + apr: { + reward: 0, + total: 0, + commission: 0 + }, + commission: '0', + totalLiquidity: { + token: '141.42135623', + usd: '311.06415164247241009334543708' + }, + tradeEnabled: true, + ownerAddress: expect.any(String), + priceRatio: { + ab: '4.44019999', + ba: '0.22521508' + }, + rewardPct: '0', + creation: { + tx: expect.any(String), + height: expect.any(Number) + }, + volume: { + d30: 103.34010406914354, + h24: 103.34010406914354 + } + }) + + const dusdPoolPair: PoolPairData = await client.poolpairs.get('23') + expect(dusdPoolPair).toStrictEqual({ + id: '23', + symbol: 'TEST-DUSD', + displaySymbol: 'dTEST-DUSD', + name: 'TEST-DUSD', + status: true, + tokenA: { + id: expect.any(String), + symbol: 'TEST', + reserve: '29.98', + blockCommission: '0', + displaySymbol: 'dTEST' + }, + tokenB: { + id: expect.any(String), + symbol: 'DUSD', + reserve: '66.71114077', + blockCommission: '0', + displaySymbol: 'DUSD' + }, + apr: { + reward: 0, + total: 0.12174783188792529, + commission: 0.12174783188792529 + }, + commission: '0.002', + totalLiquidity: { + token: '44.72135954', + usd: '133.42228154' + }, + tradeEnabled: true, + ownerAddress: expect.any(String), + priceRatio: { + ab: '0.44940019', + ba: '2.22518815' + }, + rewardPct: '0', + creation: { + tx: expect.any(String), + height: expect.any(Number) + }, + volume: { + d30: 22.25188151100734, + h24: 22.25188151100734 + } + }) + }) +}) + +describe('poolswap 24h', () => { + it('should show volume and swaps for 24h', async () => { + await testing.generate(1) + + { + const oneHour = 60 * 60 + 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) + for (let i = 0; i <= 24; i++) { + const mockTime = timeNow + i * oneHour + await testing.rpc.misc.setMockTime(mockTime) + + await testing.poolpair.swap({ + from: await testing.address('swap'), + tokenFrom: 'A', + amountFrom: 0.1, + to: await testing.address('swap'), + tokenTo: 'DFI' + }) + + await testing.generate(1) + } + + const height = await container.getBlockCount() + await testing.generate(1) + await service.waitForIndexedHeight(height) + await testing.generate(1) + } + + const poolPair: PoolPairData = await client.poolpairs.get('9') + expect(poolPair).toStrictEqual({ + id: '9', + symbol: 'A-DFI', + displaySymbol: 'dA-DFI', + name: 'A-Default Defi token', + status: true, + tokenA: { + id: expect.any(String), + symbol: 'A', + reserve: '102.5', + blockCommission: '0', + displaySymbol: 'dA' + }, + tokenB: { + id: '0', + symbol: 'DFI', + reserve: '195.12195134', + blockCommission: '0', + displaySymbol: 'DFI' + }, + apr: { + reward: 0, + total: 0, + commission: 0 + }, + commission: '0', + totalLiquidity: { + token: '141.42135623', + usd: '904.36211934160574766567579176' + }, + tradeEnabled: true, + ownerAddress: expect.any(String), + priceRatio: { + ab: '0.52531249', + ba: '1.90362879' + }, + rewardPct: '0', + creation: { + tx: expect.any(String), + height: expect.any(Number) + }, + volume: { + d30: 11.028806333434215, + h24: 10.146501826759481 + } + }) + }) +}) + +describe('poolswap aggregated', () => { + 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 testing.rpc.misc.setMockTime(timeNow) + await testing.generate(10) + + for (let i = 0; i <= numBlocks; i++) { + const mockTime = timeNow + i * fiveMinutes + await testing.rpc.misc.setMockTime(mockTime) + + await testing.rpc.poolpair.poolSwap({ + from: await testing.address('swap'), + tokenFrom: 'B', + amountFrom: 0.1, + to: await testing.address('swap'), + tokenTo: 'DFI' + }) + + await testing.generate(1) + } + + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + } + + const dayAggregated: ApiPagedResponse = await client.poolpairs.listPoolSwapAggregates('10', PoolSwapAggregatedInterval.ONE_DAY, 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: '10-86400' + }, + { + aggregated: { + amounts: { + 2: '29.00000000' + }, + usd: 128.7090118954775 + }, + block: expect.any(Object), + bucket: expect.any(Number), + id: expect.any(String), + key: '10-86400' + }, + { + aggregated: { + amounts: {}, + usd: 0 + }, + block: expect.any(Object), + bucket: expect.any(Number), + id: expect.any(String), + key: '10-86400' + } + + ]) + + const hourAggregated: ApiPagedResponse = await client.poolpairs.listPoolSwapAggregates('10', PoolSwapAggregatedInterval.ONE_HOUR, 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: '10-3600' + }, + { + aggregated: { + amounts: { 2: '1.20000000' }, + usd: 5.325890147399068 + }, + block: expect.any(Object), + bucket: expect.any(Number), + id: expect.any(String), + key: '10-3600' + }, + { + aggregated: { + amounts: { 2: '1.20000000' }, + usd: 5.325890147399068 + }, + block: expect.any(Object), + bucket: expect.any(Number), + id: expect.any(String), + key: '10-3600' + } + ]) + }) +}) + +describe('poolpair - swappable tokens', () => { + it('should get all swappable tokens', async () => { + // Let indexer catch up + await service.waitForIndexedHeight(await testing.container.getBlockCount() - 1) + + // Wait for expect needed because the token graph is synchronised with the indexer + // every X seconds - i.e. eventual consistency + await waitForExpect(async () => { + const response = await client.poolpairs.getSwappableTokens('1') // A + expect(response).toStrictEqual({ + fromToken: { + id: '1', + symbol: 'A', + displaySymbol: 'dA' + }, + swappableTokens: [ + { id: '0', symbol: 'DFI', displaySymbol: 'DFI' }, + { id: '17', symbol: 'USDT', displaySymbol: 'dUSDT' }, + { id: '8', symbol: 'H', displaySymbol: 'dH' }, + { id: '19', symbol: 'USDC', displaySymbol: 'dUSDC' }, + { id: '7', symbol: 'G', displaySymbol: 'dG' }, + { id: '6', symbol: 'F', displaySymbol: 'dF' }, + { id: '5', symbol: 'E', displaySymbol: 'dE' }, + { id: '4', symbol: 'D', displaySymbol: 'dD' }, + { id: '3', symbol: 'C', displaySymbol: 'dC' }, + { id: '2', symbol: 'B', displaySymbol: 'dB' } + ] + }) + }) + }) + + it('should throw error as numeric string is expected', async () => { + expect.assertions(2) + try { + await client.poolpairs.getSwappableTokens('A-DFI') + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + message: 'Validation failed (numeric string is expected)', + url: '/v0.0/regtest/poolpairs/paths/swappable/A-DFI' + }) + } + }) +}) + +describe('poolpair - best swap path', () => { + it('should get best swap path', async () => { + // Let indexer catch up + await service.waitForIndexedHeight(await testing.container.getBlockCount() - 1) + + // Wait for expect needed because the token graph is synchronised with the indexer + // every X seconds - i.e. eventual consistency + await waitForExpect(async () => { + const response = await client.poolpairs.getBestPath('1', '2') // A to B + expect(response).toStrictEqual({ + fromToken: { + displaySymbol: 'dA', + id: '1', + symbol: 'A' + }, + toToken: { + displaySymbol: 'dB', + id: '2', + symbol: 'B' + }, + bestPath: [ + { + poolPairId: '9', + priceRatio: { + ab: '0.50000000', + ba: '2.00000000' + }, + symbol: 'A-DFI', + tokenA: { + displaySymbol: 'dA', + id: '1', + symbol: 'A' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + symbol: 'DFI' + } + }, + { + poolPairId: '10', + priceRatio: { + ab: '0.16666666', + ba: '6.00000000' + }, + symbol: 'B-DFI', + tokenA: { + displaySymbol: 'dB', + id: '2', + symbol: 'B' + }, + tokenB: { + displaySymbol: 'DFI', + id: '0', + symbol: 'DFI' + } + } + ], + estimatedReturn: '0.33333332' + }) + }) + }) + + it('should throw error as numeric string is expected', async () => { + expect.assertions(2) + try { + await client.poolpairs.getBestPath('A', 'B') + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + message: 'Validation failed (numeric string is expected)', + url: '/v0.0/regtest/poolpairs/paths/best/from/A/to/B' + }) + } + }) +}) + +describe('poolpair - all swap paths', () => { + it('should get all possible swap paths', async () => { + // Let indexer catch up + await service.waitForIndexedHeight(await testing.container.getBlockCount() - 1) + + // Wait for expect needed because the token graph is synchronised with the indexer + // every X seconds - i.e. eventual consistency + await waitForExpect(async () => { + const response = await client.poolpairs.getAllPaths('1', '2') // A to B + expect(response).toStrictEqual({ + fromToken: { displaySymbol: 'dA', id: '1', symbol: 'A' }, + toToken: { displaySymbol: 'dB', id: '2', symbol: 'B' }, + paths: [ + [ + { + symbol: 'A-DFI', + poolPairId: '9', + priceRatio: { ab: '0.50000000', ba: '2.00000000' }, + tokenA: { displaySymbol: 'dA', id: '1', symbol: 'A' }, + tokenB: { displaySymbol: 'DFI', id: '0', symbol: 'DFI' } + }, + { + symbol: 'B-DFI', + poolPairId: '10', + priceRatio: { ab: '0.16666666', ba: '6.00000000' }, + tokenA: { displaySymbol: 'dB', id: '2', symbol: 'B' }, + tokenB: { displaySymbol: 'DFI', id: '0', symbol: 'DFI' } + } + ] + ] + }) + }) + }) + + it('should throw error as numeric string is expected', async () => { + expect.assertions(2) + try { + await client.poolpairs.getAllPaths('A', 'B') + } catch (err: any) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + message: 'Validation failed (numeric string is expected)', + url: '/v0.0/regtest/poolpairs/paths/from/A/to/B' + }) + } + }) +}) + +describe('poolpair - get dex prices', () => { + it('should get dex prices in denominated currency', async () => { + // Let indexer catch up + await service.waitForIndexedHeight(await testing.container.getBlockCount() - 1) + + const response = await client.poolpairs.listDexPrices('DFI') + await waitForExpect(async () => { + expect(response).toStrictEqual({ + denomination: { + displaySymbol: 'DFI', + id: '0', + symbol: 'DFI' + }, + dexPrices: { + A: { + denominationPrice: '2.00000000', + token: { displaySymbol: 'dA', id: '1', symbol: 'A' } + }, + B: { + denominationPrice: '6.00000000', + token: { displaySymbol: 'dB', id: '2', symbol: 'B' } + }, + C: { + denominationPrice: '4.00000000', + token: { displaySymbol: 'dC', id: '3', symbol: 'C' } + }, + D: { + denominationPrice: '0', + token: { displaySymbol: 'dD', id: '4', symbol: 'D' } + }, + E: { + denominationPrice: '0', + token: { displaySymbol: 'dE', id: '5', symbol: 'E' } + }, + USDT: { + denominationPrice: '0.43151288', + token: { displaySymbol: 'dUSDT', id: '17', symbol: 'USDT' } + }, + DUSD: { + denominationPrice: '0', + token: { displaySymbol: 'DUSD', id: '21', symbol: 'DUSD' } + }, + USDC: { + denominationPrice: '0', + token: { displaySymbol: 'dUSDC', id: '19', symbol: 'USDC' } + }, + F: { + denominationPrice: '0', + token: { displaySymbol: 'dF', id: '6', symbol: 'F' } + }, + G: { + denominationPrice: '0', + token: { displaySymbol: 'dG', id: '7', symbol: 'G' } + }, + H: { + denominationPrice: '0', + token: { displaySymbol: 'dH', id: '8', symbol: 'H' } + }, + TEST: { + denominationPrice: '0', + token: { displaySymbol: 'dTEST', id: '22', symbol: 'TEST' } + } + } + }) + }) + }) +}) diff --git a/packages/whale-api-client/__tests__/api/prices.test.ts b/packages/whale-api-client/__tests__/api/prices.test.ts new file mode 100644 index 0000000000..68fffd60e9 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/prices.test.ts @@ -0,0 +1,949 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { StubService } from '../stub.service' +import { WhaleApiClient } from '../../src' +import { StubWhaleApiClient } from '../stub.client' +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' +import { PriceFeedTimeInterval } from '@defichain/whale-api-client/dist/api/Prices' +import { Testing } from '@defichain/jellyfish-testing' +import { + OracleIntervalSeconds +} from '@defichain-apps/nest-apps/whale/src/module.model/oracle.price.aggregated.interval' + +describe('oracles', () => { + let container: MasterNodeRegTestContainer + let service: StubService + let rpcClient: JsonRpcClient + let apiClient: WhaleApiClient + + beforeAll(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + apiClient = new StubWhaleApiClient(service) + + await container.start() + await container.waitForReady() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + rpcClient = new JsonRpcClient(await container.getCachedRpcUrl()) + }) + + afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } + }) + + 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' } + ], + 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 () => { + for (const setup of Object.values(setups)) { + setup.address = await container.getNewAddress() + setup.id = await rpcClient.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) { + const timestamp = Math.floor(new Date().getTime() / 1000) + await rpcClient.oracle.setOracleData(setup.id, timestamp, { + prices: price + }) + await container.generate(1) + } + } + + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + }) + + it('should list', async () => { + const prices = await apiClient.prices.list() + expect(prices.length).toStrictEqual(4) + 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: '2.30000000', + weightage: 3, + oracles: { + active: 2, + total: 3 + } + } + } + }) + }) + + describe('TA-USD', () => { + it('should get ticker', async () => { + const ticker = await apiClient.prices.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', + 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 feeds = await apiClient.prices.getFeed('TA', 'USD') + expect(feeds.length).toStrictEqual(6) + }) + + it('should get oracles', async () => { + const oracles = await apiClient.prices.getOracles('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) + } + }) + }) + }) +}) + +describe('pricefeed with interval', () => { + const container = new MasterNodeRegTestContainer() + const service = new StubService(container) + const apiClient = new StubWhaleApiClient(service) + let client: JsonRpcClient + + beforeAll(async () => { + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + client = new JsonRpcClient(await container.getCachedRpcUrl()) + }) + + afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } + }) + + it('should get interval', async () => { + const address = await container.getNewAddress() + const oracleId = await client.oracle.appointOracle(address, [ + { token: 'S1', currency: 'USD' } + ], { + weightage: 1 + }) + await container.generate(1) + + const oneMinute = 60 + let price = 0 + let mockTime = Math.floor(new Date().getTime() / 1000) + for (let h = 0; h < 24; h++) { // loop for 24 hours to make a day + for (let z = 0; z < 4; z++) { // loop for 4 x 15 mins interval to make an hour + mockTime += (15 * oneMinute) + 1 // +1 sec to fall into the next 15 mins bucket + await client.misc.setMockTime(mockTime) + await container.generate(2) + + await client.oracle.setOracleData(oracleId, mockTime, { + prices: [ + { + tokenAmount: `${(++price).toFixed(2)}@S1`, + currency: 'USD' + } + ] + }) + await container.generate(1) + } + } + + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + + const noInterval = await apiClient.prices.getFeed('S1', 'USD', height) + expect(noInterval.length).toStrictEqual(96) + + const interval15Mins = await apiClient.prices.getFeedWithInterval('S1', 'USD', PriceFeedTimeInterval.FIFTEEN_MINUTES, height) + expect(interval15Mins.length).toStrictEqual(96) + let prevMedianTime = 0 + let checkPrice = price + interval15Mins.forEach(value => { + expect(value.aggregated.amount).toStrictEqual(checkPrice.toFixed(8)) // check if price is descending in intervals of 1 + checkPrice-- + if (prevMedianTime !== 0) { // check if time interval is in 15 mins block + expect(prevMedianTime - value.block.medianTime - 1).toStrictEqual(OracleIntervalSeconds.FIFTEEN_MINUTES) // account for +1 in mock time + } + prevMedianTime = value.block.medianTime + }) + + const interval1Hour = await apiClient.prices.getFeedWithInterval('S1', 'USD', PriceFeedTimeInterval.ONE_HOUR, height) + expect(interval1Hour.length).toStrictEqual(24) + prevMedianTime = 0 + interval1Hour.forEach(value => { // check if time interval is in 1-hour block + if (prevMedianTime !== 0) { + expect(prevMedianTime - value.block.medianTime - 4).toStrictEqual(OracleIntervalSeconds.ONE_HOUR) // account for + 1 per block in mock time + } + prevMedianTime = value.block.medianTime + }) + + expect(interval1Hour.map(x => { + return { + amount: x.aggregated.amount, + time: x.aggregated.time + } + })).toStrictEqual( + [ + { + amount: '94.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '90.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '86.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '82.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '78.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '74.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '70.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '66.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '62.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '58.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '54.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '50.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '46.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '42.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '38.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '34.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '30.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '26.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '22.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '18.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '14.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '10.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '6.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + }, + { + amount: '2.50000000', + time: { + interval: 3600, + start: expect.any(Number), + end: expect.any(Number) + } + } + ] + ) + + { // ensure all aggregated time is aligned + let prevStart = interval1Hour[0].aggregated.time.start + for (let i = 1; i < interval1Hour.length; i++) { + const interval = interval1Hour[i] + expect(interval.aggregated.time.end).toStrictEqual(prevStart) + prevStart = interval.aggregated.time.start + } + } + + const interval1Day = await apiClient.prices.getFeedWithInterval('S1', 'USD', PriceFeedTimeInterval.ONE_DAY, height) + expect(interval1Day.length).toStrictEqual(1) + prevMedianTime = 0 + interval1Day.forEach(value => { // check if time interval is in 1-day block + if (prevMedianTime !== 0) { + expect(prevMedianTime - value.block.medianTime - 96).toStrictEqual(OracleIntervalSeconds.ONE_DAY) // account for + 1 per block in mock time + } + prevMedianTime = value.block.medianTime + }) + expect(interval1Day.map(x => x.aggregated.amount)).toStrictEqual( + [ + '48.50000000' + ] + ) + }) +}) + +describe('active price', () => { + const container = new MasterNodeRegTestContainer() + const testing = Testing.create(container) + const service = new StubService(container) + const apiClient = new StubWhaleApiClient(service) + let client: JsonRpcClient + + beforeEach(async () => { + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + client = new JsonRpcClient(await container.getCachedRpcUrl()) + }) + + afterEach(async () => { + try { + await service.stop() + } finally { + await container.stop() + } + }) + + it('should get active price with 2 active oracles (exact values)', async () => { + const address = await container.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 container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + } + + await testing.generate(1) + const beforeActivePrice = await apiClient.prices.getFeedActive('S1', 'USD', 1) + expect(beforeActivePrice.length).toStrictEqual(0) + + for (const oracle of oracles) { + await client.oracle.setOracleData(oracle, Math.floor(Date.now() / 1000), { + prices: [ + { tokenAmount: '10.0@S1', currency: 'USD' } + ] + }) + } + await testing.generate(1) + + await testing.rpc.loan.setLoanToken({ + symbol: 'S1', + fixedIntervalPriceId: 'S1/USD' + }) + await testing.generate(1) + + const oneMinute = 60 + const timeNow = Math.floor(Date.now() / 1000) + for (let i = 0; i <= 6; i++) { + const mockTime = timeNow + 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 testing.generate(1) + } + + { + const height = await container.getBlockCount() + await testing.generate(1) + await service.waitForIndexedHeight(height) + } + + const activePrice = await apiClient.prices.getFeedActive('S1', 'USD', 1) + 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 testing.generate(1) + const height = await container.getBlockCount() + await testing.generate(1) + await service.waitForIndexedHeight(height) + } + + const nextActivePrice = await apiClient.prices.getFeedActive('S1', 'USD', 1) + 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 + }) + }) + + it('should get active price with 2 active oracles (vs rpc)', async () => { + const oracles = [] + for (let i = 0; i < 2; i++) { + oracles.push(await client.oracle.appointOracle(await container.getNewAddress(), [ + { token: 'S1', currency: 'USD' } + ], { weightage: 1 })) + await testing.generate(1) + } + + for (const oracle of oracles) { + await client.oracle.setOracleData(oracle, Math.floor(Date.now() / 1000), { + prices: [ + { tokenAmount: '10.0@S1', currency: 'USD' } + ] + }) + } + + await testing.generate(1) + await testing.rpc.loan.setLoanToken({ + symbol: 'S1', + fixedIntervalPriceId: 'S1/USD' + }) + await testing.generate(1) + + const oneMinute = 60 + const timeNow = Math.floor(Date.now() / 1000) + for (let i = 0; i <= 6; i++) { + const mockTime = timeNow + 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 container.generate(1) + } + + // Active price ticks over in this loop, this is to ensure the values align + for (let i = 0; i <= 5; i++) { + { + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + } + + const fixedIntervalPrice = await testing.rpc.oracle.getFixedIntervalPrice('S1/USD') + const activePrice = await apiClient.prices.getFeedActive('S1', 'USD', 1) + expect(activePrice[0]).toStrictEqual({ + active: { + amount: fixedIntervalPrice.activePrice.toFixed(8), + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + block: { + hash: expect.any(String), + height: fixedIntervalPrice.activePriceBlock, + medianTime: expect.any(Number), + time: expect.any(Number) + }, + id: expect.any(String), + key: 'S1-USD', + next: { + amount: fixedIntervalPrice.nextPrice.toFixed(8), + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + sort: expect.any(String), + isLive: fixedIntervalPrice.isLive + }) + } + }) + + it('should go active then inactive then active (vs rpc)', async () => { + const address = await container.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 beforeActivePrice = await apiClient.prices.getFeedActive('S1', 'USD', 1) + expect(beforeActivePrice.length).toStrictEqual(0) + + for (const oracle of oracles) { + await client.oracle.setOracleData(oracle, Math.floor(Date.now() / 1000), { + prices: [ + { tokenAmount: '10.0@S1', currency: 'USD' } + ] + }) + } + + await testing.generate(1) + await testing.rpc.loan.setLoanToken({ + symbol: 'S1', + fixedIntervalPriceId: 'S1/USD' + }) + await testing.generate(1) + + const oneMinute = 60 + const timeNow = Math.floor(Date.now() / 1000) + for (let i = 0; i <= 6; i++) { + const mockTime = timeNow + 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' } + ] + }) + } + } + + { + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + await new Promise((resolve) => setTimeout(resolve, 500)) + + const fixedIntervalPrice = await testing.rpc.oracle.getFixedIntervalPrice('S1/USD') + const activePrice = await apiClient.prices.getFeedActive('S1', 'USD', 1) + expect(activePrice[0]).toStrictEqual({ + block: { + hash: expect.any(String), + height: expect.any(Number), + medianTime: expect.any(Number), + time: fixedIntervalPrice.timestamp + }, + id: expect.any(String), + key: 'S1-USD', + next: { + amount: fixedIntervalPrice.nextPrice.toFixed(8), + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + sort: expect.any(String), + isLive: fixedIntervalPrice.isLive + }) + + expect(activePrice[0].isLive).toStrictEqual(false) + } + + // Set mock time in the future + const mockTime = Math.floor(new Date().getTime() / 1000) + 70 * oneMinute + await client.misc.setMockTime(mockTime) + + { + await container.generate(6) + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + await new Promise((resolve) => setTimeout(resolve, 500)) + + const fixedIntervalPrice = await testing.rpc.oracle.getFixedIntervalPrice('S1/USD') + const activePrice = await apiClient.prices.getFeedActive('S1', 'USD', 1) + + expect(activePrice[0]).toStrictEqual({ + active: { + amount: fixedIntervalPrice.activePrice.toFixed(8), + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + block: { + hash: expect.any(String), + height: fixedIntervalPrice.activePriceBlock, + medianTime: expect.any(Number), + time: expect.any(Number) + }, + id: expect.any(String), + key: 'S1-USD', + sort: expect.any(String), + isLive: fixedIntervalPrice.isLive + }) + + expect(activePrice[0].isLive).toStrictEqual(false) + } + + for (const oracle of oracles) { + await client.oracle.setOracleData(oracle, mockTime, { + prices: [ + { tokenAmount: '15.0@S1', currency: 'USD' } + ] + }) + } + + { + await container.generate(6) + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + await new Promise((resolve) => setTimeout(resolve, 500)) + + const fixedIntervalPrice = await testing.rpc.oracle.getFixedIntervalPrice('S1/USD') + const activePrice = await apiClient.prices.getFeedActive('S1', 'USD', 1) + + expect(activePrice[0]).toStrictEqual({ + active: { + amount: fixedIntervalPrice.activePrice.toFixed(8), + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + next: { + amount: fixedIntervalPrice.nextPrice.toFixed(8), + oracles: { + active: 2, + total: 2 + }, + weightage: 2 + }, + block: { + hash: expect.any(String), + height: fixedIntervalPrice.activePriceBlock, + medianTime: expect.any(Number), + time: expect.any(Number) + }, + id: expect.any(String), + key: 'S1-USD', + sort: expect.any(String), + isLive: fixedIntervalPrice.isLive + }) + + expect(activePrice[0].isLive).toStrictEqual(true) + } + }) +}) diff --git a/packages/whale-api-client/__tests__/api/rawtx.test.ts b/packages/whale-api-client/__tests__/api/rawtx.test.ts new file mode 100644 index 0000000000..34ac142dfe --- /dev/null +++ b/packages/whale-api-client/__tests__/api/rawtx.test.ts @@ -0,0 +1,264 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { createSignedTxnHex } from '@defichain/testing' +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiClient, WhaleApiException, WhaleApiValidationException } from '../../src' + +let container: MasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient + +beforeAll(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +beforeEach(async () => { + await container.waitForWalletBalanceGTE(15) +}) + +describe('test', () => { + it('should accept valid txn', async () => { + const hex = await createSignedTxnHex(container, 10, 9.9999) + await client.rawtx.send({ + hex: hex + }) + }) + + it('should accept valid txn with given maxFeeRate', async () => { + const hex = await createSignedTxnHex(container, 10, 9.995) + await client.rawtx.test({ + hex: hex, + maxFeeRate: 0.05 + }) + }) + + it('should reject due to invalid txn', async () => { + expect.assertions(2) + try { + await client.rawtx.test({ hex: '0400000100881133bb11aa00cc' }) + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + message: 'Transaction decode failed', + at: expect.any(Number), + url: '/v0.0/regtest/rawtx/test' + }) + } + }) + + it('should reject due to high fees', async () => { + const hex = await createSignedTxnHex(container, 10, 9) + expect.assertions(2) + try { + await client.rawtx.test({ + hex: hex, + maxFeeRate: 1 + }) + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + message: 'Transaction is not allowed to be inserted', + url: '/v0.0/regtest/rawtx/test' + }) + } + }) +}) + +describe('send', () => { + it('should send valid txn 0.0001 DFI as fees', async () => { + const hex = await createSignedTxnHex(container, 10, 9.9999) + const txid = await client.rawtx.send({ + hex: hex + }) + expect(txid.length).toStrictEqual(64) + + await container.generate(1) + const out = await container.call('gettxout', [txid, 0]) + expect(out.value).toStrictEqual(9.9999) + }) + + it('should send valid txn 0.01 DFI as fees', async () => { + const hex = await createSignedTxnHex(container, 10, 9.99) + const txid = await client.rawtx.send({ + hex: hex + }) + expect(txid.length).toStrictEqual(64) + + await container.generate(1) + const out = await container.call('gettxout', [txid, 0]) + expect(out.value).toStrictEqual(9.99) + }) + + it('should send valid txn with given maxFeeRate', async () => { + const hex = await createSignedTxnHex(container, 10, 9.995) + const txid = await client.rawtx.send({ + hex: hex, + maxFeeRate: 0.05 + }) + expect(txid.length).toStrictEqual(64) + + await container.generate(1) + const out = await container.call('gettxout', [txid, 0]) + expect(out.value).toStrictEqual(9.995) + }) + + it('should fail due to invalid txn', async () => { + expect.assertions(2) + try { + await client.rawtx.send({ + hex: '0400000100881133bb11aa00cc' + }) + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + url: '/v0.0/regtest/rawtx/send', + message: 'Transaction decode failed' + }) + } + }) + + it('should fail due to high fees', async () => { + const hex = await createSignedTxnHex(container, 10, 9) + + expect.assertions(2) + try { + await client.rawtx.send({ + hex: hex, + maxFeeRate: 1 + }) + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + url: '/v0.0/regtest/rawtx/send', + message: 'Absurdly high fee' + }) + } + }) + + it('should fail due to high fees using default values', async () => { + const hex = await createSignedTxnHex(container, 10, 9.95) + expect.assertions(2) + try { + await client.rawtx.send({ hex: hex }) + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + message: 'Absurdly high fee', + url: '/v0.0/regtest/rawtx/send' + }) + } + }) + + it('should fail validation (empty hex)', async () => { + expect.assertions(3) + try { + await client.rawtx.send({ + hex: '' + }) + expect('must fail').toBeUndefined() + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiValidationException) + expect(err.message).toStrictEqual('422 - ValidationError (/v0.0/regtest/rawtx/send)') + expect(err.properties).toStrictEqual([{ + constraints: [ + 'hex must be a hexadecimal number', + 'hex should not be empty' + ], + property: 'hex', + value: '' + }]) + } + }) + + it('should fail validation (not hex)', async () => { + expect.assertions(3) + try { + await client.rawtx.send({ + hex: 'fuxingloh' + }) + expect('must fail').toBeUndefined() + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiValidationException) + expect(err.message).toStrictEqual('422 - ValidationError (/v0.0/regtest/rawtx/send)') + expect(err.properties).toStrictEqual([{ + constraints: [ + 'hex must be a hexadecimal number' + ], + property: 'hex', + value: 'fuxingloh' + }]) + } + }) + + it('should fail validation (negative fee)', async () => { + expect.assertions(3) + try { + await client.rawtx.send({ + hex: '00', + maxFeeRate: -1.5 + }) + expect('must fail').toBeUndefined() + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiValidationException) + expect(err.message).toStrictEqual('422 - ValidationError (/v0.0/regtest/rawtx/send)') + expect(err.properties).toStrictEqual([{ + constraints: [ + 'maxFeeRate must not be less than 0' + ], + property: 'maxFeeRate', + value: -1.5 + }]) + } + }) + + it('should fail validation (not number fee)', async () => { + expect.assertions(3) + try { + await client.rawtx.send({ + hex: '00', + // @ts-expect-error + maxFeeRate: 'abc' + }) + expect('must fail').toBeUndefined() + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiValidationException) + expect(err.message).toStrictEqual('422 - ValidationError (/v0.0/regtest/rawtx/send)') + expect(err.properties).toStrictEqual([{ + constraints: [ + 'maxFeeRate must not be less than 0', + 'maxFeeRate must be a number conforming to the specified constraints' + ], + property: 'maxFeeRate', + value: 'abc' + }]) + } + }) +}) diff --git a/packages/whale-api-client/__tests__/api/rpc.test.ts b/packages/whale-api-client/__tests__/api/rpc.test.ts new file mode 100644 index 0000000000..ff11217d52 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/rpc.test.ts @@ -0,0 +1,71 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { blockchain } from '@defichain/jellyfish-api-core' +import { WhaleApiClient } from '../../src' +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' + +let container: MasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient + +beforeAll(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + + await container.start() + await container.waitForReady() + await service.start() +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +it('should throw error for non whitelisted method', async () => { + await expect( + client.rpc.call('getbalance', [], 'number') + ).rejects.toThrow('403 - Forbidden (/v0.0/regtest/rpc/getbalance): RPC method not whitelisted') +}) + +it('should throw error on invalid params', async () => { + await expect( + client.rpc.call('getblock', [{ block: 1 }], 'number') + ).rejects.toThrow('400 - BadRequest (/v0.0/regtest/rpc/getblock): RpcApiError: \'JSON value is not a string as expected\', code: -1') +}) + +describe('whitelisted rpc methods', () => { + it('should rpc.call(getblockchaininfo)', async () => { + const info = await client.rpc.call('getblockchaininfo', [], 'number') + + expect(info.chain).toStrictEqual('regtest') + expect(typeof info.blocks).toStrictEqual('number') + }) + + it('should rpc.call(getblockcount)', async () => { + const count = await client.rpc.call('getblockcount', [], 'number') + + expect(typeof count).toStrictEqual('number') + }) + + it('should rpc.call(getblockhash)', async () => { + await container.generate(1) + + const hash = await client.rpc.call('getblockhash', [1], 'number') + expect(hash.length).toStrictEqual(64) + }) + + it('should rpc.call(getblock)', async () => { + await container.generate(1) + + const hash = await client.rpc.call('getblockhash', [1], 'number') + const block = await client.rpc.call>('getblock', [hash], 'number') + + expect(block.hash.length).toStrictEqual(64) + expect(Array.isArray(block.tx)).toStrictEqual(true) + }) +}) diff --git a/packages/whale-api-client/__tests__/api/stats.test.ts b/packages/whale-api-client/__tests__/api/stats.test.ts new file mode 100644 index 0000000000..48175bc72b --- /dev/null +++ b/packages/whale-api-client/__tests__/api/stats.test.ts @@ -0,0 +1,505 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiClient } from '../../src' +import { addPoolLiquidity, createPoolPair, createToken, getNewAddress, mintTokens } from '@defichain/testing' +import { Testing } from '@defichain/jellyfish-testing' +import BigNumber from 'bignumber.js' + +describe('stats', () => { + let container: MasterNodeRegTestContainer + let service: StubService + let client: WhaleApiClient + + beforeAll(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + await createToken(container, 'A') + await mintTokens(container, 'A') + await createToken(container, 'B') + await mintTokens(container, 'B') + + await createPoolPair(container, 'A', 'DFI') + await addPoolLiquidity(container, { + tokenA: 'A', + amountA: 100, + tokenB: 'DFI', + amountB: 200, + shareAddress: await getNewAddress(container) + }) + await createPoolPair(container, 'B', 'DFI') + await addPoolLiquidity(container, { + tokenA: 'B', + amountA: 50, + tokenB: 'DFI', + amountB: 200, + shareAddress: await getNewAddress(container) + }) + await createToken(container, 'USDT') + await createPoolPair(container, 'USDT', 'DFI') + await mintTokens(container, 'USDT') + await addPoolLiquidity(container, { + tokenA: 'USDT', + amountA: 1000, + tokenB: 'DFI', + amountB: 431.51288, + shareAddress: await getNewAddress(container) + }) + await createToken(container, 'USDC') + await createPoolPair(container, 'USDC', 'DFI') + await mintTokens(container, 'USDC') + await addPoolLiquidity(container, { + tokenA: 'USDC', + amountA: 1000, + tokenB: 'DFI', + amountB: 431.51288, + shareAddress: await getNewAddress(container) + }) + const height = await container.getBlockCount() + await container.generate(1) + await service.waitForIndexedHeight(height) + }) + + afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } + }) + + it('should get stat data', async () => { + const data = await client.stats.get() + + expect(data).toStrictEqual({ + count: { + blocks: 122, + prices: 0, + tokens: 9, + masternodes: 8 + }, + burned: { + address: 0, + auction: 0, + emission: 7323.58, + fee: 4, + payback: 0, + total: 7327.58 + }, + tvl: { + dex: 5853.942343505482, + masternodes: 185.39423435054823, + loan: 0, + total: 6039.336577856031 + }, + price: { + usd: 2.317427929381853, + usdt: 2.317427929381853 + }, + masternodes: { + locked: [ + { + count: 8, + tvl: 185.39423435054823, + weeks: 0 + } + ] + }, + emission: { + total: 405.04, + anchor: 0.081008, + dex: 103.08268, + community: 19.887464, + masternode: 134.999832, + burned: 146.989016 + }, + loan: { + count: { + collateralTokens: 0, + loanTokens: 0, + openAuctions: 0, + openVaults: 0, + schemes: 0 + }, + value: { + collateral: 0, + loan: 0 + } + }, + blockchain: { + difficulty: expect.any(Number) + }, + net: { + protocolversion: expect.any(Number), + subversion: expect.any(String), + version: expect.any(Number) + } + }) + }) + + it('should get stat supply', async () => { + const data = await client.stats.getSupply() + expect(data).toStrictEqual({ + max: 1200000000, + total: expect.any(Number), + burned: expect.any(Number), + circulating: expect.any(Number) + }) + }) + + it('should get stat burn', async () => { + const data = await client.stats.getBurn() + expect(data).toStrictEqual({ + address: 'mfburnZSAM7Gs1hpDeNaMotJXSGA7edosG', + amount: 0, + auctionburn: 0, + dexfeetokens: [], + dfip2203: [], + dfipaybackfee: 0, + dfipaybacktokens: [], + emissionburn: expect.any(Number), + feeburn: expect.any(Number), + paybackburn: 0, + tokens: [], + paybackfees: [], + paybacktokens: [] + }) + }) +}) + +describe('loan - stats', () => { + const container = new MasterNodeRegTestContainer() + const service = new StubService(container) + const client = new StubWhaleApiClient(service) + const testing = Testing.create(container) + + beforeAll(async () => { + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + { // DFI setup + await testing.token.dfi({ + address: await testing.address('DFI'), + amount: 40000 + }) + } + + { // DEX setup + await testing.fixture.createPoolPair({ + a: { + amount: 2000, + symbol: 'DUSD' + }, + b: { + amount: 2000, + symbol: 'DFI' + } + }) + await testing.fixture.createPoolPair({ + a: { + amount: 1000, + symbol: 'USDT' + }, + b: { + amount: 2000, + symbol: 'DFI' + } + }) + } + + { // Loan Scheme + await testing.rpc.loan.createLoanScheme({ + id: 'default', + minColRatio: 100, + interestRate: new BigNumber(1) + }) + await testing.generate(1) + + await testing.rpc.loan.createLoanScheme({ + id: 'scheme', + minColRatio: 110, + interestRate: new BigNumber(1) + }) + await testing.generate(1) + } + + let oracleId: string + { // Oracle 1 + const oracleAddress = await testing.generateAddress() + const priceFeeds = [ + { + token: 'DFI', + currency: 'USD' + }, + { + token: 'TSLA', + currency: 'USD' + }, + { + token: 'AAPL', + currency: 'USD' + }, + { + token: 'GOOGL', + currency: 'USD' + } + ] + oracleId = await testing.rpc.oracle.appointOracle(oracleAddress, priceFeeds, { weightage: 1 }) + await testing.generate(1) + + const timestamp = Math.floor(new Date().getTime() / 1000) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '1@DFI', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '2@TSLA', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '2@AAPL', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '4@GOOGL', + currency: 'USD' + }] + }) + await testing.generate(1) + } + + { // Oracle 2 + const priceFeeds = [ + { + token: 'DFI', + currency: 'USD' + }, + { + token: 'TSLA', + currency: 'USD' + }, + { + token: 'AAPL', + currency: 'USD' + }, + { + token: 'GOOGL', + currency: 'USD' + } + ] + const oracleId = await testing.rpc.oracle.appointOracle(await testing.generateAddress(), priceFeeds, { weightage: 1 }) + await testing.generate(1) + + const timestamp = Math.floor(new Date().getTime() / 1000) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '1@DFI', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '2@TSLA', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '2@AAPL', + currency: 'USD' + }] + }) + await testing.rpc.oracle.setOracleData(oracleId, timestamp, { + prices: [{ + tokenAmount: '4@GOOGL', + currency: 'USD' + }] + }) + await testing.generate(1) + } + + { // Collateral Tokens + await testing.rpc.loan.setCollateralToken({ + token: 'DFI', + factor: new BigNumber(1), + fixedIntervalPriceId: 'DFI/USD' + }) + } + + { // Loan Tokens + await testing.rpc.loan.setLoanToken({ + symbol: 'TSLA', + fixedIntervalPriceId: 'TSLA/USD' + }) + await testing.generate(1) + + await testing.rpc.loan.setLoanToken({ + symbol: 'AAPL', + fixedIntervalPriceId: 'AAPL/USD' + }) + await testing.generate(1) + + await testing.rpc.loan.setLoanToken({ + symbol: 'GOOGL', + fixedIntervalPriceId: 'GOOGL/USD' + }) + await testing.generate(1) + } + + { // Vault Empty (John) + await testing.rpc.loan.createVault({ + ownerAddress: await testing.address('John'), + loanSchemeId: 'default' + }) + await testing.generate(1) + } + + { // Vault Deposit Collateral (Bob) + const bobDepositedVaultId = await testing.rpc.loan.createVault({ + ownerAddress: await testing.address('Bob'), + loanSchemeId: 'default' + }) + await testing.generate(1) + await testing.rpc.loan.depositToVault({ + vaultId: bobDepositedVaultId, + from: await testing.address('DFI'), + amount: '10000@DFI' + }) + await testing.generate(1) + } + + { // Vault Deposited & Loaned (John) + const johnLoanedVaultId = await testing.rpc.loan.createVault({ + ownerAddress: await testing.address('John'), + loanSchemeId: 'scheme' + }) + await testing.generate(1) + await testing.rpc.loan.depositToVault({ + vaultId: johnLoanedVaultId, + from: await testing.address('DFI'), + amount: '10000@DFI' + }) + await testing.generate(1) + await testing.rpc.loan.takeLoan({ + vaultId: johnLoanedVaultId, + amounts: '30@TSLA' + }) + await testing.generate(1) + } + + { // Vault Deposited, Loaned, Liquidated (Adam) + const adamLiquidatedVaultId = await testing.rpc.loan.createVault({ + ownerAddress: await testing.address('Adam'), + loanSchemeId: 'default' + }) + await testing.generate(1) + await testing.rpc.loan.depositToVault({ + vaultId: adamLiquidatedVaultId, + from: await testing.address('DFI'), + amount: '10000@DFI' + }) + await testing.generate(1) + await testing.rpc.loan.takeLoan({ + vaultId: adamLiquidatedVaultId, + amounts: '30@AAPL' + }) + await testing.generate(1) + + // Make vault enter under liquidation state by a price hike of the loan token + const timestamp2 = Math.floor(new Date().getTime() / 1000) + await testing.rpc.oracle.setOracleData(oracleId, timestamp2, { + prices: [{ + tokenAmount: '1000@AAPL', + currency: 'USD' + }] + }) + + // Wait for 12 blocks which are equivalent to 2 hours (1 block = 10 minutes in regtest) in order to liquidate the vault + await testing.generate(12) + } + + { + const height = await container.getBlockCount() + await service.waitForIndexedHeight(height - 1) + } + }) + + afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } + }) + + it('should get stat data', async () => { + const data = await client.stats.get() + + expect(data).toStrictEqual({ + count: { + blocks: 137, + prices: 4, + tokens: 8, + masternodes: 8 + }, + burned: expect.any(Object), + tvl: { + dex: 6000, + loan: 20000, + masternodes: 40, + total: 26040 + }, + price: { + usd: 0.5, + usdt: 0.5 + }, + masternodes: expect.any(Object), + emission: expect.any(Object), + loan: { + count: { + collateralTokens: 1, + loanTokens: 3, + openAuctions: 1, + openVaults: 4, + schemes: 2 + }, + value: { + collateral: 20000, + loan: 60.00018266 + } + }, + blockchain: { + difficulty: expect.any(Number) + }, + net: { + protocolversion: expect.any(Number), + subversion: expect.any(String), + version: expect.any(Number) + } + }) + }) + + it('should get stat supply', async () => { + const data = await client.stats.getSupply() + expect(data).toStrictEqual({ + max: 1200000000, + total: expect.any(Number), + burned: expect.any(Number), + circulating: expect.any(Number) + }) + }) +}) diff --git a/packages/whale-api-client/__tests__/api/tokens.test.ts b/packages/whale-api-client/__tests__/api/tokens.test.ts new file mode 100644 index 0000000000..497066cc10 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/tokens.test.ts @@ -0,0 +1,150 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiClient, WhaleApiException } from '../../src' +import { createPoolPair, createToken } from '@defichain/testing' + +let container: MasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient + +beforeAll(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + + await container.start() + await container.waitForReady() + await container.waitForWalletCoinbaseMaturity() + await service.start() + await createToken(container, 'DBTC') + await createToken(container, 'DETH') + await createPoolPair(container, 'DBTC', 'DETH') +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +describe('list', () => { + it('should listTokens', async () => { + const result = await client.tokens.list() + expect(result.length).toStrictEqual(4) + expect(result[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 + } + }) + }) + + it('should listTokens with pagination', async () => { + const first = await client.tokens.list(2) + + expect(first.length).toStrictEqual(2) + expect(first.hasNext).toStrictEqual(true) + expect(first.nextToken).toStrictEqual('1') + + expect(first[0]).toStrictEqual(expect.objectContaining({ id: '0', symbol: 'DFI', symbolKey: 'DFI' })) + expect(first[1]).toStrictEqual(expect.objectContaining({ id: '1', symbol: 'DBTC', symbolKey: 'DBTC' })) + + const next = await client.paginate(first) + + expect(next.length).toStrictEqual(2) + expect(next.hasNext).toStrictEqual(true) + expect(next.nextToken).toStrictEqual('3') + + expect(next[0]).toStrictEqual(expect.objectContaining({ id: '2', symbol: 'DETH', symbolKey: 'DETH' })) + expect(next[1]).toStrictEqual(expect.objectContaining({ id: '3', symbol: 'DBTC-DETH', symbolKey: 'DBTC-DETH' })) + + const last = await client.paginate(next) + + expect(last.length).toStrictEqual(0) + expect(last.hasNext).toStrictEqual(false) + expect(last.nextToken).toBeUndefined() + }) +}) + +describe('get', () => { + it('should get DFI by DFI numeric id', async () => { + const data = await client.tokens.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 + } + }) + }) + + it('should fail due to getting non-existent token', async () => { + expect.assertions(2) + try { + await client.tokens.get('999') + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'Unable to find token', + url: '/v0.0/regtest/tokens/999' + }) + } + }) + + it('should fail due to id is malformed', async () => { + expect.assertions(2) + try { + await client.tokens.get('$*@') + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 400, + type: 'BadRequest', + at: expect.any(Number), + message: 'Validation failed (numeric string is expected)', + url: '/v0.0/regtest/tokens/$*@' + }) + } + }) +}) diff --git a/packages/whale-api-client/__tests__/api/transaction.test.ts b/packages/whale-api-client/__tests__/api/transaction.test.ts new file mode 100644 index 0000000000..d7831a2581 --- /dev/null +++ b/packages/whale-api-client/__tests__/api/transaction.test.ts @@ -0,0 +1,165 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { StubWhaleApiClient } from '../stub.client' +import { StubService } from '../stub.service' +import { WhaleApiClient, WhaleApiException } from '../../src' +import { JsonRpcClient } from '@defichain/jellyfish-api-jsonrpc' + +let container: MasterNodeRegTestContainer +let service: StubService +let client: WhaleApiClient +let rpcClient: JsonRpcClient + +beforeAll(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleApiClient(service) + + await container.start() + await container.waitForWalletCoinbaseMaturity() + await service.start() + + await container.waitForBlockHeight(101) + await service.waitForIndexedHeight(101) + + rpcClient = new JsonRpcClient(await container.getCachedRpcUrl()) +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +beforeEach(async () => { + await container.waitForWalletBalanceGTE(15) +}) + +describe('get', () => { + let txid: string + + async function setup (): Promise { + const address = await container.getNewAddress() + const metadata = { + symbol: 'ETH', + name: 'ETH', + isDAT: true, + mintable: true, + tradeable: true, + collateralAddress: address + } + + txid = await container.call('createtoken', [metadata]) + + await container.generate(1) + + const height = await container.call('getblockcount') + + await container.generate(1) + + await service.waitForIndexedHeight(height) + } + + beforeAll(async () => { + await setup() + }) + + it('should get a single transaction', async () => { + const transaction = await client.transactions.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: txid, + 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 client.transactions.get('invalidtransactionid') + } catch (err) { + expect(err).toBeInstanceOf(WhaleApiException) + expect(err.error).toStrictEqual({ + code: 404, + type: 'NotFound', + at: expect.any(Number), + message: 'transaction not found', + url: '/v0.0/regtest/transactions/invalidtransactionid' + }) + } + }) +}) + +describe('getVins', () => { + it('should return list of vins', async () => { + const blockHash = await container.call('getblockhash', [100]) + const block = await rpcClient.blockchain.getBlock(blockHash, 2) + + const txid = block.tx[0].txid + const vins = await client.transactions.getVins(txid) + + expect(vins.length).toBeGreaterThanOrEqual(1) + }) + + it('should return list of vins when next is out of range', async () => { + const blockHash = await container.call('getblockhash', [100]) + const block = await rpcClient.blockchain.getBlock(blockHash, 2) + + const txid = block.tx[0].txid + const vins = await client.transactions.getVins(txid, 30, '100') + + expect(vins.length).toBeGreaterThanOrEqual(1) + }) + + it('should return empty page if txid is not valid', async () => { + const vins = await client.transactions.getVins('9d87a6b6b77323b6dab9d8971fff0bc7a6c341639ebae39891024f4800528532', 30) + + expect(vins.length).toStrictEqual(0) + expect(vins.hasNext).toStrictEqual(false) + }) +}) + +describe('getVouts', () => { + it('should return list of vouts', async () => { + const blockHash = await container.call('getblockhash', [100]) + const block = await rpcClient.blockchain.getBlock(blockHash, 2) + + const txid = block.tx[0].txid + const vouts = await client.transactions.getVouts(txid) + + expect(vouts.length).toBeGreaterThanOrEqual(1) + }) + + it.skip('should return list of vouts when next is out of range', async () => { + const blockHash = await container.call('getblockhash', [100]) + const block = await rpcClient.blockchain.getBlock(blockHash, 2) + + const txid = block.tx[0].txid + const vouts = await client.transactions.getVouts(txid, 30, '100') + + expect(vouts.length).toBeGreaterThanOrEqual(1) + }) + + it('should return empty page if txid is not valid', async () => { + const vouts = await client.transactions.getVouts('9d87a6b6b77323b6dab9d8971fff0bc7a6c341639ebae39891024f4800528532', 30) + + expect(vouts.length).toStrictEqual(0) + expect(vouts.hasNext).toStrictEqual(false) + }) +}) diff --git a/packages/whale-api-client/__tests__/errors/api.error.test.ts b/packages/whale-api-client/__tests__/errors/api.error.test.ts new file mode 100644 index 0000000000..67ccd405dc --- /dev/null +++ b/packages/whale-api-client/__tests__/errors/api.error.test.ts @@ -0,0 +1,19 @@ +import { WhaleApiError, WhaleApiErrorType, WhaleApiException } from '../../src' + +it('WhaleApiException should be formatted as', () => { + const error: WhaleApiError = { + code: 404, + type: WhaleApiErrorType.NotFound, + at: 123, + message: 'some message', + url: '/link/to' + } + + const exception = new WhaleApiException(error) + + expect(exception.message).toStrictEqual('404 - NotFound (/link/to): some message') + expect(exception.code).toStrictEqual(404) + expect(exception.type).toStrictEqual('NotFound') + expect(exception.at).toStrictEqual(123) + expect(exception.url).toStrictEqual('/link/to') +}) diff --git a/packages/whale-api-client/__tests__/errors/api.validation.exception.test.ts b/packages/whale-api-client/__tests__/errors/api.validation.exception.test.ts new file mode 100644 index 0000000000..177b9090b2 --- /dev/null +++ b/packages/whale-api-client/__tests__/errors/api.validation.exception.test.ts @@ -0,0 +1,36 @@ +import { WhaleApiErrorType, WhaleApiValidationException } from '../../src' + +it('WhaleApiValidationException should includes properties', () => { + const error = { + code: 422, + type: WhaleApiErrorType.ValidationError, + at: 1234, + url: '/link/to/validation/error', + validation: { + properties: [ + { + property: 'key', + value: 'value', + constraints: [ + 'value is missing' + ] + } + ] + } + } + + const exception = new WhaleApiValidationException(error) + + expect(exception.message).toStrictEqual('422 - ValidationError (/link/to/validation/error)') + expect(exception.code).toStrictEqual(422) + expect(exception.type).toStrictEqual('ValidationError') + expect(exception.properties).toStrictEqual([ + { + property: 'key', + value: 'value', + constraints: [ + 'value is missing' + ] + } + ]) +}) diff --git a/packages/whale-api-client/__tests__/errors/client.timeout.exception.test.ts b/packages/whale-api-client/__tests__/errors/client.timeout.exception.test.ts new file mode 100644 index 0000000000..ebf783289a --- /dev/null +++ b/packages/whale-api-client/__tests__/errors/client.timeout.exception.test.ts @@ -0,0 +1,7 @@ +import { WhaleClientTimeoutException } from '../../src' + +it('WhaleClientTimeoutException should be structured as', () => { + const exception = new WhaleClientTimeoutException(15000) + + expect(exception.message).toStrictEqual('request aborted due to timeout of 15000 ms') +}) diff --git a/packages/whale-api-client/__tests__/errors/index.test.ts b/packages/whale-api-client/__tests__/errors/index.test.ts new file mode 100644 index 0000000000..a3bbb3392d --- /dev/null +++ b/packages/whale-api-client/__tests__/errors/index.test.ts @@ -0,0 +1,55 @@ +import { + raiseIfError, + WhaleApiError, + WhaleApiErrorType, + WhaleApiException, + WhaleApiValidationException +} from '../../src' + +it('should raise if error', () => { + const error: WhaleApiError = { + code: 400, + type: WhaleApiErrorType.BadRequest, + at: 123456, + message: 'bad request', + url: '/link/to/bad/request' + } + + function throwError (): void { + raiseIfError({ + data: undefined, + error: error + }) + } + + expect(throwError).toThrow('400 - BadRequest (/link/to/bad/request): bad request') + expect(throwError).toThrow(WhaleApiException) +}) + +it('should raise validation error', () => { + const error: WhaleApiError = { + code: 422, + type: WhaleApiErrorType.ValidationError, + at: 123456, + message: 'validation error', + url: '/link/to/validationerror/request' + } + + function throwError (): void { + raiseIfError({ + data: undefined, + error: error + }) + } + expect(throwError).toThrow('422 - ValidationError (/link/to/validationerror/request): validation error') + expect(throwError).toThrow(WhaleApiValidationException) +}) + +it('should not raise error if error is undefined', () => { + expect(() => { + raiseIfError({ + data: undefined, + error: undefined + }) + }).not.toThrow() +}) diff --git a/packages/whale-api-client/__tests__/stub.client.ts b/packages/whale-api-client/__tests__/stub.client.ts new file mode 100644 index 0000000000..d3c565e6ed --- /dev/null +++ b/packages/whale-api-client/__tests__/stub.client.ts @@ -0,0 +1,65 @@ +import { Method, ResponseAsString, WhaleApiClient, WhaleRpcClient } from '../src' +import { StubService } from './stub.service' +import { version } from '../src/Version' +import AbortController from 'abort-controller' + +/** + * Client stubs are simulations of a real client, which are used for functional testing. + * StubWhaleApiClient simulate a real WhaleApiClient connected to a DeFi Whale Service. + */ +export class StubWhaleApiClient extends WhaleApiClient { + constructor (readonly service: StubService) { + super({ url: 'not required for stub service' }) + } + + async requestAsString (method: Method, path: string, body?: string): Promise { + if (this.service.app === undefined) { + throw new Error('StubService is not yet started.') + } + + const version = this.options.version as string + const res = await this.service.app.inject({ + method: method, + url: `/${version}/regtest/${path}`, + payload: body, + headers: method !== 'GET' ? { 'Content-Type': 'application/json' } : {} + }) + + return { + body: res.body, + status: res.statusCode + } + } +} + +export class StubWhaleRpcClient extends WhaleRpcClient { + constructor (readonly service: StubService) { + super('not required for stub service') + } + + protected async fetch (body: string, controller: AbortController): Promise { + if (this.service.app === undefined) { + throw new Error('StubService is not yet started.') + } + + const res = await this.service.app.inject({ + method: 'POST', + url: `/${version as string}/regtest/rpc`, + payload: body, + headers: { 'Content-Type': 'application/json' } + }) + + // @ts-expect-error + return { + url: res.raw.req.url, + ok: res.statusCode === 200, + redirected: false, + status: res.statusCode, + statusText: res.statusMessage, + bodyUsed: true, + async text (): Promise { + return res.body + } + } + } +} diff --git a/packages/whale-api-client/__tests__/stub.service.ts b/packages/whale-api-client/__tests__/stub.service.ts new file mode 100644 index 0000000000..85b53aacc4 --- /dev/null +++ b/packages/whale-api-client/__tests__/stub.service.ts @@ -0,0 +1,59 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { NestFastifyApplication } from '@nestjs/platform-fastify' +import { createTestingApp } from '@defichain-apps/nest-apps/whale/src/e2e.module' +import { addressToHid } from '@defichain-apps/nest-apps/whale/src/module.api/address.controller' +import waitForExpect from 'wait-for-expect' +import { ScriptAggregationMapper } from '@defichain-apps/nest-apps/whale/src/module.model/script.aggregation' +import { BlockMapper } from '@defichain-apps/nest-apps/whale/src/module.model/block' + +/** + * Service stubs are simulations of a real service, which are used for functional testing. + * Configures a TestingModule that is configured to connect to a provided @defichain/testcontainers. + */ +export class StubService { + app?: NestFastifyApplication + + constructor (readonly container: MasterNodeRegTestContainer) { + } + + async start (): Promise { + this.app = await createTestingApp(this.container) + } + + async stop (): Promise { + this.app?.close() + } + + async waitForAddressTxCount (address: string, txCount: number, timeout: number = 15000): Promise { + const hid = addressToHid('regtest', address) + const aggregationMapper = this.app?.get(ScriptAggregationMapper) + if (aggregationMapper === undefined) { + throw new Error('StubService not initialized yet') + } + await waitForExpect(async () => { + const agg = await aggregationMapper.getLatest(hid) + expect(agg?.statistic.txCount).toStrictEqual(txCount) + }, timeout) + } + + async waitForIndexedHeight (height: number, timeout: number = 30000): Promise { + const blockMapper = this.app?.get(BlockMapper) + if (blockMapper === undefined) { + throw new Error('StubService not initialized yet') + } + await waitForExpect(async () => { + const block = await blockMapper.getHighest() + await expect(block?.height).toBeGreaterThan(height) + }, timeout) + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + + async waitForIndexedTimestamp (container: MasterNodeRegTestContainer, timestamp: number, timeout: number = 30000): Promise { + await waitForExpect(async () => { + await container.generate(1) + const height = await container.call('getblockcount') + const stats = await container.call('getblockstats', [height]) + await expect(Number(stats.time)).toStrictEqual(timestamp) + }, timeout) + } +} diff --git a/packages/whale-api-client/__tests__/whale.api.response.test.ts b/packages/whale-api-client/__tests__/whale.api.response.test.ts new file mode 100644 index 0000000000..dfb09754c4 --- /dev/null +++ b/packages/whale-api-client/__tests__/whale.api.response.test.ts @@ -0,0 +1,64 @@ +import { WhaleApiResponse, ApiPagedResponse } from '../src' + +it('should behavior as an array', () => { + const response: WhaleApiResponse = { + data: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + } + + const pagination = new ApiPagedResponse(response, 'GET', '/') + + expect(pagination.length).toStrictEqual(10) + expect(pagination[0]).toStrictEqual(0) + expect(pagination[4]).toStrictEqual(4) + expect(pagination[9]).toStrictEqual(9) + expect(pagination[10]).toBeUndefined() +}) + +it('should have next token', () => { + const response: WhaleApiResponse = { + data: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + page: { + next: '9' + } + } + + const pagination = new ApiPagedResponse(response, 'GET', '/items') + + expect(pagination.hasNext).toStrictEqual(true) + expect(pagination.nextToken).toStrictEqual('9') +}) + +it('should not have next', () => { + const response: WhaleApiResponse = { + data: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + } + + const pagination = new ApiPagedResponse(response, 'GET', '/items') + + expect(pagination.hasNext).toStrictEqual(false) + expect(pagination.nextToken).toBeUndefined() +}) + +it('should be able to filter', () => { + const response: WhaleApiResponse = { + data: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + } + + const pagination = new ApiPagedResponse(response, 'GET', '/items') + + expect(pagination.filter(value => value % 2 === 0)).toStrictEqual([ + 0, 2, 4, 6, 8 + ]) +}) + +it('should be able to map', () => { + const response: WhaleApiResponse = { + data: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + } + + const pagination = new ApiPagedResponse(response, 'GET', '/items') + + expect(pagination.map(value => value * 11)).toStrictEqual([ + 0, 11, 22, 33, 44, 55, 66, 77, 88, 99 + ]) +}) diff --git a/packages/whale-api-client/__tests__/whale.rpc.client.test.ts b/packages/whale-api-client/__tests__/whale.rpc.client.test.ts new file mode 100644 index 0000000000..7cce8fa854 --- /dev/null +++ b/packages/whale-api-client/__tests__/whale.rpc.client.test.ts @@ -0,0 +1,69 @@ +import { MasterNodeRegTestContainer } from '@defichain/testcontainers' +import { WhaleRpcClient } from '../src' +import { StubService } from './stub.service' +import { StubWhaleRpcClient } from './stub.client' + +let container: MasterNodeRegTestContainer +let service: StubService +let client: WhaleRpcClient + +beforeAll(async () => { + container = new MasterNodeRegTestContainer() + service = new StubService(container) + client = new StubWhaleRpcClient(service) + + await container.start() + await service.start() +}) + +afterAll(async () => { + try { + await service.stop() + } finally { + await container.stop() + } +}) + +it('should not be able to access wallet', async () => { + return await expect(async () => { + await client.wallet.getBalance() + }).rejects.toThrow('ClientApiError: 422 - Unprocessable Entity') +}) + +it('should not be able to access accounts', async () => { + return await expect(async () => { + await client.account.listAccountHistory() + }).rejects.toThrow('ClientApiError: 422 - Unprocessable Entity') +}) + +describe('whitelisted rpc methods', () => { + it('should client.blockchain.getBlockchainInfo()', async () => { + const info = await client.blockchain.getBlockchainInfo() + + expect(info.chain).toStrictEqual('regtest') + expect(typeof info.blocks).toStrictEqual('number') + }) + + it('should client.blockchain.getBlockCount()', async () => { + const count = await client.blockchain.getBlockCount() + + expect(typeof count).toStrictEqual('number') + }) + + it('should client.blockchain.getBlockHash(1)', async () => { + await container.generate(1) + + const hash = await client.blockchain.getBlockHash(1) + expect(hash.length).toStrictEqual(64) + }) + + it('should client.blockchain.getBlock(hash, 2)', async () => { + await container.generate(1) + + const hash = await client.blockchain.getBlockHash(1) + const block = await client.blockchain.getBlock(hash, 2) + + expect(block.hash.length).toStrictEqual(64) + expect(Array.isArray(block.tx)).toStrictEqual(true) + }) +}) diff --git a/packages/whale-api-client/src/Version.ts b/packages/whale-api-client/src/Version.ts index 5675ebbf1d..a886d4f0f0 100644 --- a/packages/whale-api-client/src/Version.ts +++ b/packages/whale-api-client/src/Version.ts @@ -1,2 +1 @@ -/* eslint-disable import/no-default-export */ export const version = 'v0.0'