diff --git a/apps/wallet-mobile/src/TxHistory/TxHistoryNavigator.tsx b/apps/wallet-mobile/src/TxHistory/TxHistoryNavigator.tsx index 0e560bbff2..17934c92ec 100644 --- a/apps/wallet-mobile/src/TxHistory/TxHistoryNavigator.tsx +++ b/apps/wallet-mobile/src/TxHistory/TxHistoryNavigator.tsx @@ -1,5 +1,5 @@ import {createStackNavigator} from '@react-navigation/stack' -import {swapApiMaker, swapManagerMaker, SwapProvider, swapStorageMaker} from '@yoroi/swap' +import {supportedProviders, swapApiMaker, swapManagerMaker, SwapProvider, swapStorageMaker} from '@yoroi/swap' import React from 'react' import {defineMessages, useIntl} from 'react-intl' import {StyleSheet, Text, TouchableOpacity, TouchableOpacityProps} from 'react-native' @@ -53,6 +53,7 @@ export const TxHistoryNavigator = () => { isMainnet: wallet.networkId !== 300, stakingKey, primaryTokenId: wallet.primaryTokenInfo.id, + supportedProviders, }), [wallet.networkId, stakingKey, wallet.primaryTokenInfo.id], ) diff --git a/apps/wallet-mobile/src/features/Swap/common/SelectPool/SelectPoolFromList/SelectPoolFromList.stories.tsx b/apps/wallet-mobile/src/features/Swap/common/SelectPool/SelectPoolFromList/SelectPoolFromList.stories.tsx index ba194e6fe5..24f5de36b7 100644 --- a/apps/wallet-mobile/src/features/Swap/common/SelectPool/SelectPoolFromList/SelectPoolFromList.stories.tsx +++ b/apps/wallet-mobile/src/features/Swap/common/SelectPool/SelectPoolFromList/SelectPoolFromList.stories.tsx @@ -18,13 +18,14 @@ storiesOf('Swap List Pool', module) { tokenA: {quantity: '4500000', tokenId: 'tokenA'}, tokenB: {quantity: '9000000', tokenId: 'tokenB'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', fee: '0.3', // 0.3% provider: 'minswap', price: 2, batcherFee: {quantity: '1', tokenId: ''}, deposit: {quantity: '1', tokenId: ''}, poolId: '0', - lastUpdate: '0', lpToken: { quantity: '0', tokenId: '0', @@ -33,13 +34,14 @@ storiesOf('Swap List Pool', module) { tokenA: {quantity: '4500000', tokenId: 'tokenA'}, tokenB: {quantity: '9000000', tokenId: 'tokenB'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', fee: '0.3', // 0.3% provider: 'sundaeswap', price: 6, batcherFee: {quantity: '122', tokenId: ''}, deposit: {quantity: '432', tokenId: ''}, poolId: '23455', - lastUpdate: '235', lpToken: { quantity: '13524', tokenId: '1355', diff --git a/apps/wallet-mobile/src/features/Swap/common/mocks.ts b/apps/wallet-mobile/src/features/Swap/common/mocks.ts index c336ec767f..d81ec192ca 100644 --- a/apps/wallet-mobile/src/features/Swap/common/mocks.ts +++ b/apps/wallet-mobile/src/features/Swap/common/mocks.ts @@ -26,7 +26,6 @@ export const mocks = { batcherFee: {quantity: asQuantity(2500000), tokenId: ''}, deposit: {quantity: asQuantity(2000000), tokenId: ''}, fee: '0.05', - lastUpdate: '2023-09-08 09:56:13', lpToken: { quantity: asQuantity(68917682), tokenId: '0029cb7c88c7567b63d1a512c0ed626aa169688ec980730c0473b913.6c702083', @@ -39,6 +38,8 @@ export const mocks = { quantity: asQuantity(231696922), tokenId: '208a2ca888886921513cb777bb832a8dc685c04de990480151f12150.53484942414441', }, + ptPriceTokenA: '0', + ptPriceTokenB: '0', }, slippage: 1, type: 'market' as Type, diff --git a/apps/wallet-mobile/translations/messages/src/TxHistory/TxHistoryNavigator.json b/apps/wallet-mobile/translations/messages/src/TxHistory/TxHistoryNavigator.json index 2ed0e5e7e1..3b2e8e6844 100644 --- a/apps/wallet-mobile/translations/messages/src/TxHistory/TxHistoryNavigator.json +++ b/apps/wallet-mobile/translations/messages/src/TxHistory/TxHistoryNavigator.json @@ -4,14 +4,14 @@ "defaultMessage": "!!!Receive", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 266, + "line": 267, "column": 16, - "index": 9262 + "index": 9310 }, "end": { - "line": 269, + "line": 270, "column": 3, - "index": 9351 + "index": 9399 } }, { @@ -19,14 +19,14 @@ "defaultMessage": "!!!Swap", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 270, + "line": 271, "column": 13, - "index": 9366 + "index": 9414 }, "end": { - "line": 273, + "line": 274, "column": 3, - "index": 9439 + "index": 9487 } }, { @@ -34,14 +34,14 @@ "defaultMessage": "!!!Swap from", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 274, + "line": 275, "column": 17, - "index": 9458 + "index": 9506 }, "end": { - "line": 277, + "line": 278, "column": 3, - "index": 9535 + "index": 9583 } }, { @@ -49,14 +49,14 @@ "defaultMessage": "!!!Swap to", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 278, + "line": 279, "column": 15, - "index": 9552 + "index": 9600 }, "end": { - "line": 281, + "line": 282, "column": 3, - "index": 9625 + "index": 9673 } }, { @@ -64,14 +64,14 @@ "defaultMessage": "!!!Slippage Tolerance", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 282, + "line": 283, "column": 21, - "index": 9648 + "index": 9696 }, "end": { - "line": 285, + "line": 286, "column": 3, - "index": 9743 + "index": 9791 } }, { @@ -79,14 +79,14 @@ "defaultMessage": "!!!Select pool", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 286, + "line": 287, "column": 14, - "index": 9759 + "index": 9807 }, "end": { - "line": 289, + "line": 290, "column": 3, - "index": 9840 + "index": 9888 } }, { @@ -94,14 +94,14 @@ "defaultMessage": "!!!Send", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 290, + "line": 291, "column": 13, - "index": 9855 + "index": 9903 }, "end": { - "line": 293, + "line": 294, "column": 3, - "index": 9935 + "index": 9983 } }, { @@ -109,14 +109,14 @@ "defaultMessage": "!!!Scan QR code address", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 294, + "line": 295, "column": 18, - "index": 9955 + "index": 10003 }, "end": { - "line": 297, + "line": 298, "column": 3, - "index": 10056 + "index": 10104 } }, { @@ -124,14 +124,14 @@ "defaultMessage": "!!!Select asset", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 298, + "line": 299, "column": 20, - "index": 10078 + "index": 10126 }, "end": { - "line": 301, + "line": 302, "column": 3, - "index": 10167 + "index": 10215 } }, { @@ -139,14 +139,14 @@ "defaultMessage": "!!!Selected tokens", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 302, + "line": 303, "column": 26, - "index": 10195 + "index": 10243 }, "end": { - "line": 305, + "line": 306, "column": 3, - "index": 10299 + "index": 10347 } }, { @@ -154,14 +154,14 @@ "defaultMessage": "!!!Edit amount", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 306, + "line": 307, "column": 19, - "index": 10320 + "index": 10368 }, "end": { - "line": 309, + "line": 310, "column": 3, - "index": 10413 + "index": 10461 } }, { @@ -169,14 +169,14 @@ "defaultMessage": "!!!Confirm", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 310, + "line": 311, "column": 16, - "index": 10431 + "index": 10479 }, "end": { - "line": 313, + "line": 314, "column": 3, - "index": 10517 + "index": 10565 } }, { @@ -184,14 +184,14 @@ "defaultMessage": "!!!Share this address to receive payments. To protect your privacy, new addresses are generated automatically once you use them.", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 314, + "line": 315, "column": 19, - "index": 10538 + "index": 10586 }, "end": { - "line": 320, + "line": 321, "column": 3, - "index": 10776 + "index": 10824 } }, { @@ -199,14 +199,14 @@ "defaultMessage": "!!!Confirm transaction", "file": "src/TxHistory/TxHistoryNavigator.tsx", "start": { - "line": 321, + "line": 322, "column": 27, - "index": 10805 + "index": 10853 }, "end": { - "line": 324, + "line": 325, "column": 3, - "index": 10898 + "index": 10946 } } ] \ No newline at end of file diff --git a/packages/openswap/src/api.ts b/packages/openswap/src/api.ts index 80a2d9eb60..1b5bc17e48 100644 --- a/packages/openswap/src/api.ts +++ b/packages/openswap/src/api.ts @@ -5,15 +5,16 @@ import { getCompletedOrders, getOrders, // returns all orders for a given stake key hash. } from './orders' -import {getPools} from './pools' import {getTokens} from './tokens' import { CancelOrderRequest, CreateOrderRequest, Network, + Provider, TokenAddress, } from './types' import {axiosClient} from './config' +import {getLiquidityPools, getPoolsPair} from './pools' export class OpenSwapApi { constructor( @@ -51,19 +52,34 @@ export class OpenSwapApi { ) } - public async getPools({ + public async getPoolsPair({ tokenA, tokenB, }: { tokenA: TokenAddress tokenB: TokenAddress }) { - return getPools( + return getPoolsPair( {network: this.network, client: this.client}, {tokenA, tokenB}, ) } + public async getLiquidityPools({ + tokenA, + tokenB, + providers, + }: { + tokenA: string + tokenB: string + providers: ReadonlyArray + }) { + return getLiquidityPools( + {network: this.network, client: this.client}, + {tokenA, tokenB, providers}, + ) + } + public async getTokens({policyId = '', assetName = ''} = {}) { const tokens = await getTokens( {network: this.network, client: this.client}, @@ -74,4 +90,19 @@ export class OpenSwapApi { } } -const supportedNetworks: Network[] = ['mainnet', 'preprod'] +export const supportedNetworks: ReadonlyArray = [ + 'mainnet', + 'preprod', +] as const + +export const supportedProviders: ReadonlyArray = [ + 'minswap', + 'muesliswap_v1', + 'muesliswap_v2', + 'muesliswap_v3', + 'muesliswap_v4', + 'spectrum', + 'sundaeswap', + 'vyfi', + 'wingriders', +] as const diff --git a/packages/openswap/src/config.ts b/packages/openswap/src/config.ts index 67ec4d65e8..4cde8a0202 100644 --- a/packages/openswap/src/config.ts +++ b/packages/openswap/src/config.ts @@ -2,7 +2,8 @@ import axios from 'axios' export const SWAP_API_ENDPOINTS = { mainnet: { - getPools: 'https://onchain2.muesliswap.com/pools/pair', + getPoolsPair: 'https://onchain2.muesliswap.com/pools/pair', + getLiquidityPools: 'https://api.muesliswap.com/liquidity/pools', getOrders: 'https://onchain2.muesliswap.com/orders/all/', getCompletedOrders: 'https://api.muesliswap.com/orders/v2', getTokens: 'https://api.muesliswap.com/list', @@ -11,7 +12,8 @@ export const SWAP_API_ENDPOINTS = { 'https://aggregator.muesliswap.com/cancelSwapTransaction', }, preprod: { - getPools: 'https://preprod.pools.muesliswap.com/pools/pair', + getPoolsPair: 'https://preprod.pools.muesliswap.com/pools/pair', + getLiquidityPools: 'https://preprod.api.muesliswap.com/liquidity/pools', getOrders: 'https://preprod.pools.muesliswap.com/orders/all/', getCompletedOrders: 'https://api.muesliswap.com/orders/v2', getTokens: 'https://preprod.api.muesliswap.com/list', diff --git a/packages/openswap/src/index.ts b/packages/openswap/src/index.ts index 5ff63bff32..e7bf1e3e66 100644 --- a/packages/openswap/src/index.ts +++ b/packages/openswap/src/index.ts @@ -2,10 +2,10 @@ export * from './api' import * as Types from './types' export namespace OpenSwap { - export type Protocol = Types.Protocol + export type Provider = Types.Provider export type Network = Types.Network - // Order + // Orders export type CreateOrderRequest = Types.CreateOrderRequest export type CreateOrderResponse = Types.CreateOrderResponse export type CancelOrderRequest = Types.CancelOrderRequest @@ -14,11 +14,13 @@ export namespace OpenSwap { export type CompletedOrder = Types.CompletedOrder export type CompletedOrderResponse = Types.CompletedOrderResponse - // Pool - export type Pool = Types.Pool - export type PoolResponse = Types.PoolResponse + // Pools + export type PoolPair = Types.PoolPair + export type PoolPairResponse = Types.PoolPairResponse + export type LiquidityPool = Types.LiquidityPool + export type LiquidityPoolResponse = Types.LiquidityPoolResponse - // Token + // Tokens export type Token = Types.Token export type TokenResponse = Types.TokenResponse export type TokenAddress = Types.TokenAddress diff --git a/packages/openswap/src/pools.spec.ts b/packages/openswap/src/pools.spec.ts index 02d34ba80e..ad09bd9101 100644 --- a/packages/openswap/src/pools.spec.ts +++ b/packages/openswap/src/pools.spec.ts @@ -1,39 +1,83 @@ import {describe, expect, it, vi, Mocked} from 'vitest' -import {getPools} from './pools' +import {getLiquidityPools, getPoolsPair} from './pools' import {axiosClient} from './config' +import {LiquidityPoolResponse, PoolPairResponse} from './types' vi.mock('./config.ts') describe('SwapPoolsApi', () => { - it('should get pools list for a given token pair', async () => { - const mockAxios = axiosClient as Mocked - mockAxios.get.mockImplementationOnce(() => - Promise.resolve({ - status: 200, - data: mockedPoolRes, - }), - ) + describe('getLiquidityPools', () => { + it('should get liquidity pools list for a given token pair', async () => { + const mockAxios = axiosClient as Mocked + mockAxios.get.mockImplementationOnce(() => + Promise.resolve({ + status: 200, + data: mockedLiquidityPoolsResponse, + }), + ) + + const result = await getLiquidityPools( + {network: 'mainnet', client: mockAxios}, + { + tokenA: getLiquidityPoolsParams.sell, + tokenB: getLiquidityPoolsParams.buy, + providers: getLiquidityPoolsParams.providers, + }, + ) + expect(result).to.be.of.lengthOf(1) + }) - const result = await getPools( - {network: 'mainnet', client: mockAxios}, - {tokenA: getPoolsParams.sell, tokenB: getPoolsParams.buy}, - ) - expect(result).to.be.of.lengthOf(1) + it('should throw error for invalid response', async () => { + const mockAxios = axiosClient as Mocked + await expect(async () => { + mockAxios.get.mockImplementationOnce(() => + Promise.resolve({status: 500}), + ) + await getLiquidityPools( + {network: 'preprod', client: mockAxios}, + { + tokenA: getLiquidityPoolsParams.sell, + tokenB: getLiquidityPoolsParams.buy, + providers: getLiquidityPoolsParams.providers, + }, + ) + }).rejects.toThrow('Failed to fetch liquidity pools for token pair') + }) }) - it('should throw error for invalid response', async () => { - const mockAxios = axiosClient as Mocked - await expect(async () => { - mockAxios.get.mockImplementationOnce(() => Promise.resolve({status: 500})) - await getPools( - {network: 'preprod', client: mockAxios}, - {tokenA: getPoolsParams.sell, tokenB: getPoolsParams.buy}, + describe('getPoolsPair', () => { + it('should get pools pair list for a given token pair', async () => { + const mockAxios = axiosClient as Mocked + mockAxios.get.mockImplementationOnce(() => + Promise.resolve({ + status: 200, + data: mockedPoolsPairResponse, + }), + ) + + const result = await getPoolsPair( + {network: 'mainnet', client: mockAxios}, + {tokenA: getPoolsPairParams.sell, tokenB: getPoolsPairParams.buy}, ) - }).rejects.toThrow('Failed to fetch pools for token pair') + expect(result).to.be.of.lengthOf(1) + }) + + it('should throw error for invalid response', async () => { + const mockAxios = axiosClient as Mocked + await expect(async () => { + mockAxios.get.mockImplementationOnce(() => + Promise.resolve({status: 500}), + ) + await getPoolsPair( + {network: 'preprod', client: mockAxios}, + {tokenA: getPoolsPairParams.sell, tokenB: getPoolsPairParams.buy}, + ) + }).rejects.toThrow('Failed to fetch pools pair for token pair') + }) }) }) -const mockedPoolRes = [ +const mockedPoolsPairResponse: Readonly = [ { provider: 'minswap', fee: '0.3', @@ -46,7 +90,7 @@ const mockedPoolRes = [ token: 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72.43414b45', }, - price: 1581804.726923077, + price: 0, batcherFee: { amount: '2000000', token: '.', @@ -65,10 +109,12 @@ const mockedPoolRes = [ token: 'e4214b7cce62ac6fbba385d164df48e157eae5863521b4b67ca71d86.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', }, + batcherAddress: + 'addr1wxaptpmxcxawvr3pzlhgnpmzz3ql43n2tc8mn3av5kx0yzs09tqh8', }, ] -const getPoolsParams = { +const getPoolsPairParams = { sell: { policyId: '', assetNameHex: '', @@ -78,3 +124,69 @@ const getPoolsParams = { assetNameHex: '43414b45', }, } as const + +const mockedLiquidityPoolsResponse: Readonly = [ + { + tokenA: { + address: { + policyId: '', + name: '', + }, + symbol: 'ADA', + image: 'https://static.muesliswap.com/images/tokens/ada.png', + decimalPlaces: 6, + amount: '1000000', + status: 'verified', + priceAda: 1, + }, + tokenB: { + address: { + policyId: '9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77', + name: '53554e444145', + }, + symbol: 'SUNDAE', + image: + 'https://tokens.muesliswap.com/static/img/tokens/9a9693a9a37912a5097918f97918d15240c92ab729a0b7c4aa144d77.53554e444145.png', + decimalPlaces: 6, + amount: '100000', + status: 'verified', + priceAda: 0.02567846556, + }, + feeToken: { + address: { + policyId: '', + name: '', + }, + symbol: 'ADA', + image: 'https://static.muesliswap.com/images/tokens/ada.png', + decimalPlaces: 6, + }, + batcherFee: '2500000', + lvlDeposit: '2000000', + poolFee: '1.00', + lpToken: { + address: { + policyId: '0029cb7c88c7567b63d1a512c0ed626aa169688ec980730c0473b913', + name: '6c7020dc', + }, + amount: '316227', + }, + poolId: '0029cb7c88c7567b63d1a512c0ed626aa169688ec980730c0473b913.7020dc', + provider: 'sundaeswap', + txHash: 'f2c5186fc53546db16a52c3bec25598e69518aaa8486919074c42e8927533f4c', + outputIdx: 1, + volume24h: 0, + volume7d: 0, + liquidityApy: 0, + priceASqrt: null, + priceBSqrt: null, + batcherAddress: + 'addr1wxaptpmxcxawvr3pzlhgnpmzz3ql43n2tc8mn3av5kx0yzs09tqh8', + }, +] + +const getLiquidityPoolsParams = { + sell: '', + buy: 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72.43414b45', + providers: ['minswap'], +} as const diff --git a/packages/openswap/src/pools.ts b/packages/openswap/src/pools.ts index 8da62645fd..23b89fd41e 100644 --- a/packages/openswap/src/pools.ts +++ b/packages/openswap/src/pools.ts @@ -1,10 +1,44 @@ import {SWAP_API_ENDPOINTS} from './config' -import type {ApiDeps, PoolResponse, TokenAddress} from './types' +import type { + ApiDeps, + LiquidityPoolResponse, + PoolPairResponse, + Provider, + TokenAddress, +} from './types' -export async function getPools( +export async function getLiquidityPools( + deps: ApiDeps, + args: {tokenA: string; tokenB: string; providers: ReadonlyArray}, +): Promise { + const {tokenA, tokenB, providers} = args + const {network, client} = deps + + const params: {[key: string]: string} = { + 'token-a': tokenA, + 'token-b': tokenB, + 'providers': providers.join(','), + } + + const apiUrl = SWAP_API_ENDPOINTS[network].getLiquidityPools + const response = await client.get('', { + baseURL: apiUrl, + params, + }) + + if (response.status !== 200) { + throw new Error('Failed to fetch liquidity pools for token pair', { + cause: response.data, + }) + } + + return response.data +} + +export async function getPoolsPair( deps: ApiDeps, args: {tokenA: TokenAddress; tokenB: TokenAddress}, -): Promise { +): Promise { const {tokenA, tokenB} = args const {network, client} = deps const params: {[key: string]: string} = { @@ -19,14 +53,14 @@ export async function getPools( if ('assetNameHex' in tokenA) params['tokenname-hex1'] = tokenA.assetNameHex if ('assetNameHex' in tokenB) params['tokenname-hex2'] = tokenB.assetNameHex - const apiUrl = SWAP_API_ENDPOINTS[network].getPools - const response = await client.get('', { + const apiUrl = SWAP_API_ENDPOINTS[network].getPoolsPair + const response = await client.get('', { baseURL: apiUrl, params, }) if (response.status !== 200) { - throw new Error('Failed to fetch pools for token pair', { + throw new Error('Failed to fetch pools pair for token pair', { cause: response.data, }) } diff --git a/packages/openswap/src/types.ts b/packages/openswap/src/types.ts index 29d8ea7124..0afb7abf85 100644 --- a/packages/openswap/src/types.ts +++ b/packages/openswap/src/types.ts @@ -8,7 +8,7 @@ export type CancelOrderRequest = { export type CreateOrderRequest = { walletAddress: string - protocol: Protocol + protocol: Provider // only in the CreateOrder they call provider as protocol poolId?: string // only required for SundaeSwap trades. sell: { policyId: string @@ -27,7 +27,7 @@ export type CreateOrderResponse = | {status: 'success'; hash: string; datum: string; address: string} export type OpenOrder = { - provider: Protocol + provider: Provider owner: string from: { amount: string @@ -71,30 +71,24 @@ export type CompletedOrder = { } export type CompletedOrderResponse = CompletedOrder[] -export type Protocol = +export type Provider = | 'minswap' | 'sundaeswap' | 'wingriders' + | 'muesliswap' | 'muesliswap_v1' | 'muesliswap_v2' | 'muesliswap_v3' | 'muesliswap_v4' | 'vyfi' | 'spectrum' +// | 'muesliswap_clp' export type Network = 'mainnet' | 'preprod' -export type Pool = { - provider: - | 'minswap' - | 'sundaeswap' - | 'wingriders' - | 'muesliswap_v1' - | 'muesliswap_v2' - | 'muesliswap_v3' - | 'muesliswap_v4' - | 'vyfi' - | 'spectrum' +// NOTE: TBR +export type PoolPair = { + provider: Provider fee: string // % pool liquidity provider fee, usually 0.3. tokenA: { amount: string // amount of tokenA in the pool, without decimals. @@ -107,7 +101,7 @@ export type Pool = { price: number // float, current price in tokenA / tokenB according to the pool, NOT SUITABLE for price calculations, just for display purposes, i.e. 0.9097362621640215. batcherFee: { amount: string // amount of fee taken by protocol batchers, in lovelace. - token: '.' + token: string // most likely "." for lovelace. } deposit: number // amount of deposit / minUTxO required by protocol, returned to user, in lovelace. utxo: string // txhash#txindex of latest transaction involving this pool. @@ -117,8 +111,13 @@ export type Pool = { amount: string // amount of lpToken minted by the pool, without decimals. token: string // hexadecimal representation of lpToken, } + depositFee: { + amount: string // amount of fee taken by protocol batchers, in lovelace. + token: string // most likely "." for lovelace. + } + batcherAddress: string // address of the protocol batcher. } -export type PoolResponse = Pool[] +export type PoolPairResponse = PoolPair[] export type Token = { info: { @@ -176,3 +175,60 @@ export type ApiDeps = { network: Network client: AxiosInstance } + +export type LiquidityPoolResponse = LiquidityPool[] +export type LiquidityPool = { + tokenA: { + address: { + policyId: string + name: string + } + symbol?: string + image?: string + decimalPlaces: number + amount: string + status: string + priceAda: number + } + tokenB: { + address: { + policyId: string + name: string + } + symbol?: string + image?: string + decimalPlaces: number + amount: string + status: string + priceAda: number + } + feeToken: { + address: { + policyId: string + name: string + } + symbol?: string + image?: string + decimalPlaces: number + } + batcherFee: string + lvlDeposit: string + poolFee: string + lpToken: { + address?: { + policyId: string + name: string + } + amount?: string + } + poolId: string + provider: Provider + txHash?: string + outputIdx?: number + volume24h?: number + volume7d?: number + liquidityApy?: number + priceASqrt?: any + priceBSqrt?: any + batcherAddress: string +} diff --git a/packages/swap/src/adapters/openswap-api/api.mocks.ts b/packages/swap/src/adapters/openswap-api/api.mocks.ts index 1799259552..a38ee77bca 100644 --- a/packages/swap/src/adapters/openswap-api/api.mocks.ts +++ b/packages/swap/src/adapters/openswap-api/api.mocks.ts @@ -39,12 +39,13 @@ const createOrderData: Swap.CreateOrderData = { quantity: '1000', }, tokenB: {tokenId: '', quantity: '1000000000'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', price: 0, batcherFee: {tokenId: '', quantity: '0'}, deposit: {tokenId: '', quantity: '2000000'}, poolId: '0be55d262b29f564998ff81efe21bdc0022621c12f15af08d0f2ddb1.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', - lastUpdate: '1694691081066', lpToken: {tokenId: '', quantity: '0'}, }, amounts: { @@ -67,6 +68,8 @@ const getPools: Swap.Pool[] = [ tokenId: 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72.43414b45', }, + ptPriceTokenA: '0', + ptPriceTokenB: '0', deposit: {quantity: '2000000', tokenId: ''}, lpToken: { quantity: '981004', @@ -74,9 +77,8 @@ const getPools: Swap.Pool[] = [ 'e4214b7cce62ac6fbba385d164df48e157eae5863521b4b67ca71d86.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', }, batcherFee: {quantity: '2000000', tokenId: ''}, - lastUpdate: '2023-05-31 07:03:41', fee: '0.3', - price: 1581804.726923077, + price: 0, poolId: '0be55d262b29f564998ff81efe21bdc0022621c12f15af08d0f2ddb1.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', provider: 'minswap', @@ -88,6 +90,8 @@ const getPools: Swap.Pool[] = [ tokenId: 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72.43414b45', }, + ptPriceTokenA: '0', + ptPriceTokenB: '0', deposit: {quantity: '2000000', tokenId: ''}, lpToken: { quantity: '981004', @@ -95,9 +99,8 @@ const getPools: Swap.Pool[] = [ 'e4214b7cce62ac6fbba385d164df48e157eae5863521b4b67ca71d86.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', }, batcherFee: {quantity: '2000000', tokenId: ''}, - lastUpdate: '2023-05-31 07:03:41', fee: '0.3', - price: 1581804.726923077, + price: 0, poolId: '0be55d262b29f564998ff81efe21bdc0022621c12f15af08d0f2ddb1.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', provider: 'sundaeswap', @@ -109,6 +112,8 @@ const getPools: Swap.Pool[] = [ tokenId: 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72.43414b45', }, + ptPriceTokenA: '0', + ptPriceTokenB: '0', deposit: {quantity: '2000000', tokenId: ''}, lpToken: { quantity: '981004', @@ -116,9 +121,8 @@ const getPools: Swap.Pool[] = [ 'e4214b7cce62ac6fbba385d164df48e157eae5863521b4b67ca71d86.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', }, batcherFee: {quantity: '2000000', tokenId: ''}, - lastUpdate: '2023-05-31 07:03:41', fee: '0.3', - price: 1581804.726923077, + price: 0, poolId: '0be55d262b29f564998ff81efe21bdc0022621c12f15af08d0f2ddb1.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', provider: 'sundaeswap', diff --git a/packages/swap/src/adapters/openswap-api/api.test.ts b/packages/swap/src/adapters/openswap-api/api.test.ts index 2a015340a7..cddae51e83 100644 --- a/packages/swap/src/adapters/openswap-api/api.test.ts +++ b/packages/swap/src/adapters/openswap-api/api.test.ts @@ -7,6 +7,7 @@ import {apiMocks} from './api.mocks' const stakingKey = 'someStakingKey' const primaryTokenId = '' +const supportedProviders: ReadonlyArray = ['minswap'] describe('swapApiMaker', () => { let mockOpenSwapApi: jest.Mocked @@ -19,7 +20,8 @@ describe('swapApiMaker', () => { getOrders: jest.fn(), getTokens: jest.fn(), getCompletedOrders: jest.fn(), - getPools: jest.fn(), + getLiquidityPools: jest.fn(), + getPoolsPair: jest.fn(), network: 'mainnet', } as any }) @@ -34,6 +36,7 @@ describe('swapApiMaker', () => { isMainnet: true, stakingKey, primaryTokenId, + supportedProviders, }, { openswap: mockOpenSwapApi, @@ -56,6 +59,7 @@ describe('swapApiMaker', () => { isMainnet: true, stakingKey, primaryTokenId, + supportedProviders, }, { openswap: mockOpenSwapApi, @@ -80,6 +84,7 @@ describe('swapApiMaker', () => { isMainnet: true, stakingKey, primaryTokenId, + supportedProviders, }, { openswap: mockOpenSwapApi, @@ -107,6 +112,7 @@ describe('swapApiMaker', () => { isMainnet: true, stakingKey, primaryTokenId, + supportedProviders, }) expect(testnet).toBeDefined() @@ -114,6 +120,7 @@ describe('swapApiMaker', () => { isMainnet: false, stakingKey, primaryTokenId, + supportedProviders, }) expect(mainnet).toBeDefined() }) @@ -134,6 +141,7 @@ describe('swapApiMaker', () => { isMainnet: true, stakingKey, primaryTokenId, + supportedProviders, }, { openswap: mockOpenSwapApi, @@ -165,6 +173,7 @@ describe('swapApiMaker', () => { isMainnet: true, stakingKey, primaryTokenId, + supportedProviders, }, { openswap: mockOpenSwapApi, @@ -191,6 +200,7 @@ describe('swapApiMaker', () => { isMainnet: true, stakingKey, primaryTokenId, + supportedProviders, }, { openswap: mockOpenSwapApi, @@ -216,6 +226,7 @@ describe('swapApiMaker', () => { isMainnet: true, stakingKey, primaryTokenId, + supportedProviders, }, { openswap: mockOpenSwapApi, @@ -238,6 +249,7 @@ describe('swapApiMaker', () => { isMainnet: false, stakingKey, primaryTokenId, + supportedProviders, }, { openswap: mockOpenSwapApi, @@ -253,15 +265,16 @@ describe('swapApiMaker', () => { describe('getPools', () => { it('mainnet', async () => { - mockOpenSwapApi.getPools = jest + mockOpenSwapApi.getLiquidityPools = jest .fn() - .mockResolvedValue(openswapMocks.getPools) + .mockResolvedValue(openswapMocks.getLiquidityPools) const api = swapApiMaker( { isMainnet: true, stakingKey, primaryTokenId, + supportedProviders, }, { openswap: mockOpenSwapApi, @@ -274,19 +287,20 @@ describe('swapApiMaker', () => { }) expect(result).toEqual(apiMocks.getPools) - expect(mockOpenSwapApi.getPools).toHaveBeenCalledTimes(1) + expect(mockOpenSwapApi.getLiquidityPools).toHaveBeenCalledTimes(1) }) it('preprod (mocked)', async () => { - mockOpenSwapApi.getPools = jest + mockOpenSwapApi.getLiquidityPools = jest .fn() - .mockResolvedValue(openswapMocks.getPools) + .mockResolvedValue(openswapMocks.getLiquidityPools) const api = swapApiMaker( { isMainnet: false, stakingKey, primaryTokenId, + supportedProviders, }, { openswap: mockOpenSwapApi, @@ -299,7 +313,7 @@ describe('swapApiMaker', () => { }) expect(result).toBeDefined() - expect(mockOpenSwapApi.getPools).not.toHaveBeenCalled() + expect(mockOpenSwapApi.getLiquidityPools).not.toHaveBeenCalled() }) }) }) diff --git a/packages/swap/src/adapters/openswap-api/api.ts b/packages/swap/src/adapters/openswap-api/api.ts index bf48cd8b93..00cdbda710 100644 --- a/packages/swap/src/adapters/openswap-api/api.ts +++ b/packages/swap/src/adapters/openswap-api/api.ts @@ -9,7 +9,13 @@ export const swapApiMaker = ( isMainnet, stakingKey, primaryTokenId, - }: {isMainnet?: boolean; stakingKey: string; primaryTokenId: string}, + supportedProviders, + }: { + isMainnet?: boolean + stakingKey: string + primaryTokenId: string + supportedProviders: ReadonlyArray + }, deps?: {openswap?: OpenSwapApi}, ): Readonly => { const api = @@ -35,7 +41,6 @@ export const swapApiMaker = ( const orderRequest: OpenSwap.CreateOrderRequest = { walletAddress: address, - // TODO: check this mistmach of protocol x provider on our end protocol: selectedPool.provider as OpenSwap.CreateOrderRequest['protocol'], poolId: selectedPool.poolId, @@ -71,22 +76,18 @@ export const swapApiMaker = ( .getTokens(transformers.asOpenswapTokenId(token)) .then(transformers.asYoroiBalanceTokens) - const getPools: Swap.Api['getPools'] = async ({tokenA, tokenB}) => { + const getPools: Swap.Api['getPools'] = async ({ + tokenA, + tokenB, + providers = supportedProviders, + }) => { if (!isMainnet) return apiMocks.getPools // preprod doesn't return any pools - const tokenIdA = transformers.asOpenswapTokenId(tokenA) - const tokenIdB = transformers.asOpenswapTokenId(tokenB) - return api - .getPools({ - tokenA: { - policyId: tokenIdA.policyId, - assetNameHex: tokenIdA.assetName, - }, - tokenB: { - policyId: tokenIdB.policyId, - assetNameHex: tokenIdB.assetName, - }, + .getLiquidityPools({ + tokenA, + tokenB, + providers, }) .then(transformers.asYoroiPools) } @@ -100,5 +101,6 @@ export const swapApiMaker = ( getCompletedOrders, stakingKey, primaryTokenId, + supportedProviders, } as const } diff --git a/packages/swap/src/adapters/openswap-api/openswap.mocks.ts b/packages/swap/src/adapters/openswap-api/openswap.mocks.ts index ed914fd720..f2e5512ac7 100644 --- a/packages/swap/src/adapters/openswap-api/openswap.mocks.ts +++ b/packages/swap/src/adapters/openswap-api/openswap.mocks.ts @@ -310,133 +310,217 @@ const getOpenOrders: OpenSwap.OpenOrder[] = [ }, ] -const getPools: OpenSwap.Pool[] = [ +const getLiquidityPools: OpenSwap.LiquidityPool[] = [ { provider: 'minswap', - fee: '0.3', + poolFee: '0.3', tokenA: { amount: '1233807687', - token: '.', + address: { + policyId: '', + name: '', + }, + symbol: '', + image: '', + decimalPlaces: 0, + status: '', + priceAda: 0, }, tokenB: { amount: '780', - token: - 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72.43414b45', - }, - price: 1581804.726923077, - batcherFee: { - amount: '2000000', - token: '.', + address: { + policyId: 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72', + name: '43414b45', + }, + symbol: '', + image: '', + decimalPlaces: 0, + status: '', + priceAda: 0, }, + batcherFee: '2000000', // depositFee: { // amount: '2000000', // token: '.', // }, - deposit: 2000000, - utxo: '0596860b5970ef989c56f7ae38b3c0f74bb4979ac15ee994c30760f7f4d908ce#0', + lvlDeposit: '2000000', + batcherAddress: 'someBatcherAddress', + feeToken: { + address: { + policyId: '.', + name: '.', + }, + decimalPlaces: 0, + }, + txHash: '0596860b5970ef989c56f7ae38b3c0f74bb4979ac15ee994c30760f7f4d908ce', + outputIdx: 0, poolId: '0be55d262b29f564998ff81efe21bdc0022621c12f15af08d0f2ddb1.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', - timestamp: '2023-05-31 07:03:41', lpToken: { amount: '981004', - token: - 'e4214b7cce62ac6fbba385d164df48e157eae5863521b4b67ca71d86.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', + address: { + policyId: 'e4214b7cce62ac6fbba385d164df48e157eae5863521b4b67ca71d86', + name: '7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', + }, }, }, { provider: 'sundaeswap', - fee: '0.3', + poolFee: '0.3', tokenA: { amount: '1233807687', - token: '.', + address: { + policyId: '', + name: '', + }, + symbol: '', + image: '', + decimalPlaces: 0, + status: '', + priceAda: 0, }, tokenB: { amount: '780', - token: - 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72.43414b45', - }, - price: 1581804.726923077, - batcherFee: { - amount: '2000000', - token: '.', + address: { + policyId: 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72', + name: '43414b45', + }, + symbol: '', + image: '', + decimalPlaces: 0, + status: '', + priceAda: 0, }, + batcherFee: '2000000', // depositFee: { // amount: '2000000', // token: '.', // }, - deposit: 2000000, - utxo: '0596860b5970ef989c56f7ae38b3c0f74bb4979ac15ee994c30760f7f4d908ce#0', + lvlDeposit: '2000000', + txHash: '0596860b5970ef989c56f7ae38b3c0f74bb4979ac15ee994c30760f7f4d908ce', + outputIdx: 0, + batcherAddress: 'someBatcherAddress', + feeToken: { + address: { + policyId: '.', + name: '.', + }, + decimalPlaces: 0, + }, poolId: '0be55d262b29f564998ff81efe21bdc0022621c12f15af08d0f2ddb1.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', - timestamp: '2023-05-31 07:03:41', lpToken: { amount: '981004', - token: - 'e4214b7cce62ac6fbba385d164df48e157eae5863521b4b67ca71d86.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', + address: { + policyId: 'e4214b7cce62ac6fbba385d164df48e157eae5863521b4b67ca71d86', + name: '7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', + }, }, }, { provider: 'sundaeswap', - fee: '0.3', + poolFee: '0.3', tokenA: { amount: '1233807687', - token: '.', + address: { + policyId: '', + name: '', + }, + symbol: '', + image: '', + decimalPlaces: 0, + status: '', + priceAda: 0, }, tokenB: { amount: '780', - token: - 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72.43414b45', - }, - price: 1581804.726923077, - batcherFee: { - amount: '2000000', - token: '.', + address: { + policyId: 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72', + name: '43414b45', + }, + symbol: '', + image: '', + decimalPlaces: 0, + status: '', + priceAda: 0, }, + batcherFee: '2000000', // depositFee: { // amount: '2000000', // token: '.', // }, - deposit: 2000000, - utxo: '0596860b5970ef989c56f7ae38b3c0f74bb4979ac15ee994c30760f7f4d908ce#0', + lvlDeposit: '2000000', + txHash: '0596860b5970ef989c56f7ae38b3c0f74bb4979ac15ee994c30760f7f4d908ce', + outputIdx: 0, + batcherAddress: 'someBatcherAddress', + feeToken: { + address: { + policyId: '.', + name: '.', + }, + decimalPlaces: 0, + }, poolId: '0be55d262b29f564998ff81efe21bdc0022621c12f15af08d0f2ddb1.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', - timestamp: '2023-05-31 07:03:41', lpToken: { amount: '981004', - token: - 'e4214b7cce62ac6fbba385d164df48e157eae5863521b4b67ca71d86.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', + address: { + policyId: 'e4214b7cce62ac6fbba385d164df48e157eae5863521b4b67ca71d86', + name: '7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', + }, }, }, { provider: 'spectrum', // unsupported pool - fee: '0.3', + poolFee: '0.3', tokenA: { amount: '1233807687', - token: '.', + address: { + policyId: '', + name: '', + }, + symbol: '', + image: '', + decimalPlaces: 0, + status: '', + priceAda: 0, }, tokenB: { amount: '780', - token: - 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72.43414b45', - }, - price: 1581804.726923077, - batcherFee: { - amount: '2000000', - token: '.', + address: { + policyId: 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72', + name: '43414b45', + }, + symbol: '', + image: '', + decimalPlaces: 0, + status: '', + priceAda: 0, }, + batcherFee: '2000000', // depositFee: { // amount: '2000000', // token: '.', // }, - deposit: 2000000, - utxo: '0596860b5970ef989c56f7ae38b3c0f74bb4979ac15ee994c30760f7f4d908ce#0', + lvlDeposit: '2000000', + txHash: '0596860b5970ef989c56f7ae38b3c0f74bb4979ac15ee994c30760f7f4d908ce', + outputIdx: 0, + batcherAddress: 'someBatcherAddress', + feeToken: { + address: { + policyId: '.', + name: '.', + }, + decimalPlaces: 0, + }, poolId: '0be55d262b29f564998ff81efe21bdc0022621c12f15af08d0f2ddb1.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', - timestamp: '2023-05-31 07:03:41', lpToken: { amount: '981004', - token: - 'e4214b7cce62ac6fbba385d164df48e157eae5863521b4b67ca71d86.7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', + address: { + policyId: 'e4214b7cce62ac6fbba385d164df48e157eae5863521b4b67ca71d86', + name: '7339a8bcda85e2c997d9f16beddbeb3ad755f5202f5cfd9cb08db346a1292c01', + }, }, }, ] @@ -445,5 +529,5 @@ export const openswapMocks = { getTokens, getCompletedOrders, getOpenOrders, - getPools, + getLiquidityPools, } diff --git a/packages/swap/src/helpers/orders/getBestBuyPool.test.ts b/packages/swap/src/helpers/orders/getBestBuyPool.test.ts new file mode 100644 index 0000000000..0a7eace89c --- /dev/null +++ b/packages/swap/src/helpers/orders/getBestBuyPool.test.ts @@ -0,0 +1,279 @@ +import {Swap, Balance} from '@yoroi/types' + +import {getBestBuyPool} from './getBestBuyPool' +import {getBuyAmount} from './getBuyAmount' + +describe('getBestBuyPool', () => { + it('should return pool with maximin possible tokens to buy', () => { + const pool1: Swap.Pool = { + tokenA: {quantity: '529504614', tokenId: 'tokenA'}, + tokenB: {quantity: '7339640354', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.3', // 0.3% + provider: 'muesliswap_v2', + price: 0, + batcherFee: {quantity: '950000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool2: Swap.Pool = { + tokenA: {quantity: '143610201719', tokenId: 'tokenA'}, + tokenB: {quantity: '2055821866531', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.3', // 0.3% + provider: 'vyfi', + price: 0, + batcherFee: {quantity: '1900000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool3: Swap.Pool = { + tokenA: {quantity: '27344918300893', tokenId: 'tokenA'}, + tokenB: {quantity: '393223050468514', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.3', // 0.3% + provider: 'minswap', + price: 0, + batcherFee: {quantity: '2000000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool4: Swap.Pool = { + tokenA: {quantity: '3400529909', tokenId: 'tokenA'}, + tokenB: {quantity: '49215467634', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.35', // 0.35% + provider: 'wingriders', + price: 0, + batcherFee: {quantity: '2000000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool5: Swap.Pool = { + tokenA: {quantity: '10178222382', tokenId: 'tokenA'}, + tokenB: {quantity: '145009426744', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.3', // 0.3% + provider: 'sundaeswap', + price: 0, + batcherFee: {quantity: '2500000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool6: Swap.Pool = { + tokenA: {quantity: '973669994', tokenId: 'tokenA'}, + tokenB: {quantity: '13710853133', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.05', // 0.05% + provider: 'sundaeswap', + price: 0, + batcherFee: {quantity: '2500000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } as Swap.Pool + + const sell: Balance.Amount = { + quantity: '10000000000', + tokenId: 'tokenB', + } + + const pools = [pool1, pool2, pool3, pool4, pool5, pool6] + const bestBuyPool = getBestBuyPool(pools, sell) + if (bestBuyPool) { + expect(bestBuyPool.provider).toBe('minswap') + const buyAmount = getBuyAmount(bestBuyPool, sell) + expect(buyAmount.quantity).toBe('693300972') + } else { + fail('bestBuyPool undefined') + } + }) + + it('should return pool with maximin possible tokens to buy (case 2)', () => { + const pool1: Swap.Pool = { + tokenA: {quantity: '529504614', tokenId: 'tokenA'}, + tokenB: {quantity: '7339640354', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.3', // 0.3% + provider: 'muesliswap_v2', + price: 0, + batcherFee: {quantity: '950000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool2: Swap.Pool = { + tokenA: {quantity: '143610201719', tokenId: 'tokenA'}, + tokenB: {quantity: '2055821866531', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.3', // 0.3% + provider: 'vyfi', + price: 0, + batcherFee: {quantity: '1900000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool3: Swap.Pool = { + tokenA: {quantity: '27344918300893', tokenId: 'tokenA'}, + tokenB: {quantity: '393223050468514', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.3', // 0.3% + provider: 'minswap', + price: 0, + batcherFee: {quantity: '2000000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool4: Swap.Pool = { + tokenA: {quantity: '3400529909', tokenId: 'tokenA'}, + tokenB: {quantity: '49215467634', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.35', // 0.35% + provider: 'wingriders', + price: 0, + batcherFee: {quantity: '2000000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool5: Swap.Pool = { + tokenA: {quantity: '10178222382', tokenId: 'tokenA'}, + tokenB: {quantity: '145009426744', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.3', // 0.3% + provider: 'sundaeswap', + price: 0, + batcherFee: {quantity: '2500000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool6: Swap.Pool = { + tokenA: {quantity: '973669994', tokenId: 'tokenA'}, + tokenB: {quantity: '13710853133', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.05', // 0.05% + provider: 'sundaeswap', + price: 0, + batcherFee: {quantity: '2500000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } as Swap.Pool + + const sell: Balance.Amount = { + quantity: '1000000000', + tokenId: 'tokenA', + } + + const pools = [pool1, pool2, pool3, pool4, pool5, pool6] + const bestBuyPool = getBestBuyPool(pools, sell) + if (bestBuyPool) { + expect(bestBuyPool.provider).toBe('minswap') + const buyAmount = getBuyAmount(bestBuyPool, sell) + expect(buyAmount.quantity).toBe('14336451239') + } else { + fail('bestBuyPool undefined') + } + }) + + it('should return undefined if sell amount is 0', () => { + const pool1: Swap.Pool = { + tokenA: {quantity: '529504614', tokenId: 'tokenA'}, + tokenB: {quantity: '7339640354', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.3', // 0.3% + provider: 'muesliswap_v2', + price: 0, + batcherFee: {quantity: '950000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const sell: Balance.Amount = { + quantity: '0', + tokenId: 'tokenA', + } + + expect(getBestBuyPool([pool1], sell)).toBeUndefined() + }) + + it('should return undefined if pools list is empty', () => { + const sell: Balance.Amount = { + quantity: '1', + tokenId: 'tokenA', + } + + expect(getBestBuyPool([], sell)).toBeUndefined() + }) +}) diff --git a/packages/swap/src/helpers/orders/getBestBuyPool.ts b/packages/swap/src/helpers/orders/getBestBuyPool.ts new file mode 100644 index 0000000000..018a6d593a --- /dev/null +++ b/packages/swap/src/helpers/orders/getBestBuyPool.ts @@ -0,0 +1,54 @@ +import {Balance, Swap} from '@yoroi/types' + +import {Quantities} from '../../utils/quantities' +import {getBuyAmount} from './getBuyAmount' +import {getPriceAfterFee} from './getPriceAfterFee' +import BigNumber from 'bignumber.js' + +/** + * Find the best pool to buy based on the desired sell amount in a liquidity pool. + * + * @param pools - The liquidity pool list. + * @param sell - The desired sell amount. + * + * @returns The best pool to sell + * if the balance in the pool is insuficient it wont throw an error + * if the pools balance is 0 it will return undefined + * if the pool list is empty it will return undefined + */ +export const getBestBuyPool = ( + pools: Swap.Pool[], + sell: Balance.Amount, +): Swap.Pool | undefined => { + if (pools.length === 0 || Quantities.isZero(sell.quantity)) return undefined + + let bestPool: Swap.Pool | undefined + let bestPrice = new BigNumber(0) + + for (const pool of pools) { + const buy = getBuyAmount(pool, sell) + if (Quantities.isZero(buy.quantity)) continue + + const isSellTokenA = sell.tokenId === pool.tokenA.tokenId + const [amountA, amountB] = isSellTokenA ? [sell, buy] : [buy, sell] + const price = getPriceAfterFee( + pool, + amountA.quantity, + amountB.quantity, + sell.tokenId, + ) + + if (bestPool === undefined) { + bestPool = pool + bestPrice = price + continue + } + + if (price < bestPrice) { + bestPool = pool + bestPrice = price + } + } + + return bestPool +} diff --git a/packages/swap/src/helpers/orders/getBestSellPool.test.ts b/packages/swap/src/helpers/orders/getBestSellPool.test.ts new file mode 100644 index 0000000000..c98692a907 --- /dev/null +++ b/packages/swap/src/helpers/orders/getBestSellPool.test.ts @@ -0,0 +1,281 @@ +import {Swap, Balance} from '@yoroi/types' + +import {getBestSellPool} from './getBestSellPool' +import {getSellAmount} from './getSellAmount' + +describe('getBestSellPool', () => { + it('should return pool with min possible tokens to sell', () => { + const pool1: Swap.Pool = { + tokenA: {quantity: '529504614', tokenId: 'tokenA'}, + tokenB: {quantity: '7339640354', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.06950020009', + fee: '0.3', // 0.3% + provider: 'muesliswap_v2', + price: 0, + batcherFee: {quantity: '950000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool2: Swap.Pool = { + tokenA: {quantity: '143610201719', tokenId: 'tokenA'}, + tokenB: {quantity: '2055821866531', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.06950020009', + fee: '0.3', // 0.3% + provider: 'vyfi', + price: 0, + batcherFee: {quantity: '1900000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool3: Swap.Pool = { + tokenA: {quantity: '27337840212697', tokenId: 'tokenA'}, + tokenB: {quantity: '393349086430693', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.06950020009', + fee: '0.3', // 0.3% + provider: 'minswap', + price: 0, + batcherFee: {quantity: '2000000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool4: Swap.Pool = { + tokenA: {quantity: '3400529909', tokenId: 'tokenA'}, + tokenB: {quantity: '49215467634', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.06950020009', + fee: '0.35', // 0.35% + provider: 'wingriders', + price: 0, + batcherFee: {quantity: '2000000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool5: Swap.Pool = { + tokenA: {quantity: '10178222382', tokenId: 'tokenA'}, + tokenB: {quantity: '145009426744', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.06950020009', + fee: '0.3', // 0.3% + provider: 'sundaeswap', + price: 0, + batcherFee: {quantity: '2500000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool6: Swap.Pool = { + tokenA: {quantity: '973669994', tokenId: 'tokenA'}, + tokenB: {quantity: '13710853133', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.06950020009', + fee: '0.05', // 0.05% + provider: 'sundaeswap', + price: 0, + batcherFee: {quantity: '2500000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } as Swap.Pool + + const buy: Balance.Amount = { + quantity: '1000000000', + tokenId: 'tokenB', + } + + const pools = [pool1, pool2, pool3, pool4, pool5, pool6] + const bestSellPool = getBestSellPool(pools, buy) + + if (bestSellPool) { + expect(bestSellPool.provider).toBe('minswap') + const sellAmount = getSellAmount(bestSellPool, buy) + expect(sellAmount.quantity).toBe('69709507') + } else { + fail('bestSellPool is undefined') + } + }) + + it('should return pool with min possible tokens to sell (opposite test)', () => { + const pool1: Swap.Pool = { + tokenB: {quantity: '529504614', tokenId: 'tokenB'}, + tokenA: {quantity: '7339640354', tokenId: 'tokenA'}, + ptPriceTokenB: '1', + ptPriceTokenA: '0.06950020009', + fee: '0.3', // 0.3% + provider: 'muesliswap_v2', + price: 0, + batcherFee: {quantity: '950000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool2: Swap.Pool = { + tokenB: {quantity: '143610201719', tokenId: 'tokenB'}, + tokenA: {quantity: '2055821866531', tokenId: 'tokenA'}, + ptPriceTokenB: '1', + ptPriceTokenA: '0.06950020009', + fee: '0.3', // 0.3% + provider: 'vyfi', + price: 0, + batcherFee: {quantity: '1900000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool3: Swap.Pool = { + tokenB: {quantity: '27337840212697', tokenId: 'tokenB'}, + tokenA: {quantity: '393349086430693', tokenId: 'tokenA'}, + ptPriceTokenB: '1', + ptPriceTokenA: '0.06950020009', + fee: '0.3', // 0.3% + provider: 'minswap', + price: 0, + batcherFee: {quantity: '2000000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool4: Swap.Pool = { + tokenB: {quantity: '3400529909', tokenId: 'tokenB'}, + tokenA: {quantity: '49215467634', tokenId: 'tokenA'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.06950020009', + fee: '0.35', // 0.35% + provider: 'wingriders', + price: 0, + batcherFee: {quantity: '2000000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool5: Swap.Pool = { + tokenB: {quantity: '10178222382', tokenId: 'tokenB'}, + tokenA: {quantity: '145009426744', tokenId: 'tokenA'}, + ptPriceTokenB: '1', + ptPriceTokenA: '0.06950020009', + fee: '0.3', // 0.3% + provider: 'sundaeswap', + price: 0, + batcherFee: {quantity: '2500000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const pool6: Swap.Pool = { + tokenB: {quantity: '973669994', tokenId: 'tokenB'}, + tokenA: {quantity: '13710853133', tokenId: 'tokenA'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.06950020009', + fee: '0.05', // 0.05% + provider: 'sundaeswap', + price: 0, + batcherFee: {quantity: '2500000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } as Swap.Pool + + const buy: Balance.Amount = { + quantity: '1000000000', + tokenId: 'tokenA', + } + + const pools = [pool1, pool2, pool3, pool4, pool5, pool6] + const bestSellPool = getBestSellPool(pools, buy) + + if (bestSellPool) { + expect(bestSellPool.provider).toBe('minswap') + const sellAmount = getSellAmount(bestSellPool, buy) + expect(sellAmount.quantity).toBe('69709507') + } else { + fail('bestSellPool is undefined') + } + }) + + it('should return undefined if buy amount is 0', () => { + const pool1: Swap.Pool = { + tokenA: {quantity: '529504614', tokenId: 'tokenA'}, + tokenB: {quantity: '7339640354', tokenId: 'tokenB'}, + ptPriceTokenA: '1', + ptPriceTokenB: '0.0695404765', + fee: '0.3', // 0.3% + provider: 'muesliswap_v2', + price: 0, + batcherFee: {quantity: '950000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } + + const sell: Balance.Amount = { + quantity: '0', + tokenId: 'tokenA', + } + + expect(getBestSellPool([pool1], sell)).toBeUndefined() + }) + + it('should return undefined if pools list is empty', () => { + const sell: Balance.Amount = { + quantity: '1', + tokenId: 'tokenA', + } + + expect(getBestSellPool([], sell)).toBeUndefined() + }) +}) diff --git a/packages/swap/src/helpers/orders/getBestSellPool.ts b/packages/swap/src/helpers/orders/getBestSellPool.ts new file mode 100644 index 0000000000..226d9c9c6d --- /dev/null +++ b/packages/swap/src/helpers/orders/getBestSellPool.ts @@ -0,0 +1,56 @@ +import {Balance, Swap} from '@yoroi/types' + +import {Quantities} from '../../utils/quantities' +import BigNumber from 'bignumber.js' +import {getPriceAfterFee} from './getPriceAfterFee' +import {getSellAmount} from './getSellAmount' + +/** + * Find the best pool to sell based on the desired sell amount in a liquidity pool. + * + * @param pools - The liquidity pool list. + * @param buy - The desired buy amount. + * + * @returns The best pool to sell + * if the balance in the pool is insuficient it wont throw an error + * if the pools balance is 0 it will return undefined + * if the pool list is empty it will return undefined + */ +export const getBestSellPool = ( + pools: Swap.Pool[], + buy: Balance.Amount, +): Swap.Pool | undefined => { + if (pools.length === 0 || Quantities.isZero(buy.quantity)) return undefined + + let bestPool: Swap.Pool | undefined + let bestPrice = new BigNumber(0) + + for (const pool of pools) { + const sell = getSellAmount(pool, buy) + if (Quantities.isZero(sell.quantity)) continue + + const isBuyTokenA = buy.tokenId === pool.tokenA.tokenId + const [aAmount, bAmount] = isBuyTokenA + ? [buy.quantity, sell.quantity] + : [sell.quantity, buy.quantity] + const price = getPriceAfterFee( + pool, + aAmount, + bAmount, + isBuyTokenA ? pool.tokenB.tokenId : pool.tokenA.tokenId, + ) + + if (bestPool === undefined) { + bestPool = pool + bestPrice = price + continue + } + + if (price < bestPrice) { + bestPool = pool + bestPrice = price + } + } + + return bestPool +} diff --git a/packages/swap/src/helpers/orders/getBuyAmount.test.ts b/packages/swap/src/helpers/orders/getBuyAmount.test.ts index 449ae00c6d..1095a3b903 100644 --- a/packages/swap/src/helpers/orders/getBuyAmount.test.ts +++ b/packages/swap/src/helpers/orders/getBuyAmount.test.ts @@ -7,13 +7,14 @@ describe('getBuyAmount', () => { const pool = { tokenA: {quantity: '4500000', tokenId: 'tokenA'}, tokenB: {quantity: '9000000', tokenId: 'tokenB'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', fee: '0.3', // 0.3% provider: 'minswap', price: 2, batcherFee: {quantity: '1', tokenId: ''}, deposit: {quantity: '1', tokenId: ''}, poolId: '0', - lastUpdate: '0', lpToken: { quantity: '0', tokenId: '0', @@ -32,17 +33,48 @@ describe('getBuyAmount', () => { expect(limitedResult.tokenId).toBe('tokenB') }) + it('should calculate the correct buy amount when selling tokenA (muesli example)', () => { + const pool = { + tokenA: {quantity: '2022328173071', tokenId: ''}, + tokenB: {quantity: '277153', tokenId: 'tokenB'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', + fee: '0.3', // 0.3% + provider: 'muesliswap', + price: 7296793.37070499, + batcherFee: {quantity: '950000', tokenId: ''}, + deposit: {quantity: '2000000', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } as Swap.Pool + const sell: Balance.Amount = { + quantity: '1000000000', + tokenId: '', + } + const result = getBuyAmount(pool, sell) + expect(result.quantity).toBe('136') + expect(result.tokenId).toBe('tokenB') + + const limitedResult = getBuyAmount(pool, sell, '2.1') + //expect(limitedResult.quantity).toBe('47') + expect(limitedResult.tokenId).toBe('tokenB') + }) + it('should calculate the correct buy amount when selling tokenB', () => { const pool = { tokenA: {quantity: '4500000', tokenId: 'tokenA'}, tokenB: {quantity: '9000000', tokenId: 'tokenB'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', fee: '0.3', // 0.3% provider: 'minswap', price: 2, batcherFee: {quantity: '1', tokenId: ''}, deposit: {quantity: '1', tokenId: ''}, poolId: '0', - lastUpdate: '0', lpToken: { quantity: '0', tokenId: '0', diff --git a/packages/swap/src/helpers/orders/getMarketPrice.test.ts b/packages/swap/src/helpers/orders/getMarketPrice.test.ts index 98960122d1..99c3f6cad0 100644 --- a/packages/swap/src/helpers/orders/getMarketPrice.test.ts +++ b/packages/swap/src/helpers/orders/getMarketPrice.test.ts @@ -7,13 +7,14 @@ describe('getMarketPrice', () => { const pool = { tokenA: {quantity: '4500000', tokenId: 'tokenA'}, tokenB: {quantity: '9000000', tokenId: 'tokenB'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', fee: '0.3', // 0.3% provider: 'minswap', price: 2, batcherFee: {quantity: '1', tokenId: ''}, deposit: {quantity: '1', tokenId: ''}, poolId: '0', - lastUpdate: '0', lpToken: { quantity: '0', tokenId: '0', @@ -31,13 +32,14 @@ describe('getMarketPrice', () => { const pool = { tokenA: {quantity: '4500000', tokenId: 'tokenA'}, tokenB: {quantity: '9000000', tokenId: 'tokenB'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', fee: '0.3', // 0.3% provider: 'minswap', price: 2, batcherFee: {quantity: '1', tokenId: ''}, deposit: {quantity: '1', tokenId: ''}, poolId: '0', - lastUpdate: '0', lpToken: { quantity: '0', tokenId: '0', diff --git a/packages/swap/src/helpers/orders/getPriceAfterFee.test.ts b/packages/swap/src/helpers/orders/getPriceAfterFee.test.ts new file mode 100644 index 0000000000..44e24ab60f --- /dev/null +++ b/packages/swap/src/helpers/orders/getPriceAfterFee.test.ts @@ -0,0 +1,105 @@ +import {Swap} from '@yoroi/types' +import {getPriceAfterFee} from './getPriceAfterFee' +import BigNumber from 'bignumber.js' + +describe('getPriceAfterFee', () => { + it('should calculate the correct price after fee when selling tokenA', () => { + const pool = { + tokenA: {quantity: '1200400368252', tokenId: 'tokenA'}, + tokenB: {quantity: '11364790709', tokenId: 'tokenB'}, + ptPriceTokenA: '0.03465765134', + ptPriceTokenB: '3.81247293317', + fee: '0.3', // 0.3% + provider: 'minswap', + price: 2, + batcherFee: {quantity: '950000', tokenId: ''}, + deposit: {quantity: '1', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } as Swap.Pool + const tokenId = 'tokenA' + const tokenAAmount = '10000000000' + const tokenBAmount = '93613464' + const result = getPriceAfterFee(pool, tokenAAmount, tokenBAmount, tokenId) + const expected = new BigNumber('107.11505104205276356717') + expect(result).toStrictEqual(expected) + }) + + it('should calculate the correct price after fee when selling tokenB', () => { + const pool = { + tokenA: {quantity: '143983812522', tokenId: 'tokenA'}, + tokenB: {quantity: '2050476716943', tokenId: 'tokenB'}, + ptPriceTokenA: '0.06954250577', + ptPriceTokenB: '1', + fee: '0.3', // 0.3% + provider: 'minswap', + price: 2, + batcherFee: {quantity: '1900000', tokenId: ''}, + deposit: {quantity: '1', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } as Swap.Pool + const tokenId = 'tokenA' + const tokenAAmount = '10000000000' + const tokenBAmount = '696702612' + const result = getPriceAfterFee(pool, tokenAAmount, tokenBAmount, tokenId) + const expected = new BigNumber('14.39254173470077386167') + expect(result).toStrictEqual(expected) + }) + + it('should return 0 when sell side is 0', () => { + const pool = { + tokenA: {quantity: '143983812522', tokenId: 'tokenA'}, + tokenB: {quantity: '2050476716943', tokenId: 'tokenB'}, + ptPriceTokenA: '0.06954250577', + ptPriceTokenB: '1', + fee: '0.3', // 0.3% + provider: 'minswap', + price: 2, + batcherFee: {quantity: '1900000', tokenId: ''}, + deposit: {quantity: '1', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } as Swap.Pool + const tokenAAmount = '0' + const tokenBAmount = '93613464' + const tokenId = 'tokenA' + const result = getPriceAfterFee(pool, tokenAAmount, tokenBAmount, tokenId) + const expected = new BigNumber(0) + expect(result).toStrictEqual(expected) + }) + + it('should return 0 when buy side is 0', () => { + const pool = { + tokenA: {quantity: '143983812522', tokenId: 'tokenA'}, + tokenB: {quantity: '2050476716943', tokenId: 'tokenB'}, + ptPriceTokenA: '0.06954250577', + ptPriceTokenB: '1', + fee: '0.3', // 0.3% + provider: 'minswap', + price: 2, + batcherFee: {quantity: '1900000', tokenId: ''}, + deposit: {quantity: '1', tokenId: ''}, + poolId: '0', + lpToken: { + quantity: '0', + tokenId: '0', + }, + } as Swap.Pool + const tokenAAmount = '10000000000' + const tokenBAmount = '0' + const tokenId = 'tokenA' + const result = getPriceAfterFee(pool, tokenAAmount, tokenBAmount, tokenId) + const expected = new BigNumber(0) + expect(result).toStrictEqual(expected) + }) +}) diff --git a/packages/swap/src/helpers/orders/getPriceAfterFee.ts b/packages/swap/src/helpers/orders/getPriceAfterFee.ts new file mode 100644 index 0000000000..d9db9bb375 --- /dev/null +++ b/packages/swap/src/helpers/orders/getPriceAfterFee.ts @@ -0,0 +1,38 @@ +import {Balance, Swap} from '@yoroi/types' +import BigNumber from 'bignumber.js' +import {Quantities} from '../../utils/quantities' + +/** + * Calculate the price with batcher fee in a liquidity pool. + * + * @param pool - The liquidity pool. + * @param tokenAAmount - Token A amount in an order. + * @param tokenBAmount - Token B amount in an order. + * @param sellTokenId - The token id of the desired sell amount. + * + * @returns The price after fee + */ +export const getPriceAfterFee = ( + pool: Swap.Pool, + quantityA: Balance.Quantity, + quantityB: Balance.Quantity, + sellTokenId: string, +): BigNumber => { + if (Quantities.isZero(quantityA) || Quantities.isZero(quantityB)) + return new BigNumber(0) + + const A = new BigNumber(quantityA) + const B = new BigNumber(quantityB) + + const isSellTokenA = sellTokenId === pool.tokenA.tokenId + const [dividend, divisor] = isSellTokenA ? [A, B] : [B, A] + const sellPriceInPtTerm = isSellTokenA + ? new BigNumber(pool.ptPriceTokenA) + : new BigNumber(pool.ptPriceTokenB) + + const feeInSellTerm = sellPriceInPtTerm.isZero() + ? new BigNumber(0) + : new BigNumber(pool.batcherFee.quantity).dividedBy(sellPriceInPtTerm) + + return dividend.plus(feeInSellTerm).dividedBy(divisor) +} diff --git a/packages/swap/src/helpers/orders/getSellAmount.test.ts b/packages/swap/src/helpers/orders/getSellAmount.test.ts index 19443ea441..90dc10f7a6 100644 --- a/packages/swap/src/helpers/orders/getSellAmount.test.ts +++ b/packages/swap/src/helpers/orders/getSellAmount.test.ts @@ -7,13 +7,14 @@ describe('getSellAmount', () => { const pool = { tokenA: {quantity: '4500000', tokenId: 'tokenA'}, tokenB: {quantity: '9000000', tokenId: 'tokenB'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', fee: '0.5', // 0.5% provider: 'minswap', price: 2, batcherFee: {quantity: '1', tokenId: ''}, deposit: {quantity: '1', tokenId: ''}, poolId: '0', - lastUpdate: '0', lpToken: { quantity: '0', tokenId: '0', @@ -40,13 +41,14 @@ describe('getSellAmount', () => { const pool = { tokenA: {quantity: '4500000', tokenId: 'tokenA'}, tokenB: {quantity: '9000000', tokenId: 'tokenB'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', fee: '0.5', // 0.5% provider: 'minswap', price: 2, batcherFee: {quantity: '1', tokenId: ''}, deposit: {quantity: '1', tokenId: ''}, poolId: '0', - lastUpdate: '0', lpToken: { quantity: '0', tokenId: '0', @@ -69,13 +71,14 @@ describe('getSellAmount', () => { const pool = { tokenA: {quantity: '1000000', tokenId: 'tokenA'}, tokenB: {quantity: '2000000', tokenId: 'tokenB'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', fee: '10', provider: 'minswap', price: 2, batcherFee: {quantity: '1', tokenId: ''}, deposit: {quantity: '1', tokenId: ''}, poolId: '0', - lastUpdate: '0', lpToken: { quantity: '0', tokenId: '0', diff --git a/packages/swap/src/helpers/orders/makeLimitOrder.test.ts b/packages/swap/src/helpers/orders/makeLimitOrder.test.ts index 922d53f884..3e484d5f45 100644 --- a/packages/swap/src/helpers/orders/makeLimitOrder.test.ts +++ b/packages/swap/src/helpers/orders/makeLimitOrder.test.ts @@ -14,13 +14,14 @@ describe('makeLimitOrder', () => { const pool: Swap.Pool = { tokenA: {quantity: '4500000', tokenId: 'tokenA'}, tokenB: {quantity: '9000000', tokenId: 'tokenB'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', fee: '0.3', provider: 'minswap', price: 2, batcherFee: {quantity: '1', tokenId: ''}, deposit: {quantity: '1', tokenId: ''}, poolId: '0', - lastUpdate: '0', lpToken: { quantity: '0', tokenId: '0', diff --git a/packages/swap/src/helpers/orders/makePossibleMarketOrder.test.ts b/packages/swap/src/helpers/orders/makePossibleMarketOrder.test.ts index 31037da639..3146240bf9 100644 --- a/packages/swap/src/helpers/orders/makePossibleMarketOrder.test.ts +++ b/packages/swap/src/helpers/orders/makePossibleMarketOrder.test.ts @@ -15,13 +15,14 @@ describe('makePossibleMarketOrder', () => { const pool1: Swap.Pool = { tokenA: {quantity: '4500000', tokenId: 'tokenA'}, tokenB: {quantity: '9000000', tokenId: 'tokenB'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', fee: '0.3', provider: 'minswap', price: 2, batcherFee: {quantity: '1', tokenId: ''}, deposit: {quantity: '1', tokenId: ''}, poolId: '0', - lastUpdate: '0', lpToken: { quantity: '0', tokenId: '0', @@ -30,13 +31,14 @@ describe('makePossibleMarketOrder', () => { const pool2: Swap.Pool = { tokenA: {quantity: '5500000', tokenId: 'tokenA'}, tokenB: {quantity: '9000000', tokenId: 'tokenB'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', fee: '0.3', provider: 'sundaeswap', price: 2, batcherFee: {quantity: '10', tokenId: ''}, deposit: {quantity: '1', tokenId: ''}, poolId: '0', - lastUpdate: '0', lpToken: { quantity: '0', tokenId: '0', diff --git a/packages/swap/src/helpers/transformers.test.ts b/packages/swap/src/helpers/transformers.test.ts index 77c797cb37..c8d7dfd6a4 100644 --- a/packages/swap/src/helpers/transformers.test.ts +++ b/packages/swap/src/helpers/transformers.test.ts @@ -107,7 +107,10 @@ describe('asYoroiAmount', () => { it('success', () => { const result = transformers.asYoroiAmount({ amount: '100', - token: 'c04f4200502a998e9eebafac0291a1f38008de3fe146d136946d8f4b.30', + address: { + policyId: 'c04f4200502a998e9eebafac0291a1f38008de3fe146d136946d8f4b', + name: '30', + }, }) expect(result).toEqual({ quantity: '100', @@ -115,10 +118,23 @@ describe('asYoroiAmount', () => { }) }) + it('success nameless token', () => { + const result = transformers.asYoroiAmount({ + token: 'c04f4200502a998e9eebafac0291a1f38008de3fe146d136946d8f4b', + }) + expect(result).toEqual({ + quantity: '0', + tokenId: 'c04f4200502a998e9eebafac0291a1f38008de3fe146d136946d8f4b.', + }) + }) + it('success (lovelace) primary token', () => { const result = transformers.asYoroiAmount({ amount: '1000000', - token: 'lovelace', + address: { + policyId: '', + name: '', + }, }) expect(result).toEqual({quantity: '1000000', tokenId: ''}) }) @@ -126,7 +142,10 @@ describe('asYoroiAmount', () => { it('success (period) primary token', () => { const result = transformers.asYoroiAmount({ amount: '1000000', - token: '.', + address: { + policyId: '', + name: '.', + }, }) expect(result).toEqual({quantity: '1000000', tokenId: ''}) }) @@ -178,24 +197,24 @@ describe('asYoroiPools', () => { }) it('success (filter out unsupported pools)', () => { - const result = transformers.asYoroiPools(openswapMocks.getPools) + const result = transformers.asYoroiPools(openswapMocks.getLiquidityPools) expect(result).toEqual>(apiMocks.getPools) // should filter out unsupported pools - expect(result.length).toBe(openswapMocks.getPools.length - 1) + expect(result.length).toBe(openswapMocks.getLiquidityPools.length - 1) }) }) describe('asYoroiPool', () => { it('success (supported pool)', () => { - const result = transformers.asYoroiPool(openswapMocks.getPools[0]!) + const result = transformers.asYoroiPool(openswapMocks.getLiquidityPools[0]!) expect(result).toEqual(apiMocks.getPools[0]!) }) it('success (unsupported pool)', () => { - const result = transformers.asYoroiPool(openswapMocks.getPools[3]!) + const result = transformers.asYoroiPool(openswapMocks.getLiquidityPools[3]!) expect(result).toBeNull() }) diff --git a/packages/swap/src/helpers/transformers.ts b/packages/swap/src/helpers/transformers.ts index 880dda8b1d..45141a61e6 100644 --- a/packages/swap/src/helpers/transformers.ts +++ b/packages/swap/src/helpers/transformers.ts @@ -5,6 +5,7 @@ import {isString} from '@yoroi/common' import {Quantities} from '../utils/quantities' import {supportedProviders} from '../translators/constants' +import {asQuantity} from '../utils/asQuantity' export const transformersMaker = ( primaryTokenId: Balance.Token['info']['id'], @@ -49,11 +50,18 @@ export const transformersMaker = ( const asYoroiOpenOrder = (openswapOrder: OpenSwap.OpenOrder) => { const {from, to, deposit, ...rest} = openswapOrder + const [policyId, name = ''] = primaryTokenId.split('.') as [string, string?] return { ...rest, from: asYoroiAmount(from), to: asYoroiAmount(to), - deposit: asYoroiAmount({amount: deposit, token: primaryTokenId}), + deposit: asYoroiAmount({ + amount: deposit, + address: { + policyId, + name, + }, + }), } as const } @@ -108,31 +116,32 @@ export const transformersMaker = ( return balanceToken } - const asYoroiPool = (openswapPool: OpenSwap.Pool): Swap.Pool | null => { + const asYoroiPool = ( + openswapLiquidityPool: OpenSwap.LiquidityPool, + ): Swap.Pool | null => { const { batcherFee, - fee, - deposit, + poolFee, + lvlDeposit, lpToken, tokenA, tokenB, - timestamp, provider, - price, poolId, - } = openswapPool + } = openswapLiquidityPool if (provider && !isSupportedProvider(provider)) return null const pool: Swap.Pool = { tokenA: asYoroiAmount(tokenA), tokenB: asYoroiAmount(tokenB), - deposit: asYoroiAmount({amount: deposit.toString(), token: ''}), + ptPriceTokenA: tokenA.priceAda.toString(), + ptPriceTokenB: tokenB.priceAda.toString(), + deposit: asYoroiAmount({amount: lvlDeposit, address: undefined}), lpToken: asYoroiAmount(lpToken), - batcherFee: asYoroiAmount(batcherFee), - lastUpdate: timestamp, - fee, - price, + batcherFee: asYoroiAmount({amount: batcherFee, address: undefined}), + fee: poolFee, + price: 0, poolId, provider, } @@ -140,31 +149,48 @@ export const transformersMaker = ( } const asYoroiAmount = (openswapAmount: { - amount: string - token: string + address?: { + policyId: string + name: string + } + // openswap is inconsistent about ADA + // sometimes is '.', '' or 'lovelace' + token?: string + amount?: string }): Balance.Amount => { - if (isString(openswapAmount?.amount)) { - // openswap is inconsistent about ADA - // sometimes is '.', '' or 'lovelace' - const {amount, token} = openswapAmount - const [policyId, name = ''] = token.split('.') as [string, string?] - return { - quantity: amount as Balance.Quantity, - tokenId: asYoroiTokenId({policyId, name}), - } as const + const {amount, address, token} = openswapAmount ?? {} + + let policyId = '' + let name = '' + + if (address) { + policyId = address.policyId + name = address.name + } else if (isString(token)) { + const tokenParts = token.split('.') as [string, string?] + policyId = tokenParts[0] + name = tokenParts[1] ?? '' } - return {quantity: Quantities.zero, tokenId: ''} as const + + const yoroiAmount: Balance.Amount = { + quantity: asQuantity(amount ?? Quantities.zero), + tokenId: asYoroiTokenId({policyId, name}), + } as const + + return yoroiAmount } /** * Filter out pools that are not supported by Yoroi * - * @param openswapPools + * @param openswapLiquidityPools * @returns {Swap.Pool[]} */ - const asYoroiPools = (openswapPools: OpenSwap.Pool[]): Swap.Pool[] => { - if (openswapPools?.length > 0) - return openswapPools + const asYoroiPools = ( + openswapLiquidityPools: OpenSwap.LiquidityPool[], + ): Swap.Pool[] => { + if (openswapLiquidityPools?.length > 0) + return openswapLiquidityPools .map(asYoroiPool) .filter((pool): pool is Swap.Pool => pool !== null) diff --git a/packages/swap/src/manager.mocks.ts b/packages/swap/src/manager.mocks.ts index 1c253f3081..71b020e47b 100644 --- a/packages/swap/src/manager.mocks.ts +++ b/packages/swap/src/manager.mocks.ts @@ -172,6 +172,7 @@ export const mockSwapManager: Swap.Manager = { clearStorage: clear.success, primaryTokenId: '', stakingKey: '', + supportedProviders: [] as const, } as const export const mockSwapManagerDefault: Swap.Manager = { @@ -197,4 +198,5 @@ export const mockSwapManagerDefault: Swap.Manager = { clearStorage: clear.error.unknown, primaryTokenId: '', stakingKey: '', + supportedProviders: [] as const, } as const diff --git a/packages/swap/src/manager.test.ts b/packages/swap/src/manager.test.ts index 520f34d0c2..a40c2a1cff 100644 --- a/packages/swap/src/manager.test.ts +++ b/packages/swap/src/manager.test.ts @@ -24,6 +24,7 @@ describe('swapManagerMaker', () => { getCompletedOrders: jest.fn(), primaryTokenId: '', stakingKey: 'someStakingKey', + supportedProviders: ['minswap'] as const, } beforeEach(() => { @@ -33,7 +34,7 @@ describe('swapManagerMaker', () => { it('clearStorage clear', async () => { await expect(manager.clearStorage()).resolves.toBeUndefined() - await expect(mockedStorage.clear).toHaveBeenCalledTimes(1) + expect(mockedStorage.clear).toHaveBeenCalledTimes(1) }) it('slippage', async () => { diff --git a/packages/swap/src/manager.ts b/packages/swap/src/manager.ts index 9014335e13..eebd615374 100644 --- a/packages/swap/src/manager.ts +++ b/packages/swap/src/manager.ts @@ -14,6 +14,7 @@ export const swapManagerMaker = ( createOrder, primaryTokenId, stakingKey, + supportedProviders, } = swapApi const order = { @@ -45,5 +46,6 @@ export const swapManagerMaker = ( pools, primaryTokenId, stakingKey, + supportedProviders, } as const } diff --git a/packages/swap/src/translators/reactjs/provider/SwapProvider.test.tsx b/packages/swap/src/translators/reactjs/provider/SwapProvider.test.tsx index 57aa64942a..5bfbd9a760 100644 --- a/packages/swap/src/translators/reactjs/provider/SwapProvider.test.tsx +++ b/packages/swap/src/translators/reactjs/provider/SwapProvider.test.tsx @@ -206,7 +206,6 @@ describe('SwapProvider', () => { fee: '0.5', batcherFee: {tokenId: '', quantity: '1'}, deposit: {tokenId: '', quantity: '1'}, - lastUpdate: '123', lpToken: {tokenId: '', quantity: '1'}, poolId: '1', price: 2, @@ -263,7 +262,6 @@ describe('SwapProvider', () => { fee: '0.5', batcherFee: {tokenId: '', quantity: '1'}, deposit: {tokenId: '', quantity: '1'}, - lastUpdate: '123', lpToken: {tokenId: '', quantity: '1'}, poolId: '1', price: 2, @@ -333,7 +331,6 @@ describe('SwapProvider', () => { fee: '0.5', batcherFee: {tokenId: '', quantity: '1'}, deposit: {tokenId: '', quantity: '1'}, - lastUpdate: '123', lpToken: {tokenId: '', quantity: '1'}, poolId: '1', price: 1, diff --git a/packages/swap/src/translators/reactjs/state/state.mocks.ts b/packages/swap/src/translators/reactjs/state/state.mocks.ts index 9c8748a40c..990f2c1a90 100644 --- a/packages/swap/src/translators/reactjs/state/state.mocks.ts +++ b/packages/swap/src/translators/reactjs/state/state.mocks.ts @@ -24,11 +24,12 @@ export const mockSwapStateDefault: SwapState = { fee: '', tokenA: {tokenId: '', quantity: '0'}, tokenB: {tokenId: '', quantity: '0'}, + ptPriceTokenA: '0', + ptPriceTokenB: '0', price: 0, batcherFee: {tokenId: '', quantity: '0'}, deposit: {tokenId: '', quantity: '0'}, poolId: '', - lastUpdate: '', lpToken: {tokenId: '', quantity: '0'}, }, }, diff --git a/packages/swap/src/translators/reactjs/state/state.test.ts b/packages/swap/src/translators/reactjs/state/state.test.ts index 691d166b72..be8f673c5d 100644 --- a/packages/swap/src/translators/reactjs/state/state.test.ts +++ b/packages/swap/src/translators/reactjs/state/state.test.ts @@ -157,12 +157,13 @@ describe('State Actions', () => { fee: '0.5', batcherFee: {tokenId: '', quantity: '1'}, deposit: {tokenId: '', quantity: '1'}, - lastUpdate: '123', lpToken: {tokenId: '', quantity: '1'}, poolId: '1', price: 1, tokenA: {tokenId: '', quantity: '1'}, tokenB: {tokenId: '', quantity: '1'}, + ptPriceTokenA: '1', + ptPriceTokenB: '1', }, } const expectedState = produce(mockSwapStateDefault, (draft) => { @@ -182,12 +183,13 @@ describe('State Actions', () => { fee: '0.5', batcherFee: {tokenId: '', quantity: '1'}, deposit: {tokenId: '', quantity: '1'}, - lastUpdate: '123', lpToken: {tokenId: '', quantity: '1'}, poolId: '1', price: 1, tokenA: {tokenId: '', quantity: '1'}, tokenB: {tokenId: '', quantity: '1'}, + ptPriceTokenA: '1', + ptPriceTokenB: '1', }, } diff --git a/packages/swap/src/utils/quantities.test.ts b/packages/swap/src/utils/quantities.test.ts index eda0af30e2..2878e15726 100644 --- a/packages/swap/src/utils/quantities.test.ts +++ b/packages/swap/src/utils/quantities.test.ts @@ -146,6 +146,13 @@ describe('Quantities', () => { const denomination = 2 expect(Quantities.format(quantity, denomination)).toBe('0.0001') }) + + it('should format a quantity with the specified denomination and precision', () => { + const quantity = '0.01' + const denomination = 2 + const precision = 1 + expect(Quantities.format(quantity, denomination, precision)).toBe('0') + }) }) it('should find the maximum quantity from an array of quantities', () => { diff --git a/packages/types/src/swap/api.ts b/packages/types/src/swap/api.ts index f84ddd0842..ee59c50334 100644 --- a/packages/types/src/swap/api.ts +++ b/packages/types/src/swap/api.ts @@ -6,7 +6,7 @@ import { SwapCreateOrderResponse, SwapOpenOrder, } from './order' -import {SwapPool} from './pool' +import {SwapPool, SwapPoolProvider} from './pool' export interface SwapApi { createOrder(orderData: SwapCreateOrderData): Promise @@ -16,8 +16,10 @@ export interface SwapApi { getPools(args: { tokenA: BalanceToken['info']['id'] tokenB: BalanceToken['info']['id'] + providers?: ReadonlyArray }): Promise getTokens(tokenIdBase: BalanceToken['info']['id']): Promise stakingKey: string primaryTokenId: BalanceToken['info']['id'] + supportedProviders: ReadonlyArray } diff --git a/packages/types/src/swap/manager.ts b/packages/types/src/swap/manager.ts index c5b2ec1b86..9e9359c11b 100644 --- a/packages/types/src/swap/manager.ts +++ b/packages/types/src/swap/manager.ts @@ -1,5 +1,6 @@ import {BalanceToken} from '../balance/token' import {SwapApi} from './api' +import {SwapPoolProvider} from './pool' import {SwapStorage} from './storage' export type SwapManager = Readonly<{ @@ -25,4 +26,5 @@ export type SwapManager = Readonly<{ } stakingKey: string primaryTokenId: BalanceToken['info']['id'] + supportedProviders: ReadonlyArray }> diff --git a/packages/types/src/swap/pool.ts b/packages/types/src/swap/pool.ts index 4baa839c3f..bf34c0c970 100644 --- a/packages/types/src/swap/pool.ts +++ b/packages/types/src/swap/pool.ts @@ -27,11 +27,12 @@ export type SwapPool = { fee: string // % pool liquidity provider fee, usually 0.3. tokenA: BalanceAmount tokenB: BalanceAmount + ptPriceTokenA: string // float, current price in lovelace of tokenA, i.e. 0.000000000000000000. + ptPriceTokenB: string // float, current price in lovelace of tokenB, i.e. 0.000000000000000000. price: number // float, current price in tokenA / tokenB according to the pool, NOT SUITABLE for price calculations, just for display purposes, i.e. 0.9097362621640215. batcherFee: BalanceAmount deposit: BalanceAmount // amount of deposit / minUTxO required by protocol, returned to user, in lovelace. // utxo: string // txhash#txindex of latest transaction involving this pool. poolId: string // identifier of the pool across platforms. - lastUpdate: string // latest update of this pool in UTC, i.e. 2023-05-23 06:13:26. lpToken: BalanceAmount }