Skip to content

bug in the token select on /send page #499

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

Merged
Merged
129 changes: 60 additions & 69 deletions components/UI/TokenSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,50 @@ import { IoSearch } from 'react-icons/io5'
import { IoMdClose } from 'react-icons/io'
import { IoChevronDown } from 'react-icons/io5'
import axios from 'axios'
import {
avatarServer,
nativeCurrency,
isNativeCurrency,
nativeCurrenciesImages,
useWidth,
setTabParams
} from '../../utils'
import { niceCurrency, shortAddress, shortNiceNumber, amountFormat } from '../../utils/format'
import { nativeCurrency, nativeCurrenciesImages, useWidth, setTabParams, tokenImageSrc } from '../../utils'
import { niceCurrency, shortAddress, shortNiceNumber } from '../../utils/format'
import RadioOptions from './RadioOptions'
import { useRouter } from 'next/router'

const limit = 20

// Helper function to fetch and process trustlines for a destination address
const fetchTrustlinesForDestination = async (destinationAddress, searchQuery = '') => {
const response = await axios(`v2/objects/${destinationAddress}?limit=1000&type=state`)
const objects = response.data?.objects || []
const response = await axios(`v2/address/${destinationAddress}/acceptedTokens?limit=${limit}`)
const tokens = response.data?.tokens || []

// Filter RippleState objects to get trustlines where destination can hold tokens
const trustlines = objects.filter((obj) => {
if (parseFloat(obj.LowLimit.value) <= 0 && parseFloat(obj.HighLimit.value) <= 0) return false
// Trim the search query to handle whitespace
const trimmedQuery = searchQuery.trim()

const trustlines = tokens.filter((token) => {
// If search query is provided, filter by it
if (searchQuery) {
const currency = obj.Balance.currency
const issuerDetails =
obj.HighLimit.issuer === destinationAddress ? obj.LowLimit.issuerDetails : obj.HighLimit.issuerDetails || {}
const serviceOrUsername = issuerDetails.service || issuerDetails.username || ''
const issuer = obj.HighLimit.issuer === destinationAddress ? obj.LowLimit.issuer : obj.HighLimit.issuer || ''

const searchLower = searchQuery.toLowerCase()
if (trimmedQuery) {
const currency = token.currency
const issuerDetails = token.issuerDetails || {}
const service = issuerDetails.service || ''
const username = issuerDetails.username || ''
const issuer = token.issuer || ''

const searchLower = trimmedQuery.toLowerCase()
return (
currency.toLowerCase().includes(searchLower) ||
serviceOrUsername.toLowerCase().includes(searchLower) ||
service.toLowerCase().includes(searchLower) ||
username.toLowerCase().includes(searchLower) ||
issuer.toLowerCase().includes(searchLower)
)
}

return true
})

// Convert trustlines to token format
return trustlines.map((tl) => ({
currency: tl.Balance.currency,
issuer: tl.HighLimit.issuer === destinationAddress ? tl.LowLimit.issuer : tl.HighLimit.issuer,
issuerDetails: tl.HighLimit.issuer === destinationAddress ? tl.LowLimit.issuerDetails : tl.HighLimit.issuerDetails,
limit: Math.max(parseFloat(tl.LowLimit.value), parseFloat(tl.HighLimit.value)),
balance: tl.Balance.value
}))
return trustlines
}

// Helper function to add native currency to tokens array if needed
const addNativeCurrencyIfNeeded = (tokens, excludeNative, searchQuery = '') => {
if (excludeNative) return tokens

const shouldAddNative = !searchQuery || searchQuery.toUpperCase() === nativeCurrency.toUpperCase()
const trimmedQuery = searchQuery.trim()
const shouldAddNative = !trimmedQuery || trimmedQuery.toUpperCase() === nativeCurrency.toUpperCase()
if (shouldAddNative) {
tokens.unshift({ currency: nativeCurrency, limit: null })
}
Expand All @@ -85,6 +72,10 @@ export default function TokenSelector({
const [isLoading, setIsLoading] = useState(false)
const [searchTimeout, setSearchTimeout] = useState(null)

// Cache for search results to prevent unnecessary reloads
const [lastSearchQuery, setLastSearchQuery] = useState('')
const [cachedSearchResults, setCachedSearchResults] = useState([])

// control radio selection: 'all' | 'single'
const [filterMode, setFilterMode] = useState(() => (value?.currency ? 'single' : 'all'))

Expand Down Expand Up @@ -120,6 +111,8 @@ export default function TokenSelector({
useEffect(() => {
setSearchResults([])
setSearchQuery('')
setLastSearchQuery('')
setCachedSearchResults([])
}, [destinationAddress])

// Handle search with debounce
Expand All @@ -133,23 +126,11 @@ export default function TokenSelector({
}

const timeout = setTimeout(async () => {
if (!searchQuery) {
// Only apply the early return logic when there's no destination address
// When destination address is provided, we always want to fetch fresh data
if (!destinationAddress) {
// do not reload default token list if it's already loaded
// when searched for native currency, we also add the native currency on top,
// so check that it's not that case before canceling the search
if (
isNativeCurrency(searchResults[0]) &&
!niceCurrency(searchResults[1]?.currency)?.toLowerCase().startsWith(nativeCurrency.toLowerCase())
)
return
} else {
// For destination address case, check if we already have results loaded
if (searchResults.length > 0) {
return
}
if (!searchQuery.trim()) {
// Check if we have cached results for empty search query
if (lastSearchQuery === '' && cachedSearchResults.length > 0) {
setSearchResults(cachedSearchResults)
return
}

setIsLoading(true)
Expand All @@ -159,51 +140,77 @@ export default function TokenSelector({
if (destinationAddress) {
// Fetch tokens that destination can hold based on trustlines
tokens = await fetchTrustlinesForDestination(destinationAddress)
tokens = addNativeCurrencyIfNeeded(tokens, excludeNative)
} else {
// Fallback to original behavior if no destination address
const response = await axios('v2/trustlines/tokens?limit=' + limit + '&currencyDetails=true')
tokens = response.data?.tokens || []
if (!excludeNative) {
setSearchResults([{ currency: nativeCurrency }, ...tokens])
const defaultTokens = [{ currency: nativeCurrency }, ...tokens]
setSearchResults(defaultTokens)
// Cache the default token list
setLastSearchQuery('')
setCachedSearchResults(defaultTokens)
} else {
setSearchResults(tokens)
// Cache the default token list
setLastSearchQuery('')
setCachedSearchResults(tokens)
}
setIsLoading(false)
return
}

setSearchResults(tokens)
// Cache the default token list for destination address case
setLastSearchQuery('')
setCachedSearchResults(tokens)
} catch (error) {
console.error('Error loading tokens:', error)
if (excludeNative) {
setSearchResults([])
setLastSearchQuery('')
setCachedSearchResults([])
} else {
setSearchResults([{ currency: nativeCurrency }])
setLastSearchQuery('')
setCachedSearchResults([{ currency: nativeCurrency }])
}
} finally {
setIsLoading(false)
}
return
}

// Check if we have cached results for this search query
if (lastSearchQuery === searchQuery) {
setSearchResults(cachedSearchResults)
return
}

setIsLoading(true)
try {
if (destinationAddress) {
// For destination-specific search, filter the existing trustlines
const tokens = await fetchTrustlinesForDestination(destinationAddress, searchQuery)
const tokensWithNative = addNativeCurrencyIfNeeded(tokens, excludeNative, searchQuery)
setSearchResults(tokensWithNative)
// Cache the results
setLastSearchQuery(searchQuery)
setCachedSearchResults(tokensWithNative)
} else {
// Fallback to original search behavior
const response = await axios(`v2/trustlines/tokens/search/${searchQuery}?limit=${limit}&currencyDetails=true`)
const tokens = response.data?.tokens || []
const tokensWithNative = addNativeCurrencyIfNeeded(tokens, excludeNative, searchQuery)
setSearchResults(tokensWithNative)
// Cache the results
setLastSearchQuery(searchQuery)
setCachedSearchResults(tokensWithNative)
}
} catch (error) {
console.error('Error searching tokens:', error)
setSearchResults([])
setCachedSearchResults([])
} finally {
setIsLoading(false)
}
Expand All @@ -222,12 +229,11 @@ export default function TokenSelector({
const handleSelect = (token) => {
onChange(token)
setIsOpen(false)
setSearchQuery('')
}

// Helper to get icon url if available
const getTokenIcon = (token) => {
let imageUrl = avatarServer + token.issuer
let imageUrl = tokenImageSrc(token)
if (!token.issuer) {
imageUrl = nativeCurrenciesImages[nativeCurrency]
}
Expand All @@ -250,20 +256,6 @@ export default function TokenSelector({
return niceCurrency(token.currency)
}

// Helper to get token limit display
const getTokenLimitDisplay = (token) => {
if (!token.limit || token.currency === nativeCurrency) return null

return (
<div className="token-selector-modal-item-limit">
<span className="token-selector-modal-item-limit-label">Max:</span>
<span className="token-selector-modal-item-limit-value">
{amountFormat({ value: token.limit, currency: token.currency, issuer: token.issuer }, { short: true })}
</span>
</div>
)
}

return (
<>
{allOrOne && (
Expand Down Expand Up @@ -373,7 +365,6 @@ export default function TokenSelector({
)}
</span>
{width > 1100 ? <span>{token.issuer}</span> : <span>{shortAddress(token.issuer)}</span>}
{getTokenLimitDisplay(token)}
</div>
</div>
</div>
Expand Down
49 changes: 1 addition & 48 deletions pages/services/send.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,53 +99,6 @@ export default function Send({
setSelectedToken(token)
}

// Helper to get maximum amount that can be sent for the selected token
const getMaxAmount = () => {
if (!selectedToken || selectedToken.currency === nativeCurrency) return null
return selectedToken.limit
}

// Helper to format max amount display
const getMaxAmountDisplay = () => {
const maxAmount = getMaxAmount()
if (!maxAmount) return null

return (
<>
<span className="max-amount-display">
(<span className="max-amount-label">Dest. can accept max</span>{' '}
<span className="max-amount-value">
{amountFormat(
{ value: maxAmount, currency: selectedToken.currency, issuer: selectedToken.issuer },
{ short: true }
)}
</span>
)
</span>
<style jsx>{`
.max-amount-display {
align-items: center;
margin-top: 4px;
font-size: 12px;

.max-amount-label {
color: #6b7280;
font-weight: 500;
.dark & {
color: #9ca3af;
}
}

.max-amount-value {
color: var(--accent-link);
font-weight: 500;
}
}
`}</style>
</>
)
}

// Fetch network info for reserve amounts only when account is not activated
useEffect(() => {
const fetchNetworkInfo = async () => {
Expand Down Expand Up @@ -532,7 +485,7 @@ export default function Send({
<FormInput
title={
<>
{t('table.amount')} {getMaxAmountDisplay()}
{t('table.amount')}
</>
}
placeholder="Enter amount"
Expand Down