Repository navigation
Add MPT ticker/issuer search alongside IOU search #1340
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
Changes from all commits
2d83fc6
ad5fb7b
c6885b9
3e29347
d6c3d57
9c6be89
150ab5e
fd67cd4
49858d9
d33983b
4514ba9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,12 @@ const axios = require('axios') | |
| const log = require('../../lib/logger')({ name: 'tokens search' }) | ||
|
|
||
| const REFETCH_INTERVAL = 10 * 60 * 1000 // 10 minutes | ||
| const cachedTokenList = { tokens: [], last_updated: null, metrics: null } | ||
| const cachedTokenList = { | ||
| tokens: [], | ||
| searchTokens: [], | ||
| last_updated: null, | ||
| metrics: null, | ||
| } | ||
|
|
||
| const parseCurrency = (currency) => { | ||
| const NON_STANDARD_CODE_LENGTH = 40 | ||
|
|
@@ -82,24 +87,78 @@ async function fetchTokens() { | |
| }) | ||
| } | ||
|
|
||
| // MPTs aren't tradeable on the DEX yet, so they have no price/market cap to | ||
| // rank on or filter by. Zero-holder issuances are mostly test/abandoned | ||
| // tokens, so only ones with at least one holder are made searchable. | ||
| const MPT_MIN_HOLDERS = 0 | ||
|
|
||
| function mapMPT(mpt) { | ||
| return { | ||
| token_type: 'MPT', | ||
| mpt_issuance_id: mpt.mpt_issuance_id, | ||
| currency: mpt.mpt_issuance_id, | ||
| issuer_account: mpt.issuer, | ||
| issuer_name: mpt.meta?.token?.issuer_name ?? mpt.meta?.issuer?.name, | ||
| issuer_domain: mpt.meta?.issuer?.domain, | ||
| // `name` stays the short ticker for display (matches the IOU convention | ||
| // of a short code shown next to the currency), but the fuller product | ||
| // name (e.g. "Car Parts" for a token ticked "SCPO") is kept separately | ||
| // so it's still searchable even though it's never the display name. | ||
| name: mpt.meta?.token?.ticker ?? mpt.meta?.token?.name, | ||
| full_name: mpt.meta?.token?.name, | ||
| icon: mpt.meta?.token?.icon, | ||
| holders: mpt.metrics?.holders, | ||
| } | ||
| } | ||
|
|
||
| async function fetchMPTs() { | ||
| const url = `https://${process.env.XRPL_META_URL}/v2/tokens/mpt?limit=1000` | ||
| log.info(`Fetching MPTs from: ${url}`) | ||
|
|
||
| return axios | ||
| .get(url, { timeout: 30000 }) | ||
|
Comment on lines
+118
to
+119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Risk: Affected versions of axios are vulnerable to Inefficient Regular Expression Complexity. The axios Node.js HTTP adapter parses data: URLs in GET requests with a regex (lib/helpers/fromDataURI.js) that suffers from catastrophic backtracking; a long malformed data: URL made of slashes without a comma blocks the event loop for minutes, causing a denial of service. Fix: Upgrade this library to at least version 1.20.0 at explorer/package-lock.json:7447.
✨ Fixed in commit 4514ba9 ✨ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thx u🫶
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Axios upgrades will go in a different PR
Comment on lines
+118
to
+119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Risk: Affected versions of axios are vulnerable to Inefficient Regular Expression Complexity / Uncontrolled Resource Consumption. Axios is vulnerable to a Regular Expression Denial of Service (ReDoS) in the internal Manual Review Advice: A vulnerability from this advisory is reachable if you use Axios in Node.js with the default Fix: Upgrade this library to at least version 1.20.0 at explorer/package-lock.json:7447.
✨ Fixed in commit 4514ba9 ✨
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Axios upgrades will go in a different PR
Comment on lines
+118
to
+119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Risk: Affected versions of axios are vulnerable to Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') / Unintended Proxy or Intermediary ('Confused Deputy'). The axios Node.js HTTP adapter does not set a safe own value for Fix: Upgrade this library to at least version 1.20.0 at explorer/package-lock.json:7447.
🎉 Fixed in commit 4514ba9 🎉
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Axios upgrades will go in a different PR |
||
| .then((resp) => { | ||
| const mpts = resp.data?.tokens || [] | ||
| log.info(`Successfully fetched MPTs, count: ${mpts.length}`) | ||
| return mpts | ||
| .filter((mpt) => (mpt.metrics?.holders ?? 0) > MPT_MIN_HOLDERS) | ||
| .map(mapMPT) | ||
| }) | ||
| .catch((e) => { | ||
| log.error(`Failed to fetch MPTs from ${url}:`, { message: e.message }) | ||
| return cachedTokenList.searchTokens.filter((t) => t.token_type === 'MPT') | ||
| }) | ||
| } | ||
|
|
||
| async function cacheTokens() { | ||
| const losTokens = await fetchTokens() | ||
| const [losTokens, mpts] = await Promise.all([fetchTokens(), fetchMPTs()]) | ||
|
|
||
| if (losTokens.tokens) { | ||
| log.info(`Fetched ${losTokens.tokens.length} tokens from LOS...`) | ||
|
|
||
| cachedTokenList.tokens = losTokens.tokens.sort( | ||
| (a, b) => Number(b.holders ?? 0) - Number(a.holders ?? 0), | ||
| log.info( | ||
| `Fetched ${losTokens.tokens.length} tokens from LOS, ${mpts.length} MPTs from XRPL Meta...`, | ||
| ) | ||
|
|
||
| cachedTokenList.last_updated = Date.now() | ||
|
|
||
| // nonstandard from XRPLMeta, check for hex codes in currencies and store parsed | ||
| cachedTokenList.tokens = cachedTokenList.tokens.map((token) => ({ | ||
| const iouTokens = losTokens.tokens | ||
| .sort((a, b) => Number(b.holders ?? 0) - Number(a.holders ?? 0)) | ||
| .map((token) => ({ | ||
| ...token, | ||
| parsedCurrency: parseCurrency(token.currency), | ||
| })) | ||
| const mptTokens = mpts.map((token) => ({ | ||
| ...token, | ||
| parsedCurrency: parseCurrency(token.currency), | ||
| })) | ||
|
|
||
| // The Token Ranking page (getAllTokens) shows IOUs only — MPTs aren't | ||
| // tradeable yet, so mixing them into ranking/metrics would be misleading. | ||
| cachedTokenList.tokens = iouTokens | ||
| cachedTokenList.searchTokens = [...iouTokens, ...mptTokens].sort( | ||
| (a, b) => Number(b.holders ?? 0) - Number(a.holders ?? 0), | ||
| ) | ||
|
|
||
| cachedTokenList.last_updated = Date.now() | ||
|
|
||
| // Calculate and cache metrics | ||
| cachedTokenList.metrics = calculateMetrics(cachedTokenList.tokens) | ||
| log.info(`Cached metrics for ${cachedTokenList.metrics.count} tokens`) | ||
|
|
@@ -137,6 +196,9 @@ function queryTokens(tokenList, query) { | |
| ?.toLowerCase() | ||
| .includes(sanitizedQuery) | ||
| const nameMatch = token.name?.toLowerCase().includes(sanitizedQuery) | ||
| const fullNameMatch = token.full_name | ||
| ?.toLowerCase() | ||
| .includes(sanitizedQuery) | ||
| const issuerNameMatch = token.issuer_name | ||
| ?.toLowerCase() | ||
| .includes(sanitizedQuery) | ||
|
|
@@ -148,6 +210,7 @@ function queryTokens(tokenList, query) { | |
| currencyMatch || | ||
| parsedCurrencyMatch || | ||
| nameMatch || | ||
| fullNameMatch || | ||
| issuerNameMatch || | ||
| issuerAccountStartsMatch | ||
| ) | ||
|
|
@@ -167,12 +230,12 @@ const getTokensSearch = async (req, res) => { | |
| log.info('getting tokens list for search') | ||
| const { query } = req.params | ||
| let timeoutLimit = 10 | ||
| while (cachedTokenList.tokens.length === 0 && timeoutLimit > 0) { | ||
| while (cachedTokenList.searchTokens.length === 0 && timeoutLimit > 0) { | ||
| // eslint-disable-next-line no-await-in-loop -- necessary here to wait for cache to be filled | ||
| await sleep(1000) | ||
| timeoutLimit -= 1 | ||
| } | ||
| const queriedTokens = await queryTokens(cachedTokenList.tokens, query) | ||
| const queriedTokens = await queryTokens(cachedTokenList.searchTokens, query) | ||
| return res.status(200).json({ | ||
| result: 'success', | ||
| updated: cachedTokenList.last_updated, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.