diff --git a/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/CompletedOrders.tsx b/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/CompletedOrders.tsx index d7861f4617..d24cad5767 100644 --- a/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/CompletedOrders.tsx +++ b/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/CompletedOrders.tsx @@ -1,4 +1,7 @@ +import {useOrderByStatusCompleted} from '@yoroi/swap' +import _ from 'lodash' import React from 'react' +import {useIntl} from 'react-intl' import {Linking, ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native' import { @@ -6,51 +9,92 @@ import { ExpandableInfoCardSkeleton, HeaderWrapper, HiddenInfoWrapper, - Icon, MainInfoWrapper, Spacer, Text, + TokenIcon, } from '../../../../../components' +import {useLanguage} from '../../../../../i18n' import {useSearch} from '../../../../../Search/SearchContext' +import {useSelectedWallet} from '../../../../../SelectedWallet' import {COLORS} from '../../../../../theme' +import {useTokenInfos, useTransactionInfos} from '../../../../../yoroi-wallets/hooks' import {Counter} from '../../../common/Counter/Counter' +import {PoolIcon} from '../../../common/PoolIcon/PoolIcon' import {useStrings} from '../../../common/strings' -import {OrderProps} from './mapOrders' -import {getMockOrders} from './mocks' +import {mapOrders} from './mapOrders' export const CompletedOrders = () => { const strings = useStrings() - const {search} = useSearch() + const wallet = useSelectedWallet() const [hiddenInfoOpenId, setHiddenInfoOpenId] = React.useState(null) - const orders = getMockOrders().filter( - ({assetFromLabel, assetToLabel}) => - assetFromLabel.toLocaleLowerCase().includes(search.toLocaleLowerCase()) || - assetToLabel.toLocaleLowerCase().includes(search.toLocaleLowerCase()), - ) + const transactionsInfos = useTransactionInfos(wallet) + const {search} = useSearch() + const {numberLocale} = useLanguage() + const intl = useIntl() + + const orders = useOrderByStatusCompleted({ + queryKey: [wallet.id, 'completed-orders'], + onError: (err) => { + console.log(err) + }, + }) + const tokenIds = _.uniq(orders.flatMap((o) => [o.from.tokenId, o.to.tokenId])) + + const tokenInfos = useTokenInfos({wallet, tokenIds: tokenIds}) + + const normalizedOrders = mapOrders(orders, tokenInfos, numberLocale, Object.values(transactionsInfos)) + + const searchLower = search.toLocaleLowerCase() + + const filteredOrders = normalizedOrders.filter((order) => { + return ( + order.assetFromLabel.toLocaleLowerCase().includes(searchLower) || + order.assetToLabel.toLocaleLowerCase().includes(searchLower) + ) + }) return ( <> - {orders.map((order) => { + {filteredOrders.map((order) => { const id = `${order.assetFromLabel}-${order.assetToLabel}-${order.date}` const extended = id === hiddenInfoOpenId + const liquidityPoolIcon = + const fromIcon = + const toIcon = return ( } + adornment={ + + } header={
setHiddenInfoOpenId(hiddenInfoOpenId !== id ? id : null)} assetFromLabel={order.assetFromLabel} assetToLabel={order.assetToLabel} + assetFromIcon={fromIcon} + assetToIcon={toIcon} extended={extended} /> } extended={extended} withBoxShadow > - + ) })} @@ -64,18 +108,22 @@ export const CompletedOrders = () => { const Header = ({ assetFromLabel, assetToLabel, + assetFromIcon, + assetToIcon, extended, onPress, }: { assetFromLabel: string assetToLabel: string + assetFromIcon: React.ReactNode + assetToIcon: React.ReactNode extended: boolean onPress: () => void }) => { return ( - + {assetFromIcon} @@ -85,7 +133,7 @@ const Header = ({ - + {assetToIcon} @@ -95,32 +143,48 @@ const Header = ({ ) } -const HiddenInfo = ({order}: {id: string; order: OrderProps}) => { +const HiddenInfo = ({ + total, + liquidityPoolIcon, + liquidityPoolName, + poolUrl, + date, + txId, + txLink, +}: { + total: string + liquidityPoolIcon: React.ReactNode + liquidityPoolName: string + poolUrl: string + date: string + txId: string + txLink: string +}) => { const strings = useStrings() return ( {[ { label: strings.listOrdersTotal, - value: order.total, + value: total, }, { label: strings.listOrdersLiquidityPool, value: ( ), }, { label: strings.listOrdersTimeCreated, - value: order.date, + value: date, }, { label: strings.listOrdersTxId, - value: , + value: , }, ].map((item) => ( @@ -129,13 +193,13 @@ const HiddenInfo = ({order}: {id: string; order: OrderProps}) => { ) } -const MainInfo = ({order}: {order: OrderProps}) => { +const MainInfo = ({tokenPrice, tokenAmount}: {tokenPrice: string; tokenAmount: string}) => { const strings = useStrings() return ( {[ - {label: strings.listOrdersSheetAssetPrice, value: order.tokenPrice}, - {label: strings.listOrdersSheetAssetAmount, value: order.tokenAmount}, + {label: strings.listOrdersSheetAssetPrice, value: tokenPrice}, + {label: strings.listOrdersSheetAssetAmount, value: tokenAmount}, ].map((item, index) => ( ))} diff --git a/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/OpenOrders.tsx b/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/OpenOrders.tsx index d5f3f7327c..741d566f55 100644 --- a/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/OpenOrders.tsx +++ b/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/OpenOrders.tsx @@ -46,6 +46,7 @@ export const OpenOrders = () => { const {search} = useSearch() const orders = useOrderByStatusOpen({ + queryKey: [wallet.id, 'open-orders'], onError: (err) => { console.log(err) }, diff --git a/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/mapOrders.ts b/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/mapOrders.ts index 46cbcc8e2d..f8486809df 100644 --- a/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/mapOrders.ts +++ b/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/mapOrders.ts @@ -3,27 +3,12 @@ import {Balance} from '@yoroi/types' import {SwapOrder} from '@yoroi/types/lib/swap/order' import {isString} from '@yoroi/wallets' import BigNumber from 'bignumber.js' -import React from 'react' import {NumberLocale} from '../../../../../i18n/languages' import {TransactionInfo} from '../../../../../yoroi-wallets/types' import {Quantities} from '../../../../../yoroi-wallets/utils' -export type OrderProps = { - tokenPrice: string - tokenAmount: string - assetFromLabel: string - assetFromIcon: React.ReactNode - assetToLabel: string - assetToIcon: React.ReactNode - date: string - liquidityPoolIcon: React.ReactNode - liquidityPoolName: string - txId: string - total: string - poolUrl: string - txLink: string -} +const MAX_DECIMALS = 10 export const mapOrders = ( orders: Array, @@ -37,15 +22,19 @@ export const mapOrders = ( const id = `${order.from.tokenId}-${order.to.tokenId}-${order.utxo}` const fromLabel = fromTokenInfo?.ticker ?? fromTokenInfo?.name ?? '-' const toLabel = toTokenInfo?.ticker ?? toTokenInfo?.name ?? '-' - const tokenAmount = BigNumber(Quantities.denominated(order.to.quantity, toTokenInfo?.decimals ?? 0)).toFormat( - numberLocale, - ) + const tokenAmount = BigNumber(Quantities.denominated(order.to.quantity, toTokenInfo?.decimals ?? 0)) + .decimalPlaces(MAX_DECIMALS) + .toFormat({ + ...numberLocale, + }) const tokenPrice = BigNumber( Quantities.quotient( Quantities.denominated(order.from.quantity, fromTokenInfo?.decimals ?? 0), Quantities.denominated(order.to.quantity, toTokenInfo?.decimals ?? 0), ), - ).toFormat(numberLocale) + ) + .decimalPlaces(MAX_DECIMALS) + .toFormat(numberLocale) const txId = order.utxo.split('#')[0] const total = BigNumber(Quantities.denominated(order.from.quantity, fromTokenInfo?.decimals ?? 0)).toFormat( numberLocale, diff --git a/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/mocks.tsx b/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/mocks.tsx deleted file mode 100644 index fc01a12e65..0000000000 --- a/apps/wallet-mobile/src/features/Swap/useCases/StartSwapScreen/ListOrders/mocks.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react' -import {Defs, Image, Path, Pattern, Rect, Svg, Use} from 'react-native-svg' - -import {OrderProps} from './mapOrders' - -export const getMockOrders: () => Array = () => [ - { - assetFromLabel: 'ADA', - tokenPrice: '3 ADA', - tokenAmount: '3 USDA', - date: '26/05/2023 22:29:27', - assetFromIcon: , - assetToLabel: 'USDA', - assetToIcon: , - total: '11 ADA', - liquidityPoolName: 'Minswap', - liquidityPoolIcon: , - txId: '12334555...1224', - poolUrl: 'https://google.com', - txLink: 'https://google.com', - }, - { - date: '26/05/2023 22:29:27', - assetFromLabel: 'ADA', - assetFromIcon: , - assetToLabel: 'DOGE', - assetToIcon: , - tokenPrice: '3 ADA', - tokenAmount: '3 USDA', - liquidityPoolName: 'Minswap', - liquidityPoolIcon: , - txId: '12334555...1224', - total: '11 ADA', - poolUrl: 'https://google.com', - txLink: 'https://google.com', - }, -] - -const MinswapIcon = (props) => { - return ( - - - - - - - - - - - - ) -} - -const AdaIcon = (props) => { - return ( - - - - - - - - - - - - ) -} diff --git a/packages/openswap/src/api.ts b/packages/openswap/src/api.ts index 591ca8d533..3f26be944e 100644 --- a/packages/openswap/src/api.ts +++ b/packages/openswap/src/api.ts @@ -2,6 +2,7 @@ import {AxiosInstance} from 'axios' import { cancelOrder, // returns an unsigned transaction to cancel the order. createOrder, // returns a datum and a contract address to create the order transaction. + getCompletedOrders, getOrders, // returns all orders for a given stake key hash. } from './orders' import {getPools} from './pools' @@ -43,6 +44,13 @@ export class OpenSwapApi { ) } + public async getCompletedOrders(stakeKeyHash: string) { + return getCompletedOrders( + {network: this.network, client: this.client}, + {stakeKeyHash}, + ) + } + public async getPools({ tokenA, tokenB, diff --git a/packages/openswap/src/config.ts b/packages/openswap/src/config.ts index 2df50c2898..67ec4d65e8 100644 --- a/packages/openswap/src/config.ts +++ b/packages/openswap/src/config.ts @@ -4,6 +4,7 @@ export const SWAP_API_ENDPOINTS = { mainnet: { getPools: 'https://onchain2.muesliswap.com/pools/pair', getOrders: 'https://onchain2.muesliswap.com/orders/all/', + getCompletedOrders: 'https://api.muesliswap.com/orders/v2', getTokens: 'https://api.muesliswap.com/list', constructSwapDatum: 'https://aggregator.muesliswap.com/constructSwapDatum', cancelSwapTransaction: @@ -12,6 +13,7 @@ export const SWAP_API_ENDPOINTS = { preprod: { getPools: 'https://preprod.pools.muesliswap.com/pools/pair', getOrders: 'https://preprod.pools.muesliswap.com/orders/all/', + getCompletedOrders: 'https://api.muesliswap.com/orders/v2', getTokens: 'https://preprod.api.muesliswap.com/list', constructSwapDatum: 'https://aggregator.muesliswap.com/constructTestnetSwapDatum', diff --git a/packages/openswap/src/orders.ts b/packages/openswap/src/orders.ts index d858309d2c..2245867c3b 100644 --- a/packages/openswap/src/orders.ts +++ b/packages/openswap/src/orders.ts @@ -5,6 +5,7 @@ import type { CreateOrderRequest, CreateOrderResponse, Order, + ApiV2Order, } from './types' export async function createOrder( @@ -89,3 +90,43 @@ export async function getOrders( return response.data } + +export async function getCompletedOrders( + deps: ApiDeps, + args: {stakeKeyHash: string}, +): Promise { + const {network, client} = deps + const {stakeKeyHash} = args + const apiUrl = SWAP_API_ENDPOINTS[network].getCompletedOrders + const response = await client.get(apiUrl, { + params: { + 'stake-key-hash': stakeKeyHash, + 'canceled': 'n', + 'open': 'n', + 'matched': 'y', + 'v2_only': 'y', + }, + }) + + if (response.status !== 200) { + throw new Error(`Failed to get orders for ${stakeKeyHash}`, { + cause: response.data, + }) + } + + return response.data + .filter((order) => order.status === 'matched') + .map((order) => ({ + provider: 'muesliswap_v4', // https://api.muesliswap.com/orders/v2 does not respond with the `provider` yet + utxo: order.txHash, + from: { + amount: order.fromAmount, + token: `${order.fromToken.address.policyId}.${order.fromToken.address.name}`, + }, + to: { + amount: order.toAmount, + token: `${order.toToken.address.policyId}.${order.toToken.address.name}`, + }, + deposit: '0', // https://api.muesliswap.com/orders/v2 does not respond with the `deposit` yet + })) +} diff --git a/packages/openswap/src/types.ts b/packages/openswap/src/types.ts index d13c5f6d9b..cedbd876ee 100644 --- a/packages/openswap/src/types.ts +++ b/packages/openswap/src/types.ts @@ -40,6 +40,34 @@ export type Order = { utxo: string } +export type ApiV2Order = { + toToken: { + address: { + policyId: string + name: string + } + } + toAmount: string + fromToken: { + address: { + policyId: string + name: string + } + } + fromAmount: string + placedAt: number + status: string + receivedAmount: string + paidAmount: string + finalizedAt: any + txHash: string + outputIdx: number + attachedLvl: string + scriptVersion: string + pubKeyHash: string + feeField: number +} + export type Protocol = | 'minswap' | 'sundaeswap' diff --git a/packages/swap/src/adapters/api.ts b/packages/swap/src/adapters/api.ts index 28e289e2d0..5438a5f29b 100644 --- a/packages/swap/src/adapters/api.ts +++ b/packages/swap/src/adapters/api.ts @@ -29,6 +29,12 @@ export const makeSwapApi = ( .then((orders) => asYoroiOrders(orders, primaryTokenId)) } + const getCompletedOrders: Swap.Api['getCompletedOrders'] = async () => { + return api + .getCompletedOrders(stakingKey) + .then((orders) => asYoroiOrders(orders, primaryTokenId)) + } + const createOrder: Swap.Api['createOrder'] = async ( orderData, ): Promise => { @@ -97,5 +103,6 @@ export const makeSwapApi = ( createOrder, getTokens, getPoolPairs, + getCompletedOrders, } as const } diff --git a/packages/swap/src/translators/swapManager.ts b/packages/swap/src/translators/swapManager.ts index 1fbf98533e..b6c6d46437 100644 --- a/packages/swap/src/translators/swapManager.ts +++ b/packages/swap/src/translators/swapManager.ts @@ -5,14 +5,21 @@ export const makeSwapManager = ( swapApi: Swap.Api, ): Readonly => { const {clear: clearStorage, slippage} = swapStorage - const {getPoolPairs, getOrders, getTokens, cancelOrder, createOrder} = swapApi + const { + getPoolPairs, + getOrders, + getCompletedOrders, + getTokens, + cancelOrder, + createOrder, + } = swapApi const order = { cancel: cancelOrder, create: createOrder, list: { byStatusOpen: getOrders, - byStatusCompleted: getOrders, + byStatusCompleted: getCompletedOrders, } as const, } diff --git a/packages/types/src/swap/api.ts b/packages/types/src/swap/api.ts index 41a42d3035..812604adff 100644 --- a/packages/types/src/swap/api.ts +++ b/packages/types/src/swap/api.ts @@ -11,6 +11,7 @@ export interface SwapApi { createOrder(orderData: SwapCreateOrderData): Promise cancelOrder(orderData: SwapCancelOrderData): Promise getOrders(): Promise + getCompletedOrders(): Promise getPoolPairs(args: { tokenA: BalanceToken['info']['id'] tokenB: BalanceToken['info']['id']