Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: token list api source #2860

Merged
merged 1 commit into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions apps/wallet-mobile/src/components/AmountItem/AmountItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ export type AmountItemProps = {
isPrivacyOff?: boolean
status?: string
inWallet?: boolean
supply?: string
variant?: 'swap'
}

export const AmountItem = ({isPrivacyOff, wallet, style, amount, inWallet, supply, variant}: AmountItemProps) => {
export const AmountItem = ({isPrivacyOff, wallet, style, amount, inWallet, variant}: AmountItemProps) => {
const {quantity, tokenId} = amount
const tokenInfo = useTokenInfo({wallet, tokenId})

Expand Down Expand Up @@ -63,13 +62,15 @@ export const AmountItem = ({isPrivacyOff, wallet, style, amount, inWallet, suppl
</Middle>

<Right>
{tokenInfo.kind !== 'nft' && (
{tokenInfo.kind !== 'nft' && variant !== 'swap' && (
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed the variant swap to don't list supply / pair

<Text style={styles.quantity} testID="tokenAmountText">
{isPrivacyOff ? '**.*******' : variant === 'swap' ? `${supply}` : formattedQuantity}
{isPrivacyOff ? '**.*******' : formattedQuantity}
</Text>
)}

{isPrimary && <PairedBalance isPrivacyOff={isPrivacyOff} amount={{quantity, tokenId: tokenInfo.id}} />}
{isPrimary && variant !== 'swap' && (
<PairedBalance isPrivacyOff={isPrivacyOff} amount={{quantity, tokenId: tokenInfo.id}} />
)}
</Right>
</View>
)
Expand Down
4 changes: 2 additions & 2 deletions apps/wallet-mobile/src/features/Swap/SwapNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs'
import {useSwap, useSwapTokensByPairToken} from '@yoroi/swap'
import {useSwap, useSwapTokensOnlyVerified} from '@yoroi/swap'
import React from 'react'
import {StyleSheet} from 'react-native'
import {SafeAreaView} from 'react-native-safe-area-context'
Expand Down Expand Up @@ -57,7 +57,7 @@ export const SwapTabNavigator = () => {
}, [aggregatorTokenId, lpTokenHeld, lpTokenHeldChanged])

// pre load swap tokens
const {refetch} = useSwapTokensByPairToken('', {suspense: false, enabled: false})
const {refetch} = useSwapTokensOnlyVerified({suspense: false, enabled: false})
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre-fetch updated, until we migrate router for tan stack router

React.useEffect(() => {
refetch()
}, [refetch])
Expand Down
22 changes: 9 additions & 13 deletions apps/wallet-mobile/src/features/Swap/common/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,26 +73,22 @@ function containsOnlyValidChars(str?: string): boolean {
return typeof str === 'string' && validCharsRegex.test(str)
}

export const sortTokensByName = (a: Balance.Token, b: Balance.Token, wallet: YoroiWallet) => {
const isValidNameA = containsOnlyValidChars(a.info.name)
const isValidNameB = containsOnlyValidChars(b.info.name)
const isValidTickerA = containsOnlyValidChars(a.info.ticker)
const isValidTickerB = containsOnlyValidChars(b.info.ticker)
export const sortTokensByName = (a: Balance.TokenInfo, b: Balance.TokenInfo, wallet: YoroiWallet) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new endpoint returns TokenInfo not Token, adjusted

const isValidNameA = containsOnlyValidChars(a.name)
const isValidNameB = containsOnlyValidChars(b.name)
const isValidTickerA = containsOnlyValidChars(a.ticker)
const isValidTickerB = containsOnlyValidChars(b.ticker)

const nameA =
a.info.ticker?.toLocaleLowerCase() && isValidTickerA
? a.info.ticker?.toLocaleLowerCase()
: a.info.name.toLocaleLowerCase()
a.ticker?.toLocaleLowerCase() && isValidTickerA ? a.ticker?.toLocaleLowerCase() : a.name.toLocaleLowerCase()

const nameB =
b.info.ticker?.toLocaleLowerCase() && isValidTickerB
? b.info.ticker?.toLocaleLowerCase()
: b.info.name.toLocaleLowerCase()
b.ticker?.toLocaleLowerCase() && isValidTickerB ? b.ticker?.toLocaleLowerCase() : b.name.toLocaleLowerCase()

const isBPrimary = b.info.ticker === wallet.primaryTokenInfo.ticker
const isBPrimary = b.ticker === wallet.primaryTokenInfo.ticker
if (isBPrimary) return 1

const isAPrimary = a.info.ticker === wallet.primaryTokenInfo.ticker
const isAPrimary = a.ticker === wallet.primaryTokenInfo.ticker
if (isAPrimary) return -1

if (!isValidNameA && isValidNameB) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {FlashList} from '@shopify/flash-list'
import {useSwap, useSwapTokensByPairToken} from '@yoroi/swap'
import {useSwap, useSwapTokensOnlyVerified} from '@yoroi/swap'
import {Balance} from '@yoroi/types'
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
Expand All @@ -13,7 +13,7 @@ import {useSelectedWallet} from '../../../../../../../SelectedWallet'
import {COLORS} from '../../../../../../../theme'
import {YoroiWallet} from '../../../../../../../yoroi-wallets/cardano/types'
import {useBalance, useBalances} from '../../../../../../../yoroi-wallets/hooks'
import {Amounts, asQuantity, Quantities} from '../../../../../../../yoroi-wallets/utils'
import {Amounts} from '../../../../../../../yoroi-wallets/utils'
import {NoAssetFoundImage} from '../../../../../../Send/common/NoAssetFoundImage'
import {Counter} from '../../../../../common/Counter/Counter'
import {filterBySearch} from '../../../../../common/filterBySearch'
Expand Down Expand Up @@ -60,20 +60,20 @@ export const SelectBuyTokenFromListScreen = () => {
const TokenList = () => {
const strings = useStrings()
const wallet = useSelectedWallet()
const {pairsByToken} = useSwapTokensByPairToken('')
const {onlyVerifiedTokens} = useSwapTokensOnlyVerified()
const {search: assetSearchTerm} = useSearch()
const balances = useBalances(wallet)
const walletTokenIds = Amounts.toArray(balances).map(({tokenId}) => tokenId)

const tokens: Array<Balance.Token> = React.useMemo(() => {
if (pairsByToken === undefined) return []
return pairsByToken
}, [pairsByToken])
const tokenInfos: Array<Balance.TokenInfo> = React.useMemo(() => {
if (onlyVerifiedTokens === undefined) return []
return onlyVerifiedTokens
}, [onlyVerifiedTokens])

const filteredTokenList = React.useMemo(() => {
const filter = filterBySearch(assetSearchTerm)
return tokens.filter((token) => filter(token.info)).sort((a, b) => sortTokensByName(a, b, wallet))
}, [tokens, assetSearchTerm, wallet])
return tokenInfos.filter((tokenInfo) => filter(tokenInfo)).sort((a, b) => sortTokensByName(a, b, wallet))
}, [tokenInfos, assetSearchTerm, wallet])

return (
<View style={styles.list}>
Expand All @@ -83,8 +83,6 @@ const TokenList = () => {

<View style={styles.labels}>
<Text style={styles.label}>{strings.asset}</Text>

<Text style={styles.label}>{strings.volume}</Text>
</View>

<Spacer height={16} />
Expand All @@ -95,13 +93,13 @@ const TokenList = () => {

<FlashList
data={filteredTokenList}
renderItem={({item: token}: {item: Balance.Token}) => (
renderItem={({item: tokenInfo}: {item: Balance.TokenInfo}) => (
<Boundary loading={{fallback: <AmountItemPlaceholder style={styles.item} />}}>
<SelectableToken token={token} wallet={wallet} walletTokenIds={walletTokenIds} />
<SelectableToken tokenInfo={tokenInfo} wallet={wallet} walletTokenIds={walletTokenIds} />
</Boundary>
)}
bounces={false}
keyExtractor={({info: {id, name}}) => `${name}-${id}`}
keyExtractor={({id, name}) => `${name}-${id}`}
testID="assetsList"
estimatedItemSize={72}
ListEmptyComponent={<EmptyList filteredTokensForList={filteredTokenList} />}
Expand Down Expand Up @@ -130,10 +128,11 @@ const TokenList = () => {
type SelectableTokenProps = {
wallet: YoroiWallet
walletTokenIds: Array<string>
token: Balance.Token
tokenInfo: Balance.TokenInfo
}
const SelectableToken = ({wallet, token, walletTokenIds}: SelectableTokenProps) => {
const balanceAvailable = useBalance({wallet, tokenId: token.info.id})
const SelectableToken = ({wallet, tokenInfo, walletTokenIds}: SelectableTokenProps) => {
const {id, name, ticker, group, decimals} = tokenInfo
const balanceAvailable = useBalance({wallet, tokenId: id})
const {closeSearch} = useSearch()
const {buyTokenInfoChanged, orderData} = useSwap()
const {
Expand All @@ -143,17 +142,16 @@ const SelectableToken = ({wallet, token, walletTokenIds}: SelectableTokenProps)
const navigateTo = useNavigateTo()
const {track} = useMetrics()

const isDisabled = token.info.id === orderData.amounts.sell.tokenId && isSellTouched
const inUserWallet = walletTokenIds.includes(token.info.id)
const supplyFormatted = Quantities.format(asQuantity(token.supply?.total), token.info.decimals ?? 0)
const isDisabled = id === orderData.amounts.sell.tokenId && isSellTouched
const inUserWallet = walletTokenIds.includes(tokenInfo.id)

const handleOnTokenSelection = () => {
track.swapAssetToChanged({
to_asset: [{asset_name: token.info.name, asset_ticker: token.info.ticker, policy_id: token.info.group}],
to_asset: [{asset_name: name, asset_ticker: ticker, policy_id: group}],
})
buyTokenInfoChanged({
decimals: token.info.decimals ?? 0,
id: token.info.id,
decimals: decimals ?? 0,
id: id,
})
buyTouched()
navigateTo.startSwap()
Expand All @@ -168,18 +166,17 @@ const SelectableToken = ({wallet, token, walletTokenIds}: SelectableTokenProps)
disabled={isDisabled}
>
<AmountItem
amount={{tokenId: token.info.id, quantity: balanceAvailable}}
amount={{tokenId: id, quantity: balanceAvailable}}
wallet={wallet}
status={token.status}
status="verified"
inWallet={inUserWallet}
supply={supplyFormatted}
variant="swap"
/>
</TouchableOpacity>
)
}

const EmptyList = ({filteredTokensForList}: {filteredTokensForList: Array<Balance.Token>}) => {
const EmptyList = ({filteredTokensForList}: {filteredTokensForList: Array<Balance.TokenInfo>}) => {
const {search: assetSearchTerm, visible: isSearching} = useSearch()

if (isSearching && assetSearchTerm.length > 0 && filteredTokensForList.length === 0)
Expand Down
7 changes: 6 additions & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"npmClient": "yarn",
"version": "independent"
"version": "independent",
"command": {
"run": {
"ignore": ["e2e/*"]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignoring e2e on lerna (it was rebuilding it)

}
}
}
11 changes: 8 additions & 3 deletions packages/openswap/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getCompletedOrders,
getOrders, // returns all orders for a given stake key hash.
} from './orders'
import {getTokenPairs} from './token-pairs'
import {getTokens} from './tokens'
import {
CancelOrderRequest,
Expand Down Expand Up @@ -95,13 +96,17 @@ export class OpenSwapApi {
)
}

public async getTokens({policyId = '', assetName = ''} = {}) {
const tokens = await getTokens(
public async getTokenPairs({policyId = '', assetName = ''} = {}) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getTokens is actually getTokenPairs

const tokenPairs = await getTokenPairs(
{network: this.network, client: this.client},
{policyId, assetName},
)

return tokens
return tokenPairs
}

public async getTokens() {
return getTokens({network: this.network, client: this.client})
}
}

Expand Down
6 changes: 4 additions & 2 deletions packages/openswap/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export const SWAP_API_ENDPOINTS = {
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',
getTokenPairs: 'https://api.muesliswap.com/list',
getTokens: 'https://api.muesliswap.com/token-list',
constructSwapDatum: 'https://aggregator.muesliswap.com/constructSwapDatum',
cancelSwapTransaction:
'https://aggregator.muesliswap.com/cancelSwapTransaction',
Expand All @@ -18,7 +19,8 @@ export const SWAP_API_ENDPOINTS = {
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',
getTokenPairs: 'https://preprod.api.muesliswap.com/list',
getTokens: 'https://preprod.api.muesliswap.com/token-list',
constructSwapDatum:
'https://aggregator.muesliswap.com/constructTestnetSwapDatum',
cancelSwapTransaction:
Expand Down
6 changes: 4 additions & 2 deletions packages/openswap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ export namespace OpenSwap {
export type LiquidityPoolResponse = Types.LiquidityPoolResponse

// Tokens
export type Token = Types.Token
export type TokenResponse = Types.TokenResponse
export type TokenPair = Types.TokenPair
export type TokenInfo = Types.TokenInfo
export type TokenPairsResponse = Types.TokenPairsResponse
export type ListTokensResponse = Types.ListTokensResponse
export type TokenAddress = Types.TokenAddress

export type PriceAddress = Types.PriceAddress
Expand Down
79 changes: 79 additions & 0 deletions packages/openswap/src/token-pairs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {expect, describe, it, vi, Mocked} from 'vitest'
import {getTokenPairs} from './token-pairs'
import {axiosClient} from './config'

vi.mock('./config.ts')

describe('SwapTokenPairsApi', () => {
it('should get all tokens based pairs', async () => {
const mockAxios = axiosClient as Mocked<typeof axiosClient>
mockAxios.get.mockImplementationOnce(() =>
Promise.resolve({status: 200, data: mockedGetTokenPairsResponse}),
)

const result = await getTokenPairs({network: 'mainnet', client: mockAxios})

expect(result).to.be.lengthOf(1)
})

it('should return empty list on preprod network', async () => {
const mockAxios = axiosClient as Mocked<typeof axiosClient>

const result = await getTokenPairs({network: 'preprod', client: mockAxios})

expect(result).to.be.empty
})

it('should throw error for invalid response', async () => {
const mockAxios = axiosClient as Mocked<typeof axiosClient>
mockAxios.get.mockImplementationOnce(() => Promise.resolve({status: 500}))
expect(() =>
getTokenPairs({network: 'mainnet', client: mockAxios}),
).rejects.toThrow('Failed to fetch token pairs')
})
})

const mockedGetTokenPairsResponse = [
{
info: {
supply: {total: '1000000000000', circulating: null},
status: 'unverified',
image: 'ipfs://QmPzaykTy4yfutCtwv7nRUmgbQbA7euiThyy2i9fiFuDHX',
imageIpfsHash: 'QmPzaykTy4yfutCtwv7nRUmgbQbA7euiThyy2i9fiFuDHX',
symbol: 'ARGENT',
minting: {
type: 'time-lock-policy',
blockchain: 'cardano',
mintedBeforeSlotNumber: 91850718,
},
mediatype: 'image/png',
tokentype: 'token',
description: 'ARGENT Token',
totalsupply: 1000000000000,
address: {
policyId: 'c04f4200502a998e9eebafac0291a1f38008de3fe146d136946d8f4b',
name: '415247454e54',
},
decimalPlaces: 0,
categories: [],
},
price: {
volume: {base: 0, quote: 0},
volumeChange: {base: 0, quote: 0},
volumeTotal: {base: 0, quote: 0},
volumeAggregator: {},
price: 0,
askPrice: 0,
bidPrice: 0,
priceChange: {'24h': 0, '7d': 0},
quoteDecimalPlaces: 0,
baseDecimalPlaces: 6,
quoteAddress: {
policyId: 'c04f4200502a998e9eebafac0291a1f38008de3fe146d136946d8f4b',
name: '415247454e54',
},
baseAddress: {policyId: '', name: ''},
price10d: [],
},
},
]
25 changes: 25 additions & 0 deletions packages/openswap/src/token-pairs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {SWAP_API_ENDPOINTS} from './config'
import type {ApiDeps, TokenPairsResponse} from './types'

export async function getTokenPairs(
deps: ApiDeps,
{policyId = '', assetName = ''} = {},
): Promise<TokenPairsResponse> {
const {network, client} = deps
if (network === 'preprod') return []

const apiUrl = SWAP_API_ENDPOINTS[network].getTokenPairs
const response = await client.get<TokenPairsResponse>('', {
baseURL: apiUrl,
params: {
'base-policy-id': policyId,
'base-tokenname': assetName,
},
})

if (response.status !== 200) {
throw new Error('Failed to fetch token pairs', {cause: response.data})
}

return response.data
}
Loading