Skip to content
8 changes: 8 additions & 0 deletions src/demo/partners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ export default {
type: 'fiat',
color: '#7214F5'
},
nexchange: {
type: 'swap',
color: '#1D31B6'
},
paybis: {
type: 'fiat',
color: '#FFB400'
Expand Down Expand Up @@ -152,5 +156,9 @@ export default {
xanpool: {
type: 'fiat',
color: '#46228B'
},
xgram: {
type: 'swap',
color: '#0FA7B1'
}
} as const
5 changes: 3 additions & 2 deletions src/indexApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { analyticsRouter } from './routes/v1/analytics'
import { checkTxsRouter } from './routes/v1/checkTxs'
import { getAppIdRouter } from './routes/v1/getAppId'
import { getPluginIdsRouter } from './routes/v1/getPluginIds'
import { getTxInfoRouter } from './routes/v1/getTxInfo'
// import { getTxInfoRouter } from './routes/v1/getTxInfo'
import { HttpError } from './util/httpErrors'

export const nanoDb = nano(config.couchDbFullpath)
Expand All @@ -28,7 +28,8 @@ async function main(): Promise<void> {
app.use('/v1/checkTxs/', checkTxsRouter)
app.use('/v1/getAppId/', getAppIdRouter)
app.use('/v1/getPluginIds/', getPluginIdsRouter)
app.use('/v1/getTxInfo/', getTxInfoRouter)
// Disabled: not private enough and is scrapable.
// app.use('/v1/getTxInfo/', getTxInfoRouter)

// Error router
app.use(function(err, _req, res, _next) {
Expand Down
9 changes: 8 additions & 1 deletion src/partners/banxa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ const BANXA_HISTORICAL_COINS: Record<string, CachedAssetInfo> = {
'RLUSD-XRP': {
contractAddress: 'rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De',
pluginId: 'ripple'
}
},
// ZEC delisted from Banxa v2 catalog
'ZEC-ZEC': { contractAddress: null, pluginId: 'zcash' }
}

/**
Expand Down Expand Up @@ -607,10 +609,15 @@ function getFiatPaymentType(tx: BanxaTx): FiatPaymentType {
case 'WorldPay ApplePay':
case 'Primer Apple Pay':
return 'applepay'
case 'Primer Paypal Pay':
return 'paypal'
case 'WorldPay GooglePay':
case 'Primer Google Pay':
return 'googlepay'
case 'iDEAL Transfer':
return 'ideal'
case 'KLARNA Checkout':
return 'klarna'
case 'ZeroHash ACH Sell':
case 'Fortress/Plaid ACH':
return 'ach'
Expand Down
39 changes: 19 additions & 20 deletions src/partners/changenow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,12 @@ async function loadCurrencyCache(
}

try {
// The exchange/currencies endpoint doesn't require authentication
const url = 'https://api.changenow.io/v2/exchange/currencies?active=true'
// The exchange/currencies endpoint doesn't require authentication.
// Fetch the full list (omit `active=true`): historical transactions can
// reference currencies that ChangeNow has since deactivated/delisted (e.g.
// DASH). Filtering to active-only drops those entries, causing a cache miss
// and a fail-closed halt on otherwise-valid historical transactions.
const url = 'https://api.changenow.io/v2/exchange/currencies'
const response = await retryFetch(url, {
method: 'GET'
})
Expand Down Expand Up @@ -376,24 +380,19 @@ function getAssetInfo(network: string, currencyCode: string): EdgeAssetInfo {
)
}

try {
const tokenId = createTokenId(
tokenType,
currencyCode.toUpperCase(),
contractAddress
)
return {
chainPluginId,
evmChainId,
tokenId
}
} catch (e) {
// If tokenId creation fails, treat as native (no log available in this sync function)
return {
chainPluginId,
evmChainId,
tokenId: null
}
// Let createTokenId throw if the contract address cannot be converted: a
// token must never be silently downgraded to a native (tokenId: null)
// mapping, which would price it with the chain's gas-token rate and
// overcount volume whenever the token is worth less than the gas token.
const tokenId = createTokenId(
tokenType,
currencyCode.toUpperCase(),
contractAddress
)
return {
chainPluginId,
evmChainId,
tokenId
}
}

Expand Down
69 changes: 67 additions & 2 deletions src/partners/letsexchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,68 @@ const LETSEXCHANGE_NETWORK_TO_PLUGIN_ID: Record<string, string> = {
ZKSYNC: 'zksync'
}

// When the API omits network fields, infer native chain from currency code.
// Only unambiguous 1:1 native-ticker cases (not USDT, USDC, BNB, etc.).
const LETSEXCHANGE_CURRENCY_TO_DEFAULT_NETWORK: Record<string, string> = {
ADA: 'ADA',
ALGO: 'ALGO',
ARRR: 'ARRR',
ATOM: 'ATOM',
AVAX: 'AVAXC',
BCH: 'BCH',
BSV: 'BSV',
BTC: 'BTC',
BTG: 'BTG',
CELO: 'CELO',
COREUM: 'COREUM',
DASH: 'DASH',
DGB: 'DGB',
DOGE: 'DOGE',
DOT: 'DOT',
EOS: 'EOS',
ETC: 'ETC',
ETH: 'ETH',
ETHW: 'ETHW',
FIL: 'FIL',
FIO: 'FIO',
FIRO: 'FIRO',
FTM: 'FTM',
GRS: 'GRS',
HBAR: 'HBAR',
LTC: 'LTC',
MATIC: 'MATIC',
PIVX: 'PIVX',
POL: 'POL',
PLS: 'PLS',
QTUM: 'QTUM',
RUNE: 'RUNE',
RVN: 'RVN',
SOL: 'SOL',
SONIC: 'SONIC',
SUI: 'SUI',
TLOS: 'TLOS',
TON: 'TON',
TRX: 'TRX',
XEC: 'XEC',
XLM: 'XLM',
XMR: 'XMR',
XRP: 'XRP',
XTZ: 'XTZ',
ZANO: 'ZANO',
ZEC: 'ZEC'
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ambiguous native network inference

Medium Severity

LETSEXCHANGE_CURRENCY_TO_DEFAULT_NETWORK claims only unambiguous 1:1 native tickers, but includes multi-chain symbols such as ETH, MATIC, FTM, and AVAX. When LetsExchange omits network on a post-cutoff tx, those codes are forced onto a single chain, so L2/native or ERC-20 variants can get the wrong chainPluginId and be priced incorrectly.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit cad25fb. Configure here.


function resolveNetworkCode(
network: string | null,
currencyCode: string,
isoDate: string
): string | null {
if (network != null) return network
if (isoDate < NETWORK_FIELDS_AVAILABLE_DATE) return null
const currencyUpper = currencyCode.toUpperCase()
return LETSEXCHANGE_CURRENCY_TO_DEFAULT_NETWORK[currencyUpper] ?? null
}

// Native token placeholder addresses that should be treated as null (native coin)
// All values should be lowercase for case-insensitive matching
const NATIVE_TOKEN_ADDRESSES = new Set([
Expand Down Expand Up @@ -299,13 +361,16 @@ function getAssetInfo(
contractAddress: string | null,
isoDate: string
): AssetInfo | undefined {
if (initialNetwork == null) {
const network = resolveNetworkCode(initialNetwork, currencyCode, isoDate)
if (network == null) {
// Transactions predating the network fields cannot be backfilled, so they
// are allowed through unenriched. Anything newer must resolve a network or
// fail closed rather than persist missing chain and token data.
if (isoDate < NETWORK_FIELDS_AVAILABLE_DATE) {
return undefined
}
throw new Error(`Missing network for currency ${currencyCode}`)
}
const network = initialNetwork

const networkUpper = network.toUpperCase()
const chainPluginId = LETSEXCHANGE_NETWORK_TO_PLUGIN_ID[networkUpper]
Expand Down
Loading
Loading