Skip to content

Paul/v3 rates and fixes#205

Open
paullinator wants to merge 22 commits intomasterfrom
paul/v3RatesAndFixes
Open

Paul/v3 rates and fixes#205
paullinator wants to merge 22 commits intomasterfrom
paul/v3RatesAndFixes

Conversation

@paullinator
Copy link
Member

@paullinator paullinator commented Nov 20, 2025

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

none

Note

Medium Risk
Medium risk because it changes the persisted StandardTx schema across many partner adapters and rewrites rate-calculation logic to call a new external rates3 endpoint, which can affect valuation and backfills if mappings are wrong.

Overview
Adds chain/token awareness to transactions. StandardTx now includes deposit/payoutChainPluginId, deposit/payoutEvmChainId, and deposit/payoutTokenId, and all partner processors are updated to populate these (mostly as undefined), with Li.Fi now deriving real values via new token-id and EVM chain-id helpers.

Updates rate calculation behavior. The rates engine now uses the rates3.edge.app/v3/rates API when transactions include plugin/token identifiers (or fiat/crypto mixes), and updates the legacy rates lookup to hit rates2 v2 with iso: fiat prefixes plus a same-currency fast-path.

Misc fixes/ops. Adds a CouchDB mango index on orderId, expands Moonpay payment method support (including Revolut) and Banxa ACH mapping, tightens DB init to require couchUris (with a default), improves timeout cleanup, and normalizes a few CLI/script behaviors (exit codes, npm script names).

Written by Cursor Bugbot for commit 2eb5444. This will update automatically on new commits. Configure here.

Comment on lines +23 to +26
"start.cache": "node -r sucrase/register src/indexCache.ts",
"start.rates": "node -r sucrase/register src/indexRates.ts",
"start.api": "node -r sucrase/register src/indexApi.ts",
"start.destroyPartition": "node -r sucrase/register src/bin/destroyPartition.ts",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Optional

I'd argue to keep the colon over the dot as a separator just because colon has become the de facto standard. Not worth being different from the rest of codebases here.

Copy link
Member Author

Choose a reason for hiding this comment

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

But they are such a pain to type.

datelog(e)
process.exit(1)
}
process.exit(0)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The reason why this was hanging is because of we don't cleanup the setTimeout in promiseTimeout util. This should be fixed as a separate commit.

if (paymentMethod == null) {
throw new Error(`Unknown payment method: ${tx.paymentMethod} for ${tx.id}`)
}
return paymentMethod
Copy link

Choose a reason for hiding this comment

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

Moonpay mobile_wallet case now throws instead of returning null

High Severity

The refactored getFiatPaymentType function has a behavioral regression for the mobile_wallet payment method. When cardType is not 'apple_pay' or 'google_pay' (e.g., it's 'card' or missing), paymentMethod is set to null. The check on line 309 then throws an error for this case. The original implementation would return null without throwing, allowing these transactions to process with a null payment type.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

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

we want it to throw so the plugin fails, and we properly edit the code to add the new types.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

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: V3 rate failures cause infinite transaction reprocessing loop
    • updateTxValuesV3 now marks transactions as failed by setting _id to undefined on invalid assets, V3 fetch/parse errors, and when no payout or USD update was applied.
  • ✅ Fixed: Lifi chain plugin ID lookup ignores token fallback
    • LiFi chain plugin lookups now use the existing chain-code fallback values derived from gas token or token fields, so missing gas tokens no longer break plugin-id resolution.

Create PR

Or push these changes by commenting:

@cursor push 0986684dec
Preview (0986684dec)
diff --git a/src/partners/lifi.ts b/src/partners/lifi.ts
--- a/src/partners/lifi.ts
+++ b/src/partners/lifi.ts
@@ -232,14 +232,10 @@
   // Try using the gas token code first, then chain id if we have one.
   const depositChainPluginId =
     REVERSE_EVM_CHAIN_IDS[depositEvmChainId ?? 0] ??
-    MAINNET_CODE_TRANSCRIPTION[
-      tx.sending.gasToken?.coinKey ?? tx.sending.gasToken?.symbol ?? ''
-    ]
+    MAINNET_CODE_TRANSCRIPTION[depositChainCode ?? '']
   const payoutChainPluginId =
     REVERSE_EVM_CHAIN_IDS[payoutEvmChainId ?? 0] ??
-    MAINNET_CODE_TRANSCRIPTION[
-      tx.receiving.gasToken?.coinKey ?? tx.receiving.gasToken?.symbol ?? ''
-    ]
+    MAINNET_CODE_TRANSCRIPTION[payoutChainCode ?? '']
 
   if (depositChainPluginId == null || payoutChainPluginId == null) {
     throw new Error('Missing chain plugin id')

diff --git a/src/ratesEngine.ts b/src/ratesEngine.ts
--- a/src/ratesEngine.ts
+++ b/src/ratesEngine.ts
@@ -111,6 +111,11 @@
 }
 
 async function updateTxValuesV3(transaction: DbTx): Promise<void> {
+  let updated = false
+  const fail = (): void => {
+    transaction._id = undefined
+  }
+
   const {
     isoDate,
     depositCurrency,
@@ -154,6 +159,7 @@
     console.error(
       `Deposit asset is not a crypto asset or fiat currency ${depositCurrency} ${depositChainPluginId} ${depositTokenId}`
     )
+    fail()
     return
   }
 
@@ -179,18 +185,26 @@
     console.error(
       `Payout asset is not a crypto asset or fiat currency ${payoutCurrency} ${payoutChainPluginId} ${payoutTokenId}`
     )
+    fail()
     return
   }
 
-  const ratesResponse = await fetch('https://rates3.edge.app/v3/rates', {
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/json'
-    },
-    body: JSON.stringify(ratesRequest)
-  })
-  const ratesResponseJson = await ratesResponse.json()
-  const rates = asRatesV3Params(ratesResponseJson)
+  let rates: RatesV3Params
+  try {
+    const ratesResponse = await fetch('https://rates3.edge.app/v3/rates', {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      body: JSON.stringify(ratesRequest)
+    })
+    const ratesResponseJson = await ratesResponse.json()
+    rates = asRatesV3Params(ratesResponseJson)
+  } catch (e) {
+    console.error('Error fetching V3 rates', e)
+    fail()
+    return
+  }
   const depositRateObf = depositIsFiat
     ? rates.fiat.find(rate => rate.fiatCode === depositCurrency)
     : rates.crypto.find(
@@ -225,6 +239,7 @@
     }
     if (depositRate != null && payoutRate != null) {
       transaction.payoutAmount = (depositAmount * depositRate) / payoutRate
+      updated = true
     }
   }
 
@@ -233,10 +248,16 @@
   if (transaction.usdValue == null || transaction.usdValue <= 0) {
     if (depositRate != null) {
       transaction.usdValue = depositAmount * depositRate
+      updated = true
     } else if (payoutRate != null) {
       transaction.usdValue = transaction.payoutAmount * payoutRate
+      updated = true
     }
   }
+
+  if (!updated) {
+    fail()
+  }
 }
 
 async function updateTxValues(

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

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: Missing hyperevm chain ID in EVM_CHAIN_IDS mapping
    • Added hyperevm: 999 to EVM_CHAIN_IDS so Li.Fi HyperEVM transfers now resolve and populate EVM chain IDs consistently.

Create PR

Or push these changes by commenting:

@cursor push 05008ed393
Preview (05008ed393)
diff --git a/src/util/chainIds.ts b/src/util/chainIds.ts
--- a/src/util/chainIds.ts
+++ b/src/util/chainIds.ts
@@ -12,6 +12,7 @@
   ethereumpow: 10001,
   fantom: 250,
   filecoinfevm: 314,
+  hyperevm: 999,
   optimism: 10,
   polygon: 137,
   pulsechain: 369,

paullinator and others added 3 commits February 27, 2026 14:02
Clear the timeout when the promise resolves or rejects to prevent
the process from hanging due to uncancelled timers.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: V3 rate path lacks zero-rate guard before division
    • Added positive-rate guards in the V3 payout and USD calculations so zero rates no longer produce Infinity or overwrite sentinel values.

Create PR

Or push these changes by commenting:

@cursor push 94ec4a166c
Preview (94ec4a166c)
diff --git a/src/ratesEngine.ts b/src/ratesEngine.ts
--- a/src/ratesEngine.ts
+++ b/src/ratesEngine.ts
@@ -229,7 +229,12 @@
         `No rate found for payout ${payoutCurrency} ${payoutChainPluginId} ${payoutTokenId}`
       )
     }
-    if (depositRate != null && payoutRate != null) {
+    if (
+      depositRate != null &&
+      depositRate > 0 &&
+      payoutRate != null &&
+      payoutRate > 0
+    ) {
       transaction.payoutAmount = (depositAmount * depositRate) / payoutRate
     }
   }
@@ -237,9 +242,9 @@
   // Calculate the usdValue first trying to use the deposit amount. If that's not available
   // then try to use the payout amount.
   if (transaction.usdValue == null || transaction.usdValue <= 0) {
-    if (depositRate != null) {
+    if (depositRate != null && depositRate > 0) {
       transaction.usdValue = depositAmount * depositRate
-    } else if (payoutRate != null) {
+    } else if (payoutRate != null && payoutRate > 0) {
       transaction.usdValue = transaction.payoutAmount * payoutRate
     }
   }

}
if (depositRate != null && payoutRate != null) {
transaction.payoutAmount = (depositAmount * depositRate) / payoutRate
}
Copy link

Choose a reason for hiding this comment

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

V3 rate path lacks zero-rate guard before division

Medium Severity

The V3 rates path in updateTxValuesV3 only checks payoutRate != null before dividing by it at line 233, unlike the V2 path which consistently guards with exchangeRate > 0. If the rates API returns a rate of 0 for a payout asset, (depositAmount * depositRate) / payoutRate produces Infinity, which would be persisted to the database. Similarly, a depositRate of 0 would write usdValue = 0, which differs from the initial -1 sentinel and could prevent the transaction from being re-queried for rate updates.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems reasonable.

Copy link
Collaborator

@samholmes samholmes left a comment

Choose a reason for hiding this comment

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

I approve after the final cursor bot divide by zero fix is in

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants