Conversation
8466888 to
ddfaa68
Compare
samholmes
left a comment
There was a problem hiding this comment.
Partial review. I included an idea in my fixup! commit too.
samholmes
left a comment
There was a problem hiding this comment.
Finally finished with this. Whew
c81d6fb to
e2cebf6
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 7 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 7 issues found in the latest run.
- ✅ Fixed: Debug console.log left in production code
- Replaced the raw
console.login LiFi transaction processing with the scopedlog()call used elsewhere in the plugin.
- Replaced the raw
- ✅ Fixed: Rango tokenId uses raw addresses without normalization
- Rango now derives both deposit and payout
tokenIdvalues throughcreateTokenId(...)withtokenTypesinstead of storing raw addresses.
- Rango now derives both deposit and payout
- ✅ Fixed: LetsExchange status cleaner lacks fallback for unknown values
- Wrapped
asLetsExchangeStatuswithasMaybe(..., 'other')and added'other'tostatusMapso unknown statuses no longer throw in the cleaner.
- Wrapped
- ✅ Fixed: LetsExchange asValue has duplicate status entries
- Removed duplicate
'overdue'and'refund'entries from the LetsExchange status cleaner value list.
- Removed duplicate
- ✅ Fixed: ChangeNow conflates missing cache entry with native token
- Updated ChangeNow asset lookup to treat only
nullas native and throw onundefinedcache misses instead of mapping both totokenId: null.
- Updated ChangeNow asset lookup to treat only
- ✅ Fixed: Unused constant NETWORK_FIELDS_AVAILABLE_DATE defined
- Integrated
NETWORK_FIELDS_AVAILABLE_DATEinto LetsExchange asset resolution to allow missing network fields only before the cutoff and throw afterward.
- Integrated
- ✅ Fixed: Godex coins cache persists incomplete state on API failure
- Godex now caches coin data only on successful API responses and rethrows fetch errors so an incomplete fallback cache is not persisted.
Or push these changes by commenting:
@cursor push ecf51debb2
Preview (ecf51debb2)
diff --git a/src/partners/changenow.ts b/src/partners/changenow.ts
--- a/src/partners/changenow.ts
+++ b/src/partners/changenow.ts
@@ -344,14 +344,17 @@
// Look up contract address from cache
const contractAddress = getContractFromCache(currencyCode, network)
- // If not in cache or no contract address, it's a native token
- if (contractAddress == null) {
+ // null means native token, undefined means cache miss
+ if (contractAddress === null) {
return {
chainPluginId,
evmChainId,
tokenId: null
}
}
+ if (contractAddress === undefined) {
+ throw new Error(`Currency info not found for ${currencyCode} on ${network}`)
+ }
// Create tokenId from contract address
const tokenType = tokenTypes[chainPluginId]
diff --git a/src/partners/godex.ts b/src/partners/godex.ts
--- a/src/partners/godex.ts
+++ b/src/partners/godex.ts
@@ -132,6 +132,10 @@
try {
const url = 'https://api.godex.io/api/v1/coins'
const result = await retryFetch(url, { method: 'GET' })
+ if (!result.ok) {
+ const text = await result.text()
+ throw new Error(`Failed to fetch Godex coins: ${text}`)
+ }
const json = await result.json()
const coins = asGodexCoinsResponse(json)
@@ -149,11 +153,12 @@
}
}
log(`Coins cache loaded: ${cache.size} entries`)
+ godexCoinsCache = cache
+ return cache
} catch (e) {
- log.error('Error loading coins cache:', e)
+ log.error(`Error loading coins cache: ${String(e)}`)
+ throw e
}
- godexCoinsCache = cache
- return cache
}
interface GodexEdgeAssetInfo {
diff --git a/src/partners/letsexchange.ts b/src/partners/letsexchange.ts
--- a/src/partners/letsexchange.ts
+++ b/src/partners/letsexchange.ts
@@ -45,22 +45,23 @@
})
})
-const asLetsExchangeStatus = asValue(
- 'wait',
- 'confirmation',
- 'confirmed',
- 'exchanging',
- 'overdue',
- 'refund',
- 'sending',
- 'transferring',
- 'sending_confirmation',
- 'success',
- 'aml_check_failed',
- 'overdue',
- 'error',
- 'canceled',
- 'refund'
+const asLetsExchangeStatus = asMaybe(
+ asValue(
+ 'wait',
+ 'confirmation',
+ 'confirmed',
+ 'exchanging',
+ 'overdue',
+ 'refund',
+ 'sending',
+ 'transferring',
+ 'sending_confirmation',
+ 'success',
+ 'aml_check_failed',
+ 'error',
+ 'canceled'
+ ),
+ 'other'
)
// Cleaner for the new v2 API response
@@ -128,7 +129,8 @@
success: 'complete',
aml_check_failed: 'blocked',
canceled: 'cancelled',
- error: 'failed'
+ error: 'failed',
+ other: 'other'
}
// Map LetsExchange network codes to Edge pluginIds
@@ -289,14 +291,15 @@
initialNetwork: string | null,
currencyCode: string,
contractAddress: string | null,
- log: ScopedLog
+ isoDate: string
): AssetInfo | undefined {
- let network = initialNetwork
- if (network == null) {
- // Try using the currencyCode as the network
- network = currencyCode
- log(`Using currencyCode as network: ${network}`)
+ if (initialNetwork == null) {
+ 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]
@@ -500,14 +503,14 @@
tx.coin_from_network ?? tx.network_from_code,
tx.coin_from,
tx.coin_from_contract_address,
- log
+ isoDate
)
// Get payout asset info using contract address from API response
const payoutAsset = getAssetInfo(
tx.coin_to_network ?? tx.network_to_code,
tx.coin_to,
tx.coin_to_contract_address,
- log
+ isoDate
)
const status = statusMap[tx.status]
diff --git a/src/partners/lifi.ts b/src/partners/lifi.ts
--- a/src/partners/lifi.ts
+++ b/src/partners/lifi.ts
@@ -297,7 +297,7 @@
}
if (statusMap[tx.status] === 'complete') {
const { orderId, depositCurrency, payoutCurrency } = standardTx
- console.log(
+ log(
`${orderId} ${depositCurrency} ${depositChainPluginId} ${depositEvmChainId} ${depositTokenId?.slice(
0,
6
diff --git a/src/partners/rango.ts b/src/partners/rango.ts
--- a/src/partners/rango.ts
+++ b/src/partners/rango.ts
@@ -19,6 +19,7 @@
Status
} from '../types'
import { retryFetch } from '../util'
+import { createTokenId, tokenTypes } from '../util/asEdgeTokenId'
import { EVM_CHAIN_IDS } from '../util/chainIds'
// Start date for Rango transactions (first Edge transaction was 2024-06-23)
@@ -268,9 +269,17 @@
const dateStr = isoDate.split('T')[0]
const depositCurrency = firstStep.fromToken.symbol
- const depositTokenId = firstStep.fromToken.address ?? null
+ const depositTokenId = createTokenId(
+ tokenTypes[depositChainPluginId],
+ depositCurrency,
+ firstStep.fromToken.address ?? undefined
+ )
const payoutCurrency = lastStep.toToken.symbol
- const payoutTokenId = lastStep.toToken.address ?? null
+ const payoutTokenId = createTokenId(
+ tokenTypes[payoutChainPluginId],
+ payoutCurrency,
+ lastStep.toToken.address ?? undefined
+ )
log(
`${dateStr} ${depositCurrency} ${depositAmount} ${depositChainPluginId}${
@@ -299,7 +308,7 @@
payoutCurrency: lastStep.toToken.symbol,
payoutChainPluginId,
payoutEvmChainId,
- payoutTokenId: lastStep.toToken.address ?? null,
+ payoutTokenId,
payoutAmount,
timestamp,
isoDate,e2cebf6 to
6ed36d2
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for 3 of the 4 issues found in the latest run.
- ✅ Fixed: Moonpay sell transactions misclassified as buy transactions
- I changed Moonpay direction detection to use sell-specific fields (
quoteCurrency,payoutMethod,depositHash) so sell transactions are no longer misclassified whenpaymentMethodis present.
- I changed Moonpay direction detection to use sell-specific fields (
- ✅ Fixed: LetsExchange required fields make null guard dead code
- I restored
affiliateIdandapiKeyto optional cleaner fields so the existing null guard can gracefully return empty results when config keys are missing.
- I restored
- ✅ Fixed: Bitrefill deposits for bitcoin miss altcoin amount edge case
- I added a fallback to
btcPricewhen non-bitcoin transactions unexpectedly lackaltcoinPrice, preventing unnecessary transaction-processing failures.
- I added a fallback to
Or push these changes by commenting:
@cursor push 0da1faad74
Preview (0da1faad74)
diff --git a/src/partners/bitrefill.ts b/src/partners/bitrefill.ts
--- a/src/partners/bitrefill.ts
+++ b/src/partners/bitrefill.ts
@@ -279,10 +279,9 @@
const timestamp = tx.invoiceTime / 1000
const { paymentMethod } = tx
- let depositAmountStr: string | undefined
- if (paymentMethod === 'bitcoin') {
- depositAmountStr = tx.btcPrice
- } else if (tx.altcoinPrice != null) {
+ // Fallback to btcPrice when altcoinPrice is unexpectedly missing.
+ let depositAmountStr: string | undefined = tx.btcPrice
+ if (paymentMethod !== 'bitcoin' && tx.altcoinPrice != null) {
depositAmountStr = tx.altcoinPrice
}
if (depositAmountStr == null) {
diff --git a/src/partners/letsexchange.ts b/src/partners/letsexchange.ts
--- a/src/partners/letsexchange.ts
+++ b/src/partners/letsexchange.ts
@@ -40,8 +40,8 @@
latestIsoDate: asOptional(asString, LETSEXCHANGE_START_DATE)
}),
apiKeys: asObject({
- affiliateId: asString,
- apiKey: asString
+ affiliateId: asOptional(asString),
+ apiKey: asOptional(asString)
})
})
@@ -484,6 +484,10 @@
const { apiKey } = apiKeys
const { log } = pluginParams
+ if (apiKey == null) {
+ throw new Error('Missing LetsExchange apiKey')
+ }
+
await fetchCoinCache(apiKey, log)
const tx = asLetsExchangeTx(rawTx)
diff --git a/src/partners/moonpay.ts b/src/partners/moonpay.ts
--- a/src/partners/moonpay.ts
+++ b/src/partners/moonpay.ts
@@ -312,9 +312,14 @@
// Map Moonpay status to Edge status
const status: Status = statusMap[tx.status] ?? 'other'
- // Determine direction based on paymentMethod vs payoutMethod
- // Buy transactions have paymentMethod, sell transactions have payoutMethod
- const direction = tx.paymentMethod != null ? 'buy' : 'sell'
+ // Determine direction based on sell-specific fields.
+ // Sell transactions can also include paymentMethod, so that field alone is insufficient.
+ const direction =
+ tx.quoteCurrency != null ||
+ tx.payoutMethod != null ||
+ tx.depositHash != null
+ ? 'sell'
+ : 'buy'
// Get the payout currency - different field names for buy vs sell
const payoutCurrency = direction === 'buy' ? tx.currency : tx.quoteCurrency
eddy-edge
left a comment
There was a problem hiding this comment.
PR #207 Review Summary: Add Edge Asset Info to Partner Plugins
PR: #207
Author: paullinator (Paul V Puey)
Branch: paul/addEdgeAsset → master
Status: CHANGES_REQUESTED by samholmes
Files changed: 55 | Commits: ~30+
Overview
Adds chain-aware asset metadata (depositChainPluginId, depositEvmChainId, depositTokenId, and payout equivalents) across ~15 partner plugins. Introduces scoped logging (ScopedLog), a new rango partner, semaphore-based concurrency in the query engine, and CouchDB index updates for new fields.
Critical Issues
1. Reviewer Feedback Not Addressed: Async processTx Functions
Risk: Correctness / Performance | Status: UNRESOLVED
samholmes explicitly requested that processTx functions remain synchronous, with cache loading moved outside:
processChangeNowTx→ nowasync(changenow.ts diff L347) — callsawait loadCurrencyCache(log)inside every txprocessSideshiftTx→ nowasync(sideshift.ts diff L242) — same patternprocessBanxaOrders→ nowasync(banxa.ts diff L351)
Reviewer comments:
- sideshift.ts:309: "call the async function outside of the
processSideshiftTxroutine and pass in the cache state as a parameter" - banxa.ts:484: "can we keep processBanxaTx sync to be consistent with other plugins"
- changenow.ts: "Why make this an async function and why not just call this once before the processChangeNowTx calls?"
Impact: Promise overhead on every single transaction; inconsistent with sync plugins like moonpay and letsexchange. The Mutex/loaded-flag guards mitigate redundant fetches but the async pattern is unnecessary.
2. Moonpay: Silent Default to applepay
Risk: Data Correctness | Status: UNRESOLVED
When cardType is undefined for mobile_wallet payment method, it defaults to 'applepay' (moonpay.ts diff ~L396).
Reviewer comment: "Why do we assume applePay by default?" — paullinator replied "will fix" but the code still has the default.
3. Moonpay: Reduced Type Safety from Merged Cleaners
Risk: Correctness | Severity: Medium
The separate asMoonpayTx and asMoonpaySellTx cleaners were merged into a single asMoonpayTx with many optional fields (cryptoTransactionId, currency, walletAddress, depositHash, quoteCurrency, payoutMethod). This means the cleaner no longer validates that buy txs have buy-required fields and sell txs have sell-required fields.
Reviewer comment (moonpay.ts:153): "merging the types ... we then lose the type strictness"
4. Moonpay: All Statuses Now Ingested (Previously Only completed)
Risk: Data Regression | Severity: Medium
Previously, only status === 'completed' transactions were ingested. Now ALL transactions are pushed to standardTxs regardless of status, with unknown statuses silently mapped to 'other'. The statusMap only covers completed and pending.
This is intentional (commit "Include all moonpay txs") but could flood the database with incomplete/failed transactions that were previously excluded.
Addressed Issues (Fixed in Commit 6ed36d2)
The Cursor Bugbot autofix commit addressed several issues:
| Issue | File | Status |
|---|---|---|
console.log in production |
lifi.ts:299 |
Fixed → uses log() |
| Rango raw tokenId addresses | rango.ts:271-283 |
Fixed → uses createTokenId() |
| LetsExchange duplicate status values | letsexchange.ts:36-47 |
Fixed → asMaybe + deduped |
| ChangeNow null vs undefined cache | changenow.ts:344-360 |
Fixed → === null / === undefined |
| Godex cache persistence on failure | godex.ts:132-153 |
Fixed → only caches on success |
NETWORK_FIELDS_AVAILABLE_DATE unused |
letsexchange.ts:117 |
Fixed → integrated into getAssetInfo |
Other Notable Review Items
5. Global Caches Without TTL
Risk: Stale Data | Severity: Low-Medium
Module-level caches in banxa, changenow, sideshift, godex, letsexchange persist for the entire process lifetime. Since the query engine loops infinitely, caches never refresh after first load.
Reviewer comment (banxa.ts:95): "module-level cache persists for the process lifetime ... consider adding TTL-based invalidation"
6. QueryEngine: console.log in datelog Wrapper
Severity: Low
The moved datelog function in queryEngine.ts still uses raw console.log (L39). The old console.log(e) error handler was properly replaced with log.error (L341). The datelog usage is expected since it's a standalone utility.
7. Rates Engine: Not Actually Round-Robin
Severity: Low
Commit message says "round-robin" but implementation uses hardcoded server URLs (rates3.edge.app for v3, rates2.edge.app for v2). No rotation or random selection.
Reviewer comment (ratesEngine.ts:442): "this isn't round-robin as the commit message suggests"
8. disablePartnerQuery Field Undocumented
Severity: Low
New boolean field added to plugin settings but no comment explaining semantics.
Reviewer comment (types.ts:255): "Add comment explaining the semantics"
9. Thorchain: Silent Null Returns
Severity: Low
makeThorchainProcessTx silently returns null for multiple conditions without logging.
Reviewer comment (thorchain.ts:317): "Consider adding debug-level logging to indicate why transactions are being filtered out"
Security Review
- No injection risks: API keys come from config, not user input.
- No secrets in code: API keys passed via
pluginParams.apiKeys. - Cache poisoning: If a ChangeNow/Godex/etc API returns malformed data, it could populate caches with incorrect token mappings. The
godex.tsfix (only cache on success + rethrow) mitigates this for Godex. Other plugins have similar risk but use cleaners for validation. - No new endpoints exposed to external callers.
Recommendation
Do not merge as-is. The following should be addressed before merge:
- Must fix: Make
processChangeNowTx,processSideshiftTxsync per reviewer request — load caches once before the tx processing loop - Must fix: Remove or justify the
applepaydefault in moonpay - Should fix: Document the behavior change of ingesting all moonpay statuses (not just
completed) — confirm this is desired - Should fix: Add comment for
disablePartnerQuerysemantics - Nice to have: Add cache TTL or periodic refresh mechanism
eddy-edge
left a comment
There was a problem hiding this comment.
Thanks for the updates. Requesting changes because there are still blocking items that should be addressed before merge. Inline comments call out specific places.
|
All inline threads from this review have been addressed — either via code changes (5 fixup commits: 685dc66, 351d711, 794e0ce, c2f93f8, 0f391d9) or by replies explaining the rationale where we're keeping the current approach (per-tx async |
Review response — all 38 threads addressedSummary of what changed in code vs. what stays: Fixup commits (5)
Already addressed in prior commits
Intentionally kept (with reasoning)
Ready for re-review. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for 3 of the 4 issues found in the latest run.
- ✅ Fixed: Moonpay sell transactions missing payment type information
- Updated Moonpay sell transaction processing to use
getFiatPaymentType(tx)so payment metadata is preserved.
- Updated Moonpay sell transaction processing to use
- ✅ Fixed: ChangeHero module-level cache shared across all partners
- Added a 24-hour TTL with timestamp tracking to refresh the ChangeHero currency cache during long-running processes.
- ✅ Resolved by another fix: ChangeHero currency cache has no TTL refresh
- This was resolved by the same ChangeHero cache TTL refresh implementation added for the related cache staleness issue.
Or push these changes by commenting:
@cursor push 9bef4f3bc8
Preview (9bef4f3bc8)
diff --git a/src/partners/changehero.ts b/src/partners/changehero.ts
--- a/src/partners/changehero.ts
+++ b/src/partners/changehero.ts
@@ -125,6 +125,8 @@
contractAddress: string | null
}
let currencyCache: Map<string, CurrencyInfo> | null = null
+let currencyCacheTimestamp = 0
+const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
function makeCurrencyCacheKey(ticker: string, chain: string): string {
return `${ticker.toUpperCase()}_${chain.toLowerCase()}`
@@ -180,7 +182,12 @@
apiKey: string,
log: ScopedLog
): Promise<void> {
- if (currencyCache != null) return
+ if (
+ currencyCache != null &&
+ Date.now() - currencyCacheTimestamp < CACHE_TTL_MS
+ ) {
+ return
+ }
try {
const response = await retryFetch(API_URL, {
@@ -213,6 +220,7 @@
currencyCache.set(key, info)
}
}
+ currencyCacheTimestamp = Date.now()
log(`Cached ${currencyCache.size} currency entries`)
} catch (e) {
diff --git a/src/partners/moonpay.ts b/src/partners/moonpay.ts
--- a/src/partners/moonpay.ts
+++ b/src/partners/moonpay.ts
@@ -372,7 +372,7 @@
depositAmount: tx.baseCurrencyAmount,
direction,
exchangeType: 'fiat',
- paymentType: null,
+ paymentType: getFiatPaymentType(tx),
payoutTxid: undefined,
payoutAddress: undefined,
payoutCurrency: sellFields.quoteCurrency.code.toUpperCase(),You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: ChangeNow currency cache shared as global singleton
- I replaced the singleton ChangeNow currency cache with a key-scoped cache map (using API key/public key) and threaded the selected cache through transaction processing.
- ✅ Fixed: Exported function
processTxname is too generic- I renamed Moonpay’s exported processor to
processMoonpayTxand updated its internal call sites to match the established partner naming convention.
- I renamed Moonpay’s exported processor to
Or push these changes by commenting:
@cursor push 3854e5320d
Preview (3854e5320d)
diff --git a/src/partners/changenow.ts b/src/partners/changenow.ts
--- a/src/partners/changenow.ts
+++ b/src/partners/changenow.ts
@@ -117,14 +117,13 @@
// Key format: "ticker:network" -> tokenContract
interface CurrencyCache {
currencies: Map<string, string | null> // ticker:network -> tokenContract
- loaded: boolean
}
-
-const currencyCache: CurrencyCache = {
- currencies: new Map(),
- loaded: false
+interface ChangeNowCacheEntry {
+ cache: CurrencyCache
+ timestamp: number
}
-let currencyCacheTimestamp = 0
+const currencyCacheByKey: Map<string, ChangeNowCacheEntry> = new Map()
+const PUBLIC_CACHE_KEY = '__public__'
const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
/**
@@ -133,15 +132,16 @@
async function loadCurrencyCache(
log: ScopedLog,
apiKey?: string
-): Promise<void> {
- if (
- currencyCache.loaded &&
- Date.now() - currencyCacheTimestamp < CACHE_TTL_MS
- ) {
- return
+): Promise<CurrencyCache> {
+ const cacheKey = apiKey ?? PUBLIC_CACHE_KEY
+ const existing = currencyCacheByKey.get(cacheKey)
+ if (existing != null && Date.now() - existing.timestamp < CACHE_TTL_MS) {
+ return existing.cache
}
try {
+ const cache: CurrencyCache = { currencies: new Map() }
+
// The exchange/currencies endpoint doesn't require authentication
const url = 'https://api.changenow.io/v2/exchange/currencies?active=true'
const response = await retryFetch(url, {
@@ -158,7 +158,7 @@
for (const currency of currencies) {
const key = `${currency.ticker.toLowerCase()}:${currency.network.toLowerCase()}`
- currencyCache.currencies.set(key, currency.tokenContract ?? null)
+ cache.currencies.set(key, currency.tokenContract ?? null)
// Also cache by legacyTicker if different from ticker
if (
@@ -166,13 +166,13 @@
currency.legacyTicker !== currency.ticker
) {
const legacyKey = `${currency.legacyTicker.toLowerCase()}:${currency.network.toLowerCase()}`
- currencyCache.currencies.set(legacyKey, currency.tokenContract ?? null)
+ cache.currencies.set(legacyKey, currency.tokenContract ?? null)
}
}
- currencyCache.loaded = true
- currencyCacheTimestamp = Date.now()
+ currencyCacheByKey.set(cacheKey, { cache, timestamp: Date.now() })
log(`Currency cache loaded with ${currencies.length} entries`)
+ return cache
} catch (e) {
log.error(`Error loading currency cache: ${e}`)
throw e
@@ -183,6 +183,7 @@
* Look up contract address from cache
*/
function getContractFromCache(
+ currencyCache: CurrencyCache,
ticker: string,
network: string
): string | null | undefined {
@@ -338,7 +339,11 @@
* Get the Edge asset info for a given network and currency code.
* Uses the cached currency data from the ChangeNow API.
*/
-function getAssetInfo(network: string, currencyCode: string): EdgeAssetInfo {
+function getAssetInfo(
+ currencyCache: CurrencyCache,
+ network: string,
+ currencyCode: string
+): EdgeAssetInfo {
// Map network to pluginId
const chainPluginId = CHANGENOW_NETWORK_TO_PLUGIN_ID[network.toLowerCase()]
if (chainPluginId == null) {
@@ -348,7 +353,11 @@
const evmChainId = EVM_CHAIN_IDS[chainPluginId]
// Look up contract address from cache
- const contractAddress = getContractFromCache(currencyCode, network)
+ const contractAddress = getContractFromCache(
+ currencyCache,
+ currencyCode,
+ network
+ )
// null means native token, undefined means cache miss
if (contractAddress === null) {
@@ -388,7 +397,8 @@
): Promise<StandardTx> {
const { log } = pluginParams
// Load currency cache before processing transactions
- await loadCurrencyCache(log)
+ const { apiKeys } = asChangeNowPluginParams(pluginParams)
+ const currencyCache = await loadCurrencyCache(log, apiKeys.apiKey)
const tx: ChangeNowTx = asChangeNowTx(rawTx)
const date = new Date(
@@ -397,10 +407,18 @@
const timestamp = date.getTime() / 1000
// Get deposit asset info
- const depositAsset = getAssetInfo(tx.payin.network, tx.payin.currency)
+ const depositAsset = getAssetInfo(
+ currencyCache,
+ tx.payin.network,
+ tx.payin.currency
+ )
// Get payout asset info
- const payoutAsset = getAssetInfo(tx.payout.network, tx.payout.currency)
+ const payoutAsset = getAssetInfo(
+ currencyCache,
+ tx.payout.network,
+ tx.payout.currency
+ )
const standardTx: StandardTx = {
status: statusMap[tx.status],
diff --git a/src/partners/moonpay.ts b/src/partners/moonpay.ts
--- a/src/partners/moonpay.ts
+++ b/src/partners/moonpay.ts
@@ -229,7 +229,7 @@
const txs = asMoonpayResult(await result.json())
for (const rawTx of txs) {
- const standardTx = processTx(rawTx, 'sell')
+ const standardTx = processMoonpayTx(rawTx, 'sell')
standardTxs.push(standardTx)
}
@@ -261,7 +261,7 @@
// in bulk update it throws an error for document update conflict because of this.
for (const rawTx of txs) {
- const standardTx = processTx(rawTx, 'buy')
+ const standardTx = processMoonpayTx(rawTx, 'buy')
standardTxs.push(standardTx)
}
if (txs.length > 0) {
@@ -309,7 +309,7 @@
pluginId: 'moonpay'
}
-export function processTx(
+export function processMoonpayTx(
rawTx: unknown,
direction: 'buy' | 'sell'
): StandardTx {You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: ChangeNow cache never clears stale entries on refresh
loadCurrencyCachenow builds a fresh Map and atomically replacescurrencyCache.currencies, removing delisted entries on refresh.
- ✅ Fixed: Bitrefill deposits with bitcoin use raw division not
smartIsoDateFromTimestamptimestampprocessBitrefillTxnow uses the normalizedtimestampreturned bysmartIsoDateFromTimestamp(tx.invoiceTime)instead of manually dividing by 1000.
- ✅ Fixed: Godex
getGodexCoinsCachereturns stale data after API failuregetGodexCoinsCachenow falls back to existing or delisted-token cache data on fetch failure so repeated calls do not keep re-hitting a failing API.
Or push these changes by commenting:
@cursor push 70aa0556a5
Preview (70aa0556a5)
diff --git a/src/partners/bitrefill.ts b/src/partners/bitrefill.ts
--- a/src/partners/bitrefill.ts
+++ b/src/partners/bitrefill.ts
@@ -254,7 +254,7 @@
} catch (e) {
throw new Error(`${String(e)}: ${JSON.stringify(rawTx)}`)
}
- const { isoDate } = smartIsoDateFromTimestamp(tx.invoiceTime)
+ const { isoDate, timestamp } = smartIsoDateFromTimestamp(tx.invoiceTime)
const countryCode = countryCodeMap[tx.country]
if (tx.altcoinPrice != null) {
@@ -276,8 +276,6 @@
}
const evmChainId = EVM_CHAIN_IDS[edgeAsset.pluginId]
- const timestamp = tx.invoiceTime / 1000
-
const { paymentMethod } = tx
let depositAmountStr: string | undefined
if (paymentMethod === 'bitcoin') {
diff --git a/src/partners/changenow.ts b/src/partners/changenow.ts
--- a/src/partners/changenow.ts
+++ b/src/partners/changenow.ts
@@ -155,10 +155,11 @@
const result = await response.json()
const currencies = asChangeNowCurrencyArray(result)
+ const cache = new Map<string, string | null>()
for (const currency of currencies) {
const key = `${currency.ticker.toLowerCase()}:${currency.network.toLowerCase()}`
- currencyCache.currencies.set(key, currency.tokenContract ?? null)
+ cache.set(key, currency.tokenContract ?? null)
// Also cache by legacyTicker if different from ticker
if (
@@ -166,10 +167,11 @@
currency.legacyTicker !== currency.ticker
) {
const legacyKey = `${currency.legacyTicker.toLowerCase()}:${currency.network.toLowerCase()}`
- currencyCache.currencies.set(legacyKey, currency.tokenContract ?? null)
+ cache.set(legacyKey, currency.tokenContract ?? null)
}
}
+ currencyCache.currencies = cache
currencyCache.loaded = true
currencyCacheTimestamp = Date.now()
log(`Currency cache loaded with ${currencies.length} entries`)
diff --git a/src/partners/godex.ts b/src/partners/godex.ts
--- a/src/partners/godex.ts
+++ b/src/partners/godex.ts
@@ -163,7 +163,16 @@
return cache
} catch (e) {
log.error(`Error loading coins cache: ${String(e)}`)
- throw e
+ if (godexCoinsCache != null) {
+ // Keep using stale cache if refresh fails to avoid repeated failing requests.
+ godexCoinsCacheTimestamp = Date.now()
+ return godexCoinsCache
+ }
+
+ // Fall back to delisted-token cache when the first API fetch fails.
+ godexCoinsCache = cache
+ godexCoinsCacheTimestamp = Date.now()
+ return cache
}
}You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Chain lookup missing case normalization in ChangeHero
- Updated ChangeHero chain map lookup to use
chain.toLowerCase()so mixed-case API chain names resolve correctly.
- Updated ChangeHero chain map lookup to use
- ✅ Fixed: Exolix gas token check uses case-sensitive comparison
- Changed Exolix gas-token contract matching to compare lowercase values so address casing differences no longer break native-token detection.
Or push these changes by commenting:
@cursor push cb43a5fe3a
Preview (cb43a5fe3a)
diff --git a/src/partners/changehero.ts b/src/partners/changehero.ts
--- a/src/partners/changehero.ts
+++ b/src/partners/changehero.ts
@@ -256,7 +256,7 @@
throw new Error(`Missing chain for currency ${currencyCode}`)
}
- const chainPluginId = CHANGEHERO_CHAIN_TO_PLUGIN_ID[chain]
+ const chainPluginId = CHANGEHERO_CHAIN_TO_PLUGIN_ID[chain.toLowerCase()]
if (chainPluginId == null) {
throw new Error(
`Unknown Changehero chain "${chain}" for currency ${currencyCode}. Add mapping to CHANGEHERO_CHAIN_TO_PLUGIN_ID.`
diff --git a/src/partners/exolix.ts b/src/partners/exolix.ts
--- a/src/partners/exolix.ts
+++ b/src/partners/exolix.ts
@@ -204,7 +204,13 @@
// Look up tokenId from contract address
let tokenId: EdgeTokenId = null
if (contract != null) {
- if (GASTOKEN_CONTRACTS.includes(contract) && network === currencyCode) {
+ if (
+ GASTOKEN_CONTRACTS.some(
+ gasTokenContract =>
+ gasTokenContract.toLowerCase() === contract.toLowerCase()
+ ) &&
+ network === currencyCode
+ ) {
tokenId = null
} else {
const tokenType = tokenTypes[chainPluginId]You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Moonpay silently drops chain info for unknown networks
- Added an explicit throw in processMetadata when neither networkCode nor chainId resolves to a known chain so unknown Moonpay networks no longer silently pass through.
Or push these changes by commenting:
@cursor push 7eb8d22ce6
Preview (7eb8d22ce6)
diff --git a/src/partners/moonpay.ts b/src/partners/moonpay.ts
--- a/src/partners/moonpay.ts
+++ b/src/partners/moonpay.ts
@@ -84,6 +84,11 @@
? MOONPAY_NETWORK_TO_PLUGIN_ID[networkCode]
: undefined) ??
(chainIdNum != null ? REVERSE_EVM_CHAIN_IDS[chainIdNum] : undefined)
+ if (chainPluginId == null) {
+ throw new Error(
+ `Unknown network for currency ${currencyCode}: networkCode=${networkCode}, chainId=${rawChainId}`
+ )
+ }
// Determine evmChainId
let evmChainId: number | undefinedYou can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Bugbot Autofix prepared a fix for 1 of the 3 issues found in the latest run.
- ✅ Fixed: ChangeNow shared global currency cache across concurrent partners
- I serialized concurrent cache refreshes with a shared in-flight promise and atomically swapped in a rebuilt map to prevent clear-and-repopulate races.
Or push these changes by commenting:
@cursor push d0596199bf
Preview (d0596199bf)
diff --git a/src/partners/changenow.ts b/src/partners/changenow.ts
--- a/src/partners/changenow.ts
+++ b/src/partners/changenow.ts
@@ -125,6 +125,7 @@
loaded: false
}
let currencyCacheTimestamp = 0
+let currencyCacheLoadPromise: Promise<void> | undefined
const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
/**
@@ -141,44 +142,57 @@
return
}
- try {
- // The exchange/currencies endpoint doesn't require authentication
- const url = 'https://api.changenow.io/v2/exchange/currencies?active=true'
- const response = await retryFetch(url, {
- method: 'GET'
- })
+ if (currencyCacheLoadPromise != null) {
+ await currencyCacheLoadPromise
+ return
+ }
- if (!response.ok) {
- const text = await response.text()
- throw new Error(`Failed to fetch currencies: ${text}`)
- }
+ currencyCacheLoadPromise = (async () => {
+ try {
+ // The exchange/currencies endpoint doesn't require authentication
+ const url = 'https://api.changenow.io/v2/exchange/currencies?active=true'
+ const response = await retryFetch(url, {
+ method: 'GET'
+ })
- const result = await response.json()
- const currencies = asChangeNowCurrencyArray(result)
+ if (!response.ok) {
+ const text = await response.text()
+ throw new Error(`Failed to fetch currencies: ${text}`)
+ }
- // Clear stale entries (delisted/renamed currencies) before repopulating
- currencyCache.currencies.clear()
+ const result = await response.json()
+ const currencies = asChangeNowCurrencyArray(result)
- for (const currency of currencies) {
- const key = `${currency.ticker.toLowerCase()}:${currency.network.toLowerCase()}`
- currencyCache.currencies.set(key, currency.tokenContract ?? null)
+ // Build a new map and swap it in atomically to avoid partial reads.
+ const nextCurrencies = new Map<string, string | null>()
+ for (const currency of currencies) {
+ const key = `${currency.ticker.toLowerCase()}:${currency.network.toLowerCase()}`
+ nextCurrencies.set(key, currency.tokenContract ?? null)
- // Also cache by legacyTicker if different from ticker
- if (
- currency.legacyTicker != null &&
- currency.legacyTicker !== currency.ticker
- ) {
- const legacyKey = `${currency.legacyTicker.toLowerCase()}:${currency.network.toLowerCase()}`
- currencyCache.currencies.set(legacyKey, currency.tokenContract ?? null)
+ // Also cache by legacyTicker if different from ticker
+ if (
+ currency.legacyTicker != null &&
+ currency.legacyTicker !== currency.ticker
+ ) {
+ const legacyKey = `${currency.legacyTicker.toLowerCase()}:${currency.network.toLowerCase()}`
+ nextCurrencies.set(legacyKey, currency.tokenContract ?? null)
+ }
}
+
+ currencyCache.currencies = nextCurrencies
+ currencyCache.loaded = true
+ currencyCacheTimestamp = Date.now()
+ log(`Currency cache loaded with ${currencies.length} entries`)
+ } catch (e) {
+ log.error(`Error loading currency cache: ${e}`)
+ throw e
}
+ })()
- currencyCache.loaded = true
- currencyCacheTimestamp = Date.now()
- log(`Currency cache loaded with ${currencies.length} entries`)
- } catch (e) {
- log.error(`Error loading currency cache: ${e}`)
- throw e
+ try {
+ await currencyCacheLoadPromise
+ } finally {
+ currencyCacheLoadPromise = undefined
}
}You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit f1bbd4a. Configure here.


CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
noneDescription
noneNote
Medium Risk
Moderate risk because it changes transaction normalization across multiple partner plugins (chain pluginIds, EVM chainIds, tokenIds) and adds new CouchDB indexes and bulk-mutation scripts, which could affect downstream analytics and data correctness.
Overview
Adds chain/token asset attribution across partner ingesters. Multiple partner plugins now populate
deposit/payoutChainPluginId,deposit/payoutEvmChainId, anddeposit/payoutTokenIdby mapping provider network/chain fields and (when needed) fetching/caching provider coin metadata (notablybanxa,changenow,changehero,godex,letsexchange,exolix,moonpay,sideshift; plus improved token-id creation/logging inlifi).Introduces a new partner + operational tooling. Adds a new
rangopartner plugin, adds new CouchDB design-doc indexes for chain/token fields, and introduces two maintenance scripts (fixTokenIdsandresetUsdValues) to bulk-normalize tokenIds / force usdValue recalculation; also updates CLI utilities to use scoped logging and improvestestpartnerto dynamically load a partner by id. Demo endpoints were updated to useapiKeyinstead ofappIdwhen requesting plugin ids/analytics.Reviewed by Cursor Bugbot for commit b5ae82a. Bugbot is set up for automated code reviews on this repo. Configure here.