diff --git a/docs/node/CATEGORIES/06-poolpair.md b/docs/node/CATEGORIES/06-poolpair.md index 7e64c1734a..e7b8f4ca3d 100644 --- a/docs/node/CATEGORIES/06-poolpair.md +++ b/docs/node/CATEGORIES/06-poolpair.md @@ -214,7 +214,11 @@ Create a test pool swap transaction to check pool swap's return result ```ts title="client.poolpair.testPoolSwap()" interface poolpair { - testPoolSwap (metadata: PoolSwapMetadata): Promise + testPoolSwap ( + metadata: PoolSwapMetadata, + path: 'auto' | 'direct' = 'direct', + verbose: boolean = false + ): Promise } interface PoolSwapMetadata { @@ -225,6 +229,12 @@ interface PoolSwapMetadata { tokenTo: string maxPrice?: number } + +interface EstimatedCompositePath { + amount: string, + path: 'auto' | 'direct', + pools: string[] +} ``` ## removePoolLiquidity diff --git a/packages/jellyfish-api-core/__tests__/category/poolpair/testpoolswap.test.ts b/packages/jellyfish-api-core/__tests__/category/poolpair/testpoolswap.test.ts index c199986973..d6813e9199 100644 --- a/packages/jellyfish-api-core/__tests__/category/poolpair/testpoolswap.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/poolpair/testpoolswap.test.ts @@ -1,9 +1,17 @@ import { MasterNodeRegTestContainer } from '@defichain/testcontainers' import { ContainerAdapterClient } from '../../container_adapter_client' import BigNumber from 'bignumber.js' -import { addPoolLiquidity, createPoolPair, createToken, getNewAddress, mintTokens } from '@defichain/testing' +import { + addPoolLiquidity, + createPoolPair, + createToken, + getNewAddress, + mintTokens, + sendTokensToAddress +} from '@defichain/testing' import { RpcApiError } from '../../../src' import { poolpair } from '@defichain/jellyfish-api-core' +import { EstimatedCompositePath } from '../../../src/category/poolpair' describe('Poolpair', () => { const container = new MasterNodeRegTestContainer() @@ -49,7 +57,7 @@ describe('Poolpair', () => { const reserveAAfter: BigNumber = new BigNumber(poolpair.reserveA).plus(666) const reserveBAfter: BigNumber = new BigNumber(poolpair.totalLiquidity).pow(2).div(reserveAAfter) - const result = await client.poolpair.testPoolSwap({ + const result = await client.poolpair.testPoolSwap({ from: tokenAddress, tokenFrom: 'CAT', amountFrom: 666, @@ -80,7 +88,7 @@ describe('Poolpair', () => { shareAddress: poolLiquidityAddress }) - const receive = await client.poolpair.testPoolSwap({ + const receive = await client.poolpair.testPoolSwap({ from: tokenAddress, tokenFrom: 'ELF', amountFrom: 666, @@ -113,7 +121,7 @@ describe('Poolpair', () => { shareAddress: poolLiquidityAddress }) - const promise = client.poolpair.testPoolSwap({ + const promise = client.poolpair.testPoolSwap({ from: tokenAddress, tokenFrom: 'FOX', amountFrom: 666, @@ -147,7 +155,7 @@ describe('Poolpair', () => { const poolpairResultBefore: poolpair.PoolPairsResult = await container.call('getpoolpair', ['DOG-DFI']) expect(Object.keys(poolpairResultBefore).length).toStrictEqual(1) - await client.poolpair.testPoolSwap({ + await client.poolpair.testPoolSwap({ from: tokenAddress, tokenFrom: 'CAT', amountFrom: 666, @@ -167,7 +175,7 @@ describe('Poolpair', () => { await mintTokens(container, 'BAT') await createPoolPair(container, 'BAT', 'DFI') - const promise = client.poolpair.testPoolSwap({ + const promise = client.poolpair.testPoolSwap({ from: tokenBatAddress, tokenFrom: 'BAT', amountFrom: 13, @@ -177,4 +185,207 @@ describe('Poolpair', () => { await expect(promise).rejects.toThrow(RpcApiError) await expect(promise).rejects.toThrow('Lack of liquidity') }) + + it.skip('testpoolswap does direct swap when \'auto\' path specified and direct swap available', async () => { + // Create ZOO-DFI pool and add liquidity + const zooDfiPoolAddress = await getNewAddress(container) + await createToken(container, 'ZOO') + await mintTokens(container, 'ZOO') + await createPoolPair(container, 'ZOO', 'DFI') + await addPoolLiquidity(container, { + tokenA: 'ZOO', + amountA: 1000, + tokenB: 'DFI', + amountB: 500, + shareAddress: zooDfiPoolAddress + }) + + // Create an address that has ZOO and wants to swap for DFI + const fromAddress = await getNewAddress(container) + await sendTokensToAddress(container, fromAddress, 1000, 'ZOO') + + // Get the testPoolSwap result as '@' + const metadata = { + from: fromAddress, + to: zooDfiPoolAddress, + tokenFrom: 'ZOO', + tokenTo: 'DFI', + amountFrom: 666 + } + const testResult = await client.poolpair.testPoolSwap(metadata, 'auto') + const testResultVerbose = await client.poolpair.testPoolSwap(metadata, 'auto', true) + + // TODO(limeli): should be a direct swap, but it's making a composite swap instead. Why? + expect(testResultVerbose.amount).toStrictEqual(testResult) + expect(testResultVerbose.path).toStrictEqual('auto') + expect(testResultVerbose.pools).toStrictEqual(['12']) + expect(testResult).toStrictEqual('199.87995198@0') + + // Actually perform the swap + const txn = await client.poolpair.poolSwap({ + from: fromAddress, + to: zooDfiPoolAddress, + tokenFrom: 'ZOO', + tokenTo: 'DFI', + amountFrom: 666 + }) + expect(txn.length).toStrictEqual(64) + await container.generate(1) + + // Verify ZOO-DFI pool state + const zooDfiPair: poolpair.PoolPairsResult = await container.call('getpoolpair', ['ZOO-DFI']) + const zooDfiPool: poolpair.PoolPairInfo = Object.values(zooDfiPair)[0] + + expect(new BigNumber(zooDfiPool.reserveA).toFixed(4)) // ZOO + .toStrictEqual(new BigNumber(1666).toFixed(4)) + + expect(new BigNumber(zooDfiPool.reserveB).toFixed(4)) // DFI + .toStrictEqual(new BigNumber(300.120048).toFixed(4)) + + // Check test swap result against actual swap result + const testResultDfi = new BigNumber(testResult.split('@')[0]) + expect(testResultDfi.toFixed(4)) + .toStrictEqual( // initial dfi reserve - current dfi reserve + new BigNumber(500).minus(new BigNumber(zooDfiPool.reserveB)).toFixed(4) + ) + }) + + it('testpoolswap does compositeswap when \'auto\' path specified and no direct swap available', async () => { + // Create BEE-DFI pool and add liquidity + const beeDfiPoolAddress = await getNewAddress(container) + await createToken(container, 'BEE') + await mintTokens(container, 'BEE') + await createPoolPair(container, 'BEE', 'DFI') + await addPoolLiquidity(container, { + tokenA: 'BEE', + amountA: 1000, + tokenB: 'DFI', + amountB: 500, + shareAddress: beeDfiPoolAddress + }) + + // Create FLY-DFI pool and add liquidity + const flyDfiPoolAddress = await getNewAddress(container) + await createToken(container, 'FLY') + await mintTokens(container, 'FLY') + await createPoolPair(container, 'FLY', 'DFI') + await addPoolLiquidity(container, { + tokenA: 'FLY', + amountA: 2000, + tokenB: 'DFI', + amountB: 500, + shareAddress: flyDfiPoolAddress + }) + + // Create an address that has BEE and wants to swap for FLY + const fromAddress = await getNewAddress(container) + await sendTokensToAddress(container, fromAddress, 1000, 'BEE') + + // Get the testPoolSwap result as '@' + const testResult = await client.poolpair.testPoolSwap({ + from: fromAddress, + to: beeDfiPoolAddress, + tokenFrom: 'BEE', + tokenTo: 'FLY', + amountFrom: 666 + }, 'auto') + + // Actually perform the composite swap, then check it against testPoolSwap result + const txn = await client.poolpair.compositeSwap({ + from: fromAddress, + to: beeDfiPoolAddress, + tokenFrom: 'BEE', + tokenTo: 'FLY', + amountFrom: 666 + }) + expect(txn.length).toStrictEqual(64) + await container.generate(1) + + // Verify BEE-DFI pool state + const beeDfiPair: poolpair.PoolPairsResult = await container.call('getpoolpair', ['BEE-DFI']) + const beeDfiPool: poolpair.PoolPairInfo = Object.values(beeDfiPair)[0] + + expect(new BigNumber(beeDfiPool.reserveA).toFixed(4)) // BEE + .toStrictEqual(new BigNumber(1666).toFixed(4)) + + expect(new BigNumber(beeDfiPool.reserveB).toFixed(4)) // DFI + .toStrictEqual(new BigNumber(300.120048).toFixed(4)) + + // Verify FLY-DFI pool state + const flyDfiPair: poolpair.PoolPairsResult = await container.call('getpoolpair', ['FLY-DFI']) + const flyDfiPool: poolpair.PoolPairInfo = Object.values(flyDfiPair)[0] + + expect(new BigNumber(flyDfiPool.reserveA).toFixed(4)) // FLY + .toStrictEqual(new BigNumber(1428.816467).toFixed(4)) + + expect(new BigNumber(flyDfiPool.reserveB).toFixed(4)) // DFI + .toStrictEqual(new BigNumber(699.879952).toFixed(4)) + + // Compare testpoolswap compositeswap result with actual compositeswap result + // 666 BEE for 571.18353344 FLY + const testResultFlyAmount = new BigNumber(testResult.split('@')[0]) + expect(testResultFlyAmount.toFixed(4)) + .toStrictEqual(new BigNumber(571.1835).toFixed(4)) + const flyReserveDiff = new BigNumber(2000).minus(new BigNumber(flyDfiPool.reserveA)) + + expect(testResultFlyAmount.toFixed(4)).toStrictEqual(flyReserveDiff.toFixed(4)) + }) + + it('testpoolswap(..., \'direct\') should fail if no pool exists for a direct swap', async () => { + // GOO-DFI + const tokenAddress = await getNewAddress(container) + await createToken(container, 'GOO') + await mintTokens(container, 'GOO') + await createPoolPair(container, 'GOO', 'DFI') + + // LOO-DFI + await createToken(container, 'LOO') + await mintTokens(container, 'LOO') + await createPoolPair(container, 'LOO', 'DFI') + + // Try to swap GOO-LOO directly + const promise = client.poolpair.testPoolSwap({ + from: tokenAddress, + tokenFrom: 'GOO', + amountFrom: 13, + to: await getNewAddress(container), + tokenTo: 'LOO' + }, 'direct') + await expect(promise) + .rejects + .toThrow('RpcApiError: \'Direct pool pair not found. ' + + 'Use \'auto\' mode to use composite swap.\', code: -32600, method: testpoolswap') + }) + + it('should return swap path when verbose=true', async () => { + // Create FOO-DFI pool and add liquidity + const tokenAddress = await getNewAddress(container) + await createToken(container, 'FOO') + await mintTokens(container, 'FOO') + await createPoolPair(container, 'FOO', 'DFI') + await addPoolLiquidity(container, { + tokenA: 'FOO', + amountA: 1000, + tokenB: 'DFI', + amountB: 500, + shareAddress: tokenAddress + }) + + const metadata = { + from: await getNewAddress(container), + to: tokenAddress, + tokenFrom: 'FOO', + tokenTo: 'DFI', + amountFrom: 666 + } + const testResult = await client.poolpair.testPoolSwap(metadata, 'direct') + const testResultVerbose = await client.poolpair.testPoolSwap(metadata, 'direct', true) + + const amount = testResult.split('@')[0] + const amountFromVerbose = testResult.split('@')[0] + expect(amount).toStrictEqual('199.87995198') + expect(amountFromVerbose).toStrictEqual(amount) + expect(testResultVerbose.path).toStrictEqual('direct') + expect(testResultVerbose.pools).toStrictEqual(['20']) + }) }) diff --git a/packages/jellyfish-api-core/src/category/poolpair.ts b/packages/jellyfish-api-core/src/category/poolpair.ts index 02a0ac1930..e1fac32cdb 100644 --- a/packages/jellyfish-api-core/src/category/poolpair.ts +++ b/packages/jellyfish-api-core/src/category/poolpair.ts @@ -147,16 +147,23 @@ export class PoolPair { * Create a test pool swap transaction to check pool swap's return result * * @param {PoolSwapMetadata} metadata a provided information to create test pool swap transaction + * @param {'auto' | 'direct'} [path='direct'] one of auto/direct. + * Note: the default will be switched to auto in the upcoming versions. + * - auto: automatically use composite swap or direct swap as needed. + * - direct: uses direct path only or fails. + * @param {boolean} [verbose=false] returns estimated composite path when true, + * otherwise returns a string formatted as 'amount@token' swapped. * @param {string} metadata.from address of the owner of tokenFrom * @param {string} metadata.tokenFrom swap from token {symbol/id} * @param {number} metadata.amountFrom amount from tokenA * @param {string} metadata.to address of the owner of tokenTo * @param {string} metadata.tokenTo swap to token {symbol/id} * @param {number} [metadata.maxPrice] acceptable max price - * @return {Promise} formatted as 'amount@token' swapped + * @return {Promise} formatted as 'amount@token' swapped or + * the estimated composite path. */ - async testPoolSwap (metadata: PoolSwapMetadata): Promise { - return await this.client.call('testpoolswap', [metadata], 'bignumber') + async testPoolSwap(metadata: PoolSwapMetadata, path: 'auto' | 'direct' = 'direct', verbose: boolean = false): Promise { + return await this.client.call('testpoolswap', [metadata, path, verbose], 'bignumber') } /** @@ -255,3 +262,9 @@ export interface PoolSwapMetadata { tokenTo: string maxPrice?: number } + +export interface EstimatedCompositePath { + amount: string + path: 'auto' | 'direct' + pools: string[] +}