Skip to content

Feat/nexchange plugin#217

Open
paullinator wants to merge 2 commits into
masterfrom
feat/nexchange-plugin
Open

Feat/nexchange plugin#217
paullinator wants to merge 2 commits into
masterfrom
feat/nexchange-plugin

Conversation

@paullinator
Copy link
Copy Markdown
Member

@paullinator paullinator commented Jun 4, 2026

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

none

Note

Medium Risk
New partner sync and stricter token-id handling change which transactions persist and how USD volume is computed; misconfiguration could block ingestion or alter reported swap volume until mappings are fixed.

Overview
Adds an n.exchange swap partner plugin that pulls Edge audit orders (with x-api-key), enriches deposit/payout via the v2 currency catalog (network → Edge chain plugin, contract → tokenId), and registers it in the query engine and demo partner list.

Token/volume safety: ChangeNow no longer swallows createTokenId failures as native (tokenId: null). Rango stops skipping per-transaction processing errors so bad token resolution halts the run (partial progress still saved). Rango also maps MONAD and SUI blockchains.

Unit tests cover n.exchange status mapping, asset resolution (including throw paths), dates, and pagination helpers.

Reviewed by Cursor Bugbot for commit 130f101. Bugbot is set up for automated code reviews on this repo. Configure here.

MMrj9 and others added 2 commits June 4, 2026 07:49
Co-authored-by: Cursor <cursoragent@cursor.com>
When an asset has a contract address it is a token, but several plugins
silently fell back to a native (tokenId: null) mapping when the token
could not be resolved. That prices the token with the chain's gas-token
rate and overcounts volume whenever the token is worth less than the gas
token.

- nexchange: throw when a contract-bearing asset is on a chain whose
  tokenType is missing, instead of returning tokenId: null.
- changenow: drop the try/catch around createTokenId that swallowed
  failures and returned tokenId: null.
- rango: drop the per-tx try/catch that logged and continued, silently
  dropping any transaction whose asset could not be resolved.

All three plugins paginate oldest-to-newest and persist progress on
throw, so a failing order halts and is retried next run rather than being
mispriced or silently dropped.

Also add the SUI and MONAD chain mappings to rango: these were
previously dropped silently and would now halt the plugin. Verified by
reprocessing the last three months of orders (nexchange 22.7k, changenow
61.9k, rango 2.9k) with zero processing failures.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copy link
Copy Markdown

@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.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: processNexchangeTx wrong signature
    • Changed processNexchangeTx to accept PluginParams and load the cached Nexchange currency map internally.
  • ✅ Fixed: Offset stale after cursor pages
    • Advanced the offset after every fetched page so fallback offset pagination resumes after all cursor-fetched orders.

Create PR

Or push these changes by commenting:

@cursor push 6c25e6a18c
Preview (6c25e6a18c)
diff --git a/src/partners/nexchange.ts b/src/partners/nexchange.ts
--- a/src/partners/nexchange.ts
+++ b/src/partners/nexchange.ts
@@ -69,6 +69,10 @@
 const LIMIT = 200
 const MAX_ERROR_TEXT_LENGTH = 500
 
+let nexchangeCurrencyMapPromise:
+  | Promise<NexchangeCurrencyInfoMap>
+  | undefined
+
 const statusMap: { [key: string]: Status } = {
   released: 'complete',
   complete: 'complete',
@@ -192,6 +196,27 @@
   return map
 }
 
+async function getNexchangeCurrencyMap(
+  pluginParams: PluginParams
+): Promise<NexchangeCurrencyInfoMap> {
+  if (nexchangeCurrencyMapPromise == null) {
+    nexchangeCurrencyMapPromise = fetchNexchangeCurrencyMap()
+      .then(currencyMap => {
+        pluginParams.log(
+          `Nexchange currency map loaded with ${
+            Object.keys(currencyMap).length
+          } entries`
+        )
+        return currencyMap
+      })
+      .catch(error => {
+        nexchangeCurrencyMapPromise = undefined
+        throw error
+      })
+  }
+  return await nexchangeCurrencyMapPromise
+}
+
 /**
  * Returned by `resolveNexchangeAsset`.  The shape is consistent across all
  * exit branches so callers can rely on the field set.  `chainPluginId`,
@@ -311,12 +336,6 @@
   let offset = 0
 
   try {
-    // The currency catalog supplies the network/contract metadata that the
-    // audit-orders endpoint omits, so it is required for chain/token
-    // enrichment.  Fetch it up front; a failure aborts the run (saving
-    // nothing) rather than persisting a batch of unenriched transactions.
-    const currencyMap = await fetchNexchangeCurrencyMap()
-
     while (true) {
       const params: string[] = [
         `dateFrom=${encodeURIComponent(queryDateFrom)}`,
@@ -341,7 +360,7 @@
       const { orders, nextCursor, hasMore } = asNexchangeOrdersResponse(json)
 
       for (const rawOrder of orders) {
-        const standardTx = processNexchangeTx(rawOrder, currencyMap)
+        const standardTx = await processNexchangeTx(rawOrder, pluginParams)
         txByOrderId.set(standardTx.orderId, standardTx)
         if (standardTx.isoDate > latestIsoDate) {
           latestIsoDate = standardTx.isoDate
@@ -351,6 +370,7 @@
 
       if (!hasMore || orders.length === 0) break
 
+      offset += orders.length
       if (nextCursor != null && nextCursor !== '') {
         cursor = nextCursor
       } else {
@@ -358,7 +378,6 @@
         // cursor value would re-pin pagination to the wrong position next
         // iteration.
         cursor = undefined
-        offset += orders.length
       }
     }
   } catch (e) {
@@ -384,6 +403,15 @@
 
 export function processNexchangeTx(
   rawTx: unknown,
+  pluginParams: PluginParams
+): Promise<StandardTx> {
+  return getNexchangeCurrencyMap(pluginParams).then(currencyMap =>
+    standardizeNexchangeOrder(rawTx, currencyMap)
+  )
+}
+
+export function standardizeNexchangeOrder(
+  rawTx: unknown,
   currencyMap: NexchangeCurrencyInfoMap
 ): StandardTx {
   const tx = asNexchangeOrder(rawTx)

diff --git a/test/nexchange.test.ts b/test/nexchange.test.ts
--- a/test/nexchange.test.ts
+++ b/test/nexchange.test.ts
@@ -5,8 +5,8 @@
   NEXCHANGE_NETWORK_TO_PLUGIN_ID,
   NexchangeCurrencyInfoMap,
   parseApiDate,
-  processNexchangeTx,
   resolveNexchangeAsset,
+  standardizeNexchangeOrder,
   toQueryIsoDate
 } from '../src/partners/nexchange'
 
@@ -115,9 +115,9 @@
 }
 
 describe('nexchange plugin', () => {
-  describe('processNexchangeTx', () => {
+  describe('standardizeNexchangeOrder', () => {
     it('maps Edge audit order payload into StandardTx with chain plugin and token ids', () => {
-      const tx = processNexchangeTx(
+      const tx = standardizeNexchangeOrder(
         makeRawOrder({ orderId: 'NEX-ABCD1234' }),
         currencyMap
       )
@@ -160,7 +160,7 @@
     ]
     for (const [rawStatus, expected] of statusCases) {
       it(`maps status "${rawStatus}" to "${expected}"`, () => {
-        const tx = processNexchangeTx(
+        const tx = standardizeNexchangeOrder(
           makeRawOrder({ status: rawStatus }),
           currencyMap
         )

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 130f101. Configure here.

Comment thread src/partners/nexchange.ts
export function processNexchangeTx(
rawTx: unknown,
currencyMap: NexchangeCurrencyInfoMap
): StandardTx {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

processNexchangeTx wrong signature

Medium Severity

processNexchangeTx takes a currencyMap argument instead of pluginParams, unlike other partner process*Tx handlers. Backfill tooling is expected to call every processor as (rawTx, pluginParams) without preloading partner-specific caches.

Fix in Cursor Fix in Web

Triggered by learned rule: process*Tx must have uniform signature for backfill scripts

Reviewed by Cursor Bugbot for commit 130f101. Configure here.

Comment thread src/partners/nexchange.ts
// cursor value would re-pin pagination to the wrong position next
// iteration.
cursor = undefined
offset += orders.length
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Offset stale after cursor pages

Medium Severity

When pagination moves from cursor-based requests to offset-based ones, offset only increases by the last page size, not by all orders already fetched while cursoring. The next offset request can repeat or skip orders if the API treats offset as a global index into the full result set.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 130f101. Configure here.

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.

2 participants