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

Add Token Search Results #1056

Merged
merged 84 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
81d1f0e
sesrch queries a socket
achowdhry-ripple Aug 22, 2024
c5a7288
websocet successfully pulls from publix xrplmeta node
achowdhry-ripple Aug 22, 2024
0d04d6f
fully working search bar for tokens
achowdhry-ripple Sep 5, 2024
2fe25da
rudimentary nontoken search parsing in results bar
achowdhry-ripple Sep 5, 2024
a7e9bb7
new search results per design
achowdhry-ripple Sep 12, 2024
a0d00fa
scrollable fix
achowdhry-ripple Sep 12, 2024
23ea42f
better icon and linking
achowdhry-ripple Sep 13, 2024
cc3ef20
minor pathves
achowdhry-ripple Sep 13, 2024
3d945b9
fixed uiu cleanups
achowdhry-ripple Sep 16, 2024
7cdadb4
upgraded websocket logic. now reconnects as well
achowdhry-ripple Sep 16, 2024
0477a5e
added commas
achowdhry-ripple Sep 16, 2024
bfbcad6
spacing improvements
achowdhry-ripple Sep 17, 2024
c586a74
real xrp usd price and style fixes
achowdhry-ripple Sep 19, 2024
87e6280
external logo
achowdhry-ripple Sep 19, 2024
1a54c5b
header fixed to top
achowdhry-ripple Sep 19, 2024
a88aaf0
minor style fixes
achowdhry-ripple Sep 20, 2024
7534676
partial code cleanup
achowdhry-ripple Oct 2, 2024
3e43166
separate row component'
achowdhry-ripple Oct 21, 2024
bb22b2c
banner
achowdhry-ripple Oct 24, 2024
35e9787
break down large html code
achowdhry-ripple Oct 25, 2024
9a29c92
translations
achowdhry-ripple Oct 25, 2024
addb988
only render on mainnet env
achowdhry-ripple Oct 29, 2024
5d4165b
external search banner for better behavior
achowdhry-ripple Oct 29, 2024
2aa3e50
closable banner
achowdhry-ripple Oct 29, 2024
cd409db
Merge branch 'staging' into search-results
pdp2121 Oct 29, 2024
fba95ad
fix package merge issue
pdp2121 Oct 29, 2024
74ad246
fix lint
pdp2121 Oct 29, 2024
3e80c7d
small fixes & renames
pdp2121 Oct 29, 2024
3d0b6c2
use existing socket for oracle
pdp2121 Oct 29, 2024
5d2909e
use api instead of websocket to fetch tokens
pdp2121 Oct 29, 2024
6b89972
add interval for usd conversion
pdp2121 Oct 30, 2024
f009f22
Replace renderCurrency with Currency
pdp2121 Oct 30, 2024
8e5bc75
use css for default logo
pdp2121 Oct 30, 2024
9b2a663
remove react-tooltip
pdp2121 Oct 30, 2024
b7006b2
fix lint
pdp2121 Oct 30, 2024
598dcea
fix tests
pdp2121 Oct 30, 2024
42c38b4
use domain link
pdp2121 Nov 1, 2024
33458ad
remove parseDomain
pdp2121 Nov 1, 2024
76eb402
make close banner clickable
pdp2121 Nov 1, 2024
dff1d89
change banner duration back to 10 seconds
pdp2121 Nov 1, 2024
dcf0f97
fix banner
pdp2121 Nov 4, 2024
f658c06
initial server implementation
pdp2121 Nov 4, 2024
da1745b
fix mobile view
pdp2121 Nov 4, 2024
31add9f
only render banner for mainnet
pdp2121 Nov 5, 2024
a43a333
Merge branch 'staging' into search-results
pdp2121 Nov 5, 2024
d69b53a
cache all filtered tokens
achowdhry-ripple Nov 5, 2024
7e48d13
consistent capitalization in search algo, parse currency code on backend
achowdhry-ripple Nov 5, 2024
970db70
website spacing fix
achowdhry-ripple Nov 5, 2024
acb273a
cleanup in looping logic
achowdhry-ripple Nov 5, 2024
b04d3f9
cleanups and comments
achowdhry-ripple Nov 5, 2024
69d20b6
spacing issue on pills fix
achowdhry-ripple Nov 5, 2024
dc3a059
add tests
pdp2121 Nov 5, 2024
6a783e3
Merge branch 'search-results' into search-results-server
achowdhry-ripple Nov 5, 2024
9ee057b
translate banner
achowdhry-ripple Nov 5, 2024
6440723
remove extra whitespace
achowdhry-ripple Nov 5, 2024
196ab39
remove extra import
achowdhry-ripple Nov 5, 2024
dbbf791
lint translation fix
achowdhry-ripple Nov 5, 2024
f57e4b1
search issuer startswith instead of include
achowdhry-ripple Nov 5, 2024
1cc9ce3
dom cleanup and mobile width fix
achowdhry-ripple Nov 5, 2024
2cd9ca9
fix test
pdp2121 Nov 5, 2024
bfd3e8a
use color variables
achowdhry-ripple Nov 5, 2024
a2f99b3
Update src/containers/shared/components/TokenSearchResults/TokenSearc…
achowdhry-ripple Nov 5, 2024
eff6cbb
Update src/containers/shared/components/TokenSearchResults/TokenSearc…
achowdhry-ripple Nov 5, 2024
44ed125
Update src/containers/shared/components/TokenSearchResults/TokenSearc…
achowdhry-ripple Nov 5, 2024
be79f53
remove propTypes
pdp2121 Nov 5, 2024
0e54e10
Merge branch 'search-results' of https://github.com/ripple/explorer i…
pdp2121 Nov 5, 2024
af1c964
review suggestions cleanups
achowdhry-ripple Nov 5, 2024
8f95e07
remove QuickHarness
pdp2121 Nov 5, 2024
99943d6
fix lint
pdp2121 Nov 5, 2024
0c054d2
usequery instead of useeffect
achowdhry-ripple Nov 5, 2024
5036059
usequery fix
achowdhry-ripple Nov 6, 2024
c9fec39
lint
achowdhry-ripple Nov 6, 2024
d258ac7
add protocol
pdp2121 Nov 6, 2024
d1d3769
log url
pdp2121 Nov 6, 2024
3ed1350
remove test log
pdp2121 Nov 6, 2024
2d67c78
fix issue link spacing
achowdhry-ripple Nov 6, 2024
20d6656
fix stale result
pdp2121 Nov 6, 2024
64ef3c8
remove test log
pdp2121 Nov 6, 2024
cd44d9e
usequery for rate
pdp2121 Nov 6, 2024
c75b2dc
fix lint
pdp2121 Nov 6, 2024
e58436e
manage state in usequery
achowdhry-ripple Nov 6, 2024
fa439f6
usequery cleanup
achowdhry-ripple Nov 6, 2024
c25603d
use component
pdp2121 Nov 6, 2024
c94ea47
Merge branch 'search-results' of https://github.com/ripple/explorer i…
pdp2121 Nov 6, 2024
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
6 changes: 5 additions & 1 deletion public/locales/ca-CA/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,9 @@
"asset_class": null,
"trading_pairs": null,
"deleted": null,
"holders": null,
"trustlines": null,
"website": null,
"assets.mpt_tab_title": null,
"assets.no_mpts_message": null,
"transaction_type_name_MPTokenIssuanceCreate": null,
Expand All @@ -553,5 +556,6 @@
"can_escrow": null,
"can_trade": null,
"can_transfer": null,
"can_clawback": null
"can_clawback": null,
"search_results_banner": null
}
8 changes: 6 additions & 2 deletions public/locales/en-US/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"explorer": "Explorer",
"xrpl_org": "XRPL.org",
"github": "GitHub",
"header.search.placeholder": "Search by Address, Ledger or Txn",
"header.search.placeholder": "Search by Token, Address, Ledger or Txn",
achowdhry-ripple marked this conversation as resolved.
Show resolved Hide resolved
"xrp": "XRP",
"xrpl_explorer": "XRPL Explorer",
"ledgers": "Ledgers",
Expand Down Expand Up @@ -540,6 +540,9 @@
"asset_class": "Asset Class",
"trading_pairs": "Trading Pairs",
"deleted": "Deleted",
"holders": "HOLDERS: {{holders}}",
"trustlines": " TRUSTLINES: {{trustlines}}",
"website": "Wesbite",
"mpt_issuance_id": "MPT Issuance ID",
"asset_scale": "Asset Scale",
"metadata": "Metadata",
Expand All @@ -553,5 +556,6 @@
"can_escrow": "Can Escrow",
"can_trade": "Can Trade",
"can_transfer": "Can Transfer",
"can_clawback": "Can Clawback"
"can_clawback": "Can Clawback",
"search_results_banner": "Token search by name and account is now available! Try searching for USD"
}
6 changes: 5 additions & 1 deletion public/locales/es-ES/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,9 @@
"asset_class": null,
"trading_pairs": null,
"deleted": null,
"holders": null,
"trustlines": null,
"website": null,
"assets.mpt_tab_title": null,
"assets.no_mpts_message": null,
"transaction_type_name_MPTokenIssuanceCreate": null,
Expand All @@ -549,5 +552,6 @@
"can_escrow": null,
"can_trade": null,
"can_transfer": null,
"can_clawback": null
"can_clawback": null,
"search_results_banner": null
}
6 changes: 5 additions & 1 deletion public/locales/fr-FR/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,9 @@
"asset_class": null,
"trading_pairs": null,
"deleted": null,
"holders": null,
"trustlines": null,
"website": null,
"assets.mpt_tab_title": null,
"assets.no_mpts_message": null,
"transaction_type_name_MPTokenIssuanceCreate": null,
Expand All @@ -550,5 +553,6 @@
"can_escrow": null,
"can_trade": null,
"can_transfer": null,
"can_clawback": null
"can_clawback": null,
"search_results_banner": null
}
6 changes: 5 additions & 1 deletion public/locales/ja-JP/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,9 @@
"asset_class": null,
"trading_pairs": null,
"deleted": null,
"holders": null,
"trustlines": null,
"website": null,
"assets.mpt_tab_title": null,
"assets.no_mpts_message": null,
"transaction_type_name_MPTokenIssuanceCreate": null,
Expand All @@ -549,5 +552,6 @@
"can_escrow": null,
"can_trade": null,
"can_transfer": null,
"can_clawback": null
"can_clawback": null,
"search_results_banner": null
}
6 changes: 5 additions & 1 deletion public/locales/ko-KR/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,9 @@
"asset_class": null,
"trading_pairs": null,
"deleted": null,
"holders": null,
"trustlines": null,
"website": null,
"assets.mpt_tab_title": null,
"assets.no_mpts_message": null,
"transaction_type_name_MPTokenIssuanceCreate": null,
Expand All @@ -547,5 +550,6 @@
"can_escrow": null,
"can_trade": null,
"can_transfer": null,
"can_clawback": null
"can_clawback": null,
"search_results_banner": null
}
2 changes: 2 additions & 0 deletions server/routes/v1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const api = require('express').Router()
const getTokenDiscovery = require('./tokenDiscovery')
const getHealth = require('./health')
const getCurrentMetrics = require('./currentMetrics')
const getTokensSearch = require('./tokens')

if (process.env.VITE_ENVIRONMENT === 'mainnet') {
api.use('/token/top', getTokenDiscovery)
Expand All @@ -13,6 +14,7 @@ if (process.env.VITE_ENVIRONMENT !== 'custom') {
// these require a single hardcoded rippled node to connect to
api.use('/health', getHealth)
api.use('/metrics', getCurrentMetrics)
api.use('/tokens/search/:query', getTokensSearch)
}

module.exports = api
121 changes: 121 additions & 0 deletions server/routes/v1/tokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
const axios = require('axios')
const log = require('../../lib/logger')({ name: 'tokens search' })

const REFETCH_INTERVAL = 60 * 60 * 1000 // 1 hour
const XRPLMETA_QUERY_LIMIT = 1000
const cachedTokenSearchList = { tokens: [], last_updated: null }

const parseCurrency = (currency) => {
const NON_STANDARD_CODE_LENGTH = 40
const LP_TOKEN_IDENTIFIER = '03'

const hexToString = (hex) => {
let string = ''
for (let i = 0; i < hex.length; i += 2) {
const part = hex.substring(i, i + 2)
const code = parseInt(part, 16)
if (!isNaN(code) && code !== 0) {
string += String.fromCharCode(code)
}
}
return string
}

return currency.length === NON_STANDARD_CODE_LENGTH &&
currency?.substring(0, 2) !== LP_TOKEN_IDENTIFIER
? hexToString(currency)
: currency
}

async function fetchXRPLMetaTokens(offset) {
log.info(`caching tokens from ${process.env.XRPL_META_URL}`)
return axios
.get(
`https://${process.env.XRPL_META_URL}/tokens?trust_level=1&trust_level=2&trust_level=3`,
{
params: {
sort_by: 'holders',
offset,
limit: XRPLMETA_QUERY_LIMIT,
},
},
)
.then((resp) => resp.data)
.catch((e) => log.error(e))
}

async function cacheXRPLMetaTokens() {
let offset = 0
let tokensDataBatch = []
const allTokensFetched = []

tokensDataBatch = await fetchXRPLMetaTokens(0)
const { count } = tokensDataBatch
while (offset < count) {
allTokensFetched.push(...tokensDataBatch.tokens)
offset += XRPLMETA_QUERY_LIMIT
// eslint-disable-next-line no-await-in-loop
tokensDataBatch = await fetchXRPLMetaTokens(offset)
}

cachedTokenSearchList.tokens = allTokensFetched.filter(
(result) =>
result.metrics.trustlines > 50 &&
result.metrics.holders > 50 &&
result.metrics.marketcap > 0 &&
result.metrics.volume_7d > 0,
)
cachedTokenSearchList.last_updated = Date.now()

// nonstandard from XRPLMeta, check for hex codes in currencies and store parsed
cachedTokenSearchList.tokens.map((token) => ({
...token,
currency: parseCurrency(token.currency),
}))
}

function startCaching() {
if (process.env.VITE_ENVIRONMENT !== 'mainnet') {
return
}
cacheXRPLMetaTokens()
setInterval(() => cacheXRPLMetaTokens(), REFETCH_INTERVAL)
}

startCaching()

function queryTokens(tokenList, query) {
const sanitizedQuery = query.toLowerCase()

return tokenList.filter(
(token) =>
token.currency?.toLowerCase().includes(sanitizedQuery) ||
token.meta?.token?.name?.toLowerCase().includes(sanitizedQuery) ||
token.meta?.issuer?.name?.toLowerCase().includes(sanitizedQuery) ||
token.issuer?.toLowerCase().startsWith(sanitizedQuery),
)
}

function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}

module.exports = async (req, res) => {
try {
log.info('getting tokens list for search')
const { query } = req.params
while (cachedTokenSearchList.tokens.length === 0) {
// eslint-disable-next-line no-await-in-loop -- necessary here to wait for cache to be filled
await sleep(1000)
}
const queriedTokens = await queryTokens(cachedTokenSearchList.tokens, query)
return res.status(200).json({
result: 'success',
updated: cachedTokenSearchList.last_updated,
tokens: queriedTokens,
})
} catch (error) {
log.error(error)
return res.status(error.code || 500).json({ message: error.message })
}
}
72 changes: 63 additions & 9 deletions src/containers/Header/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { KeyboardEventHandler, useContext } from 'react'
import {
FC,
KeyboardEventHandler,
useContext,
useEffect,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { XrplClient } from 'xrpl-client'

import {
isValidClassicAddress,
isValidXAddress,
classicAddressToXAddress,
} from 'ripple-address-codec'
import CloseIcon from '../shared/images/close.png'

import { useAnalytics } from '../shared/analytics'
import SocketContext from '../shared/SocketContext'
import {
Expand All @@ -33,6 +40,7 @@ import {
VALIDATOR_ROUTE,
MPT_ROUTE,
} from '../App/routes'
import TokenSearchResults from '../shared/components/TokenSearchResults/TokenSearchResults'

const determineHashType = async (id: string, rippledContext: XrplClient) => {
try {
Expand Down Expand Up @@ -153,6 +161,26 @@ const normalizeAccount = (id: string) => {
return id
}

const SearchBanner: FC<{ setIsBannerVisible: (visible: boolean) => void }> = ({
setIsBannerVisible,
}) => {
const { t } = useTranslation()
return (
<div className="banner-search">
<div className="banner-content">
<div>{t('search_results_banner')}</div>
<button
className="banner-button"
type="button"
onClick={() => setIsBannerVisible(false)}
>
<img src={CloseIcon} alt="close-icon" width={10} height={10} />
</button>
</div>
</div>
)
}

export interface SearchProps {
callback?: Function
}
Expand All @@ -163,6 +191,8 @@ export const Search = ({ callback = () => {} }: SearchProps) => {
const socket = useContext(SocketContext)
const navigate = useNavigate()

const [currentSearchInput, setCurrentSearchInput] = useState('')

const handleSearch = async (id: string) => {
const strippedId = id.replace(/^["']|["']$/g, '')
const route = await getRoute(strippedId, socket)
Expand All @@ -178,16 +208,40 @@ export const Search = ({ callback = () => {} }: SearchProps) => {
const onKeyDown: KeyboardEventHandler<HTMLInputElement> = (event) => {
if (event.key === 'Enter') {
handleSearch(event.currentTarget?.value?.trim())
setCurrentSearchInput('')
}
}

const [isBannerVisible, setIsBannerVisible] = useState(true)

useEffect(() => {
const timeoutId = setTimeout(() => {
setIsBannerVisible(false)
}, 10000) // Disappear after 10 seconds

return () => clearTimeout(timeoutId)
}, [])

return (
<div className="search">
<input
type="text"
placeholder={t('header.search.placeholder')}
onKeyDown={onKeyDown}
/>
</div>
<>
{process.env.VITE_ENVIRONMENT === 'mainnet' && isBannerVisible && (
<SearchBanner setIsBannerVisible={setIsBannerVisible} />
)}
<div className="search">
<input
type="text"
placeholder={t('header.search.placeholder')}
onKeyDown={onKeyDown}
value={currentSearchInput}
onChange={(e) => setCurrentSearchInput(e.target.value)}
/>
{process.env.VITE_ENVIRONMENT === 'mainnet' && (
<TokenSearchResults
setCurrentSearchInput={setCurrentSearchInput}
currentSearchValue={currentSearchInput}
/>
)}
</div>
</>
)
}
Loading
Loading