Feat/nexchange plugin#217
Conversation
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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
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.
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.
| export function processNexchangeTx( | ||
| rawTx: unknown, | ||
| currencyMap: NexchangeCurrencyInfoMap | ||
| ): StandardTx { |
There was a problem hiding this comment.
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.
Triggered by learned rule: process*Tx must have uniform signature for backfill scripts
Reviewed by Cursor Bugbot for commit 130f101. Configure here.
| // cursor value would re-pin pagination to the wrong position next | ||
| // iteration. | ||
| cursor = undefined | ||
| offset += orders.length |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 130f101. Configure here.



CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
noneDescription
noneNote
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
createTokenIdfailures 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.