From a33ca9fbbaf3e6429e7e16a5e3a241b5cc92e707 Mon Sep 17 00:00:00 2001 From: Jonathan Tzeng Date: Thu, 20 Aug 2026 22:02:35 -0700 Subject: [PATCH] Search Manage Tokens by contract address The Manage Tokens search only matched a token's currency code and display name, so pasting a contract address returned nothing even though the Assets search finds the same token. Match the token's networkLocation values too, the same way the wallet list and create-wallet list searches already do. --- CHANGELOG.md | 1 + eslint.config.mjs | 2 - src/__tests__/walletSearch.test.ts | 83 ++++++++++++++++++++- src/components/scenes/ManageTokensScene.tsx | 15 ++-- src/util/tokenSearch.ts | 35 +++++++++ 5 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 src/util/tokenSearch.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cfba92d8c57..174070fe4bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - fixed: Improve the unstake error experience by replacing the popup alert and generic "unknown error occurred" with the real error in the scene's error field, and showing a clear message when the wallet lacks the balance to cover the unstaking network fee. - fixed: Tapping Max on the Sell scene no longer briefly shows the entered fiat amount in the crypto field while the max is being calculated. - fixed: An info card no longer disappears into an empty gap when the carousel's card list shrinks. A card's position comes entirely from an animated transform keyed on its index, and that transform is not re-applied when a surviving card shifts slots, so dropping a card left the ones after it parked a full card-width off-screen. The carousel now remounts a card whose slot changes. Reproduces wherever the list shrinks after mount - most visibly when a `noBalance` card is filtered out as balances finish loading. +- fixed: Manage Tokens search now finds a token by its contract address, matching the Assets search. ## 4.50.2 (2026-08-06) diff --git a/eslint.config.mjs b/eslint.config.mjs index e817da3e660..5632b878336 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -296,8 +296,6 @@ export default [ 'src/components/scenes/Loans/LoanManageScene.tsx', 'src/components/scenes/Loans/LoanStatusScene.tsx', - 'src/components/scenes/ManageTokensScene.tsx', - 'src/components/scenes/NotificationCenterScene.tsx', 'src/components/scenes/NotificationScene.tsx', diff --git a/src/__tests__/walletSearch.test.ts b/src/__tests__/walletSearch.test.ts index 9df5582cd4c..a93a3c123c1 100644 --- a/src/__tests__/walletSearch.test.ts +++ b/src/__tests__/walletSearch.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from '@jest/globals' -import type { EdgeToken } from 'edge-core-js' +import type { EdgeToken, EdgeTokenMap } from 'edge-core-js' import { searchWalletList } from '../components/services/SortedWalletList' import { filterWalletCreateItemListBySearchText } from '../selectors/getCreateWalletList' @@ -14,6 +14,7 @@ import { testTetherToken, testWstethToken } from '../util/fake/fakeSearchTestData' +import { searchTokenIds } from '../util/tokenSearch' // ----------------------------------------------------------------------------- // searchWalletList Tests @@ -503,3 +504,83 @@ describe('Regression: Original search issues', () => { }) }) }) + +// ----------------------------------------------------------------------------- +// searchTokenIds Tests +// ----------------------------------------------------------------------------- + +describe('searchTokenIds', () => { + // MOG uses a checksummed contract address, as the token lists do: + const mogToken: EdgeToken = { + currencyCode: 'MOG', + displayName: 'Mog Coin', + denominations: [{ name: 'MOG', multiplier: '100000000000000000' }], + networkLocation: { + contractAddress: '0xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a' + } + } + const mogTokenId = 'aaee1a9723aadb7afa2810263653a34ba2c21c7a' + const tetherTokenId = 'dac17f958d2ee523a2206206994597c13d831ec7' + const wstethTokenId = '7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0' + + const allTokens: EdgeTokenMap = { + [mogTokenId]: mogToken, + [tetherTokenId]: testTetherToken, + [wstethTokenId]: testWstethToken + } + const tokenIds = [mogTokenId, tetherTokenId, wstethTokenId] + + test('returns all tokenIds when search is empty', () => { + expect(searchTokenIds(allTokens, tokenIds, '')).toEqual(tokenIds) + }) + + test('matches currency code', () => { + expect(searchTokenIds(allTokens, tokenIds, 'MOG')).toEqual([mogTokenId]) + }) + + test('matches display name', () => { + expect(searchTokenIds(allTokens, tokenIds, 'Tether')).toEqual([ + tetherTokenId + ]) + }) + + test('matches a full contract address', () => { + expect( + searchTokenIds( + allTokens, + tokenIds, + '0xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a' + ) + ).toEqual([mogTokenId]) + }) + + test('matches a full contract address in any case', () => { + expect( + searchTokenIds( + allTokens, + tokenIds, + '0xaaee1a9723aadb7afa2810263653a34ba2c21c7a' + ) + ).toEqual([mogTokenId]) + }) + + test('matches a partial contract address', () => { + expect(searchTokenIds(allTokens, tokenIds, '0xdac17f')).toEqual([ + tetherTokenId + ]) + }) + + test('returns nothing for an unknown contract address', () => { + expect( + searchTokenIds( + allTokens, + tokenIds, + '0x0000000000000000000000000000000000000000' + ) + ).toEqual([]) + }) + + test('skips tokenIds missing from the token map', () => { + expect(searchTokenIds(allTokens, ['missing-token-id'], 'mog')).toEqual([]) + }) +}) diff --git a/src/components/scenes/ManageTokensScene.tsx b/src/components/scenes/ManageTokensScene.tsx index 02512622e7e..eb00d03416f 100644 --- a/src/components/scenes/ManageTokensScene.tsx +++ b/src/components/scenes/ManageTokensScene.tsx @@ -12,7 +12,7 @@ import type { EdgeAppSceneProps } from '../../types/routerTypes' import type { FlatListItem } from '../../types/types' import { getWalletName } from '../../util/CurrencyWalletHelpers' import { logActivity } from '../../util/logger' -import { normalizeForSearch } from '../../util/utils' +import { searchTokenIds } from '../../util/tokenSearch' import { ButtonsView } from '../buttons/ButtonsView' import { SceneWrapper } from '../common/SceneWrapper' import { withWallet } from '../hoc/withWallet' @@ -148,15 +148,10 @@ const ManageTokensSceneComponent: React.FC = props => { }, [allTokens, sortingBaselineSet]) // Filter the list of tokens based on the search term: - const filteredTokenIds = React.useMemo(() => { - const target = normalizeForSearch(searchValue) - return sortedTokenIds.filter(tokenId => { - const token = allTokens[tokenId] - const currencyCode = normalizeForSearch(token.currencyCode) - const displayName = normalizeForSearch(token.displayName) - return currencyCode.includes(target) || displayName.includes(target) - }) - }, [allTokens, searchValue, sortedTokenIds]) + const filteredTokenIds = React.useMemo( + () => searchTokenIds(allTokens, sortedTokenIds, searchValue), + [allTokens, searchValue, sortedTokenIds] + ) // Split the list of tokens based on if there were auto-detected tokens given const autoDetectedTokenIds = React.useMemo( diff --git a/src/util/tokenSearch.ts b/src/util/tokenSearch.ts new file mode 100644 index 00000000000..cf70b80931d --- /dev/null +++ b/src/util/tokenSearch.ts @@ -0,0 +1,35 @@ +import type { EdgeTokenMap } from 'edge-core-js' + +import { normalizeForSearch } from './utils' + +/** + * Filters a list of tokenIds using a search string. + * + * Asset identification fields (currency code and display name) match anywhere + * in the field, while network location values (ie. contract address) match the + * same way the wallet list and create-wallet list searches do. + */ +export function searchTokenIds( + allTokens: EdgeTokenMap, + tokenIds: string[], + searchText: string +): string[] { + const target = normalizeForSearch(searchText) + if (target === '') return tokenIds + + return tokenIds.filter(tokenId => { + const token = allTokens[tokenId] + if (token == null) return false + + const { currencyCode, displayName, networkLocation } = token + if (normalizeForSearch(currencyCode).includes(target)) return true + if (normalizeForSearch(displayName).includes(target)) return true + + // Search networkLocation values ie. contractAddress: + if (networkLocation == null) return false + return Object.values(networkLocation).some( + value => + typeof value === 'string' && normalizeForSearch(value).includes(target) + ) + }) +}