Skip to content

Batch reports-server umbrella: fix test suite + CI and add Revolut, Swapter, NYM, nexchange plugins - #228

Open
j0ntz wants to merge 11 commits into
masterfrom
jon/reports-server-umbrella
Open

Batch reports-server umbrella: fix test suite + CI and add Revolut, Swapter, NYM, nexchange plugins#228
j0ntz wants to merge 11 commits into
masterfrom
jon/reports-server-umbrella

Conversation

@j0ntz

@j0ntz j0ntz commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Batched umbrella PR consolidating all open edge-reports-server subtask work into one branch
off current master, per the umbrella task
reports-server: Umbrella.

Order matters: the broken test suite is fixed first (everything else depends on npm test
passing), then each provider lands as its own commit.

Commits (one concern per commit)

  1. Fix broken mocha test suite - test/util.test.ts imported three utils from the gitignored
    compiled ../lib/util; repointed to ../src/demo/clientUtil (where they actually live) and
    renamed 4 stale pluginIdpartnerId fixture keys in testData.json. npm test was fully
    broken on master (module-not-found); now 71 passing, 1 pending.
  2. Add CI job to run the test suite - new .github/workflows/test.yml generates the gitignored
    clientConfig.json (configure.ts) then runs npm test on every PR. No test job existed before,
    so pr-land's npm test gate was un-guarded.
  3. Add Revolut fiat payment provider - recreated under Edge credentials from partner PR Add Revolut fiat payment provider #211.
  4. Add Swapter reporting - recreated under Edge credentials from partner PR Swapter plugin integration #219.
  5. Add NYM Swap (nymswap) reporting - new plugin, reconciled to the live NYM partner reporting API.
  6. Add nexchange partner plugin - the clean plugin subset of partner PRs Feat/nexchange plugin #217/Feat: nexchange plugin #212 (excludes the
    unrelated changenow/rango/sideshift refactors those PRs bundled).

Plus review-hardening commits addressing 12 cursor reviewer-bot findings (High to Low): swapter
idempotent-retry buffering + progress guard + tolerant cleaners + demo entry + in-flight status
mapping + frozen pagination window; nexchange tolerant cleaners + zero-address native handling;
nym silent-skip on missing key + sanitized test fixtures; revolut cursor encoding + tolerant state
pre-filter. All CI checks and both cursor reviewers pass on HEAD with zero unresolved threads.

Supersedes (close on merge)

This PR replaces these Edge-owned PRs - close them with a pointer here and delete their branches:
#225 (test suite + CI),
#227 (Revolut),
#224 (Swapter),
#226 (NYM),
and the nexchange code originates from #217 /
#212.

Partner-owned fork PRs are lineage only, do NOT delete their branches:
#211 (claudinethelobster, Revolut),
#219 (markovo4, Swapter),
#214 (n.exchange, nexchange).

Requirements audit (vs edge-exchange-plugins docs/API_REQUIREMENTS.md)

Lens: §6 (Reporting API) primary; §1 (chain/token id), §2 (order id), §5 (status set), §10 (fiat/region) where touched. StandardTx field set per src/types.ts. Only nexchange and NYM are live-validated against real reporting data this run; Revolut and Simplex are blocked on human-provisioned credentials; Swapter is validated on a throwaway key; Bridgeless has no API.

Provider (pluginId) Incremental query Required fields Settlement date Unit convention Addr / txid / chainId Fiat fields Status mapping Functional status
NYM (nymswap) PASS (startDate+cursor+limit) PASS GAP (partner) PASS (native→major, documented) PASS / PASS / GAP (ours) N/A PASS WORKING (live, 29 tx)
Swapter (swapter) PASS (timeFrom/timeTo+page) GAP (no txids) GAP (ours, unused time.success) GAP (partner) PASS / GAP / GAP N/A PASS WORKING (throwaway key, 5 tx)
nexchange (nexchange) PASS (dateFrom+limit+cursor) PASS GAP (partner) GAP (partner) PASS / PASS / PASS N/A PASS CODE-READY, 403 unauthorised
Revolut (revolut) PASS (from/to+limit+cursor) PASS GAP-minor GAP (partner) PASS / PASS / GAP (partner) PASS GAP (completed-only) CODE-COMPLETE, 404 not provisioned
Simplex (simplex) GAP (no server date range) GAP (no addr/txid) GAP (partner) GAP (partner) GAP / GAP / GAP GAP (buy-only) GAP (approved-only) FINDING-ONLY (endpoint alive, no key)
Bridgeless (BTCx) N/A N/A N/A N/A N/A N/A N/A BLOCKED - NO REPORTING API

Highlights per provider (full field-by-field tables + copy-paste post-merge setup blocks + cleanup ledger are in the consolidated-audit doc attached to the Asana task):

  • NYM - §6 clean. Two ours-to-fix gaps: processNymTx drops the API's sourceTokenId/destinationTokenId and sourceNetwork (no chainPluginId/evmChainId enrichment); pricing rides currency code. completedDate null on settled orders is a partner-ask (plugin correctly keys off createdDate).
  • Swapter - §6 clean. time.success is returned but unused (ours-to-fix, could adopt for settled orders); no deposit/withdraw txids in the API (partner-ask); deposit.network/withdraw.network unmapped.
  • nexchange - strongest chain/token enrichment of the swaps (catalog-driven chainPluginId+tokenId+evmChainId, throws rather than mis-price). Blocked only on an affiliate-authorized reporting key (GUI key 403s on the resource).
  • Revolut - fiat fields (countryCode, direction, paymentType) all present. Two ours-to-fix: getRevolutPaymentType THROWS on an unmapped payment_method (one novel value aborts a tx - should degrade to null); completed-only reporting means pending/refunded volume never surfaces. No crypto network field from the API (partner-ask).
  • Simplex - existing plugin has the most gaps: starting_at=0 hardcoded (no server-side date range, walks all history client-side), legacy settings.lastTimestamp not latestIsoDate, approved-only + buy-only, no addresses/txids/paymentType. Re-enabling is gated on Edge resolving the prior API-key-exposure issue (business + security decision, not just a couch write).
  • Bridgeless - every §6 field N/A; a reports plugin would need an on-chain indexer keyed on referralId=2 over the bridge module, out of scope for a couch-key plugin. Partner-ask: request an off-chain order-listing API.

Cross-cutting: every couch plugin returns [] silently on a null/absent apiKey, so post-deploy "no rows" is indistinguishable from "not wired" without checking pm2 logs - each setup block's verify step accounts for this.

Not shipped (findings, no code)

  • Bridgeless (BTCx) - a DEX (edge-exchange-plugins/src/swap/defi/bridgeless.ts, isDex: true)
    whose only endpoints are on-chain bridge RPC and a single-order status check. No order-listing /
    partner reporting API exists; the referralId (uint16 = 2 for Edge) is on-chain attribution only.
    A reports plugin needs an on-chain indexer keyed on that referralId; not implementable against a
    partner API. No plugin shipped.
  • Simplex - the existing src/partners/simplex.ts endpoint turnkey.api.simplex.com/transactions
    is still alive (HTTP 403 with a bogus key this run). Simplex was disabled by Edge over API-key
    exposure (last volume ~May 7) and env.json carries no plain reporting apiKey (only publishable
    publicKey + jwtTokenProvider). No credential to confirm data flow; no new-API rewrite shipped.

Testing

  • npm test: 71 passing, 1 pending. npm run build.types (tsc): clean. eslint: clean.
  • Live reporting-API calls this run (real env.json GUI credentials, read-only, never prod couch):
    • NYM: 29 real StandardTx (11 complete, 18 expired) - GUI key authenticates. WORKING.
    • Swapter: 5 real StandardTx (1 complete ETH→LTC, usdValue 30.05) - processSwapterTx ran on
      real rawTx. WORKING (throwaway account).
    • nexchange: HTTP 403 "API key is not authorised for this resource" - GUI key recognized but
      not authorized for the audit-orders resource. Code ready; needs a partner-issued reporting key.
    • Revolut: api.revolut.com/partner/v1/transactions → 404 (key not provisioned); GUI ramp host
      → 403. Code ready; needs a prod Revolut reporting key.

Note

Medium Risk
New partner ingestion paths affect reported volume and USD pricing (nexchange deliberately throws on mis-mapped tokens; Revolut can abort a row on an unknown payment_method), but changes are additive with progress/dedup safeguards rather than core auth or payment execution.

Overview
Repairs npm test by fixing test/util.test.ts imports and renaming stale pluginId keys to partnerId in analytics fixtures, and adds a GitHub Actions workflow that generates clientConfig.json then runs the mocha suite on every PR.

Registers four new partner reporting plugins in the query engine and demo partner list: Revolut (fiat buy/sell with time-window + cursor pagination), Swapter (swap history POST API), NYM (nymswap) (native-unit amounts → major units via /currencies), and nexchange (audit orders plus v2 currency catalog for chainPluginId / tokenId / evmChainId enrichment). Each plugin follows the same couch pattern: silent no-op without apiKey, lookback re-query, buffered pages/windows for idempotent retries, and guarded latestIsoDate advancement when pagination does not finish.

Adds focused unit tests for nexchange (asset resolution, dates, status mapping) and NYM (processNymTx amount/status behavior).

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

Comment thread test/nym.test.ts Outdated
Comment thread src/demo/partners.ts
Comment thread src/partners/swapter.ts
Comment thread src/partners/swapter.ts
Comment thread src/partners/nexchange.ts
Comment thread src/partners/swapter.ts
Comment thread src/partners/nym.ts
Comment thread src/partners/revolut.ts Outdated
Comment thread src/partners/nexchange.ts
Comment thread src/partners/swapter.ts Outdated
Comment thread src/partners/revolut.ts
Comment thread src/partners/swapter.ts
j0ntz added 11 commits July 30, 2026 13:59
util.test.ts imported createQuarterBuckets, movingAveDataSort, and
sevenDayDataMerge from '../lib/util', which does not exist (lib/ is
gitignored build output). The functions live in src/demo/clientUtil.ts,
so the import resolved to undefined and crashed the whole suite at load.
Point the import at '../src/demo/clientUtil', matching the repo
convention of importing source directly under sucrase/register.

The analytics fixtures (outputOne..Four in testData.json) still expected
the stale keys 'appId' and 'pluginId', but getAnalytics and the
asAnalyticsResult cleaner both use 'app' and 'partnerId'. Rename the
fixture keys so they match the type contract and the function output.
The repo had no CI job that runs the tests, so PRs showed green while
npm test failed. Add a GitHub Actions workflow that runs the mocha suite
on every pull request.

The suite loads src/demo/clientUtil.ts, which imports clientConfig.json
(gitignored local dev config). The workflow generates it with defaults
via the existing configure.ts (the same step scripts/prepare.sh runs)
before running the tests, so the job is self-contained without needing
the committed config.
Recreate the Revolut partner plugin following the moonpay pattern:
- src/partners/revolut.ts queries the Revolut partner transactions API,
  cleans results, and maps completed buy/sell txs to StandardTx.
- Register the plugin in queryEngine.ts.
- Add the revolut demo entry (fiat, #191C33).
Adds the Swapter exchange partner integration: querySwapter pages the
/personal/exchange/tool-history endpoint with retry handling and
latestIsoDate tracking, and processSwapterTx converts Swapter deposit/
withdraw records into StandardTx. Registers the swapter plugin in the
query engine.
Query NYM's partner reporting API for completed swap orders and map them
into StandardTx, modeled on the clean shape of swapuz. Register nymswap in
queryEngine and the demo partners config. Add a transform unit test.

The reporting endpoint, auth scheme, and order field names are the
documented assumption to confirm with NYM; credentials are human/ops set.
Port the nexchange reporting plugin (queries https://api.n.exchange audit-orders
endpoint with the x-api-key header) from partner PR #217/#212, without the unrelated
changenow/rango/sideshift refactors bundled in those PRs. Wire it into queryEngine and
the demo partner list.
- swapter: buffer each page and only advance latestIsoDate on clean completion, so
  a mid-page throw no longer re-appends rows (duplicate orderIds / Couch _id conflicts)
  and an error-driven exit keeps the pre-run progress marker instead of skipping older
  unfetched pages (High + Medium).
- swapter: relax strict cleaners on fields that never feed StandardTx (info.type/link,
  deposit/withdraw.network, the partner block) to asMaybe so an unexpected encoding
  degrades that field instead of aborting the page (Medium).
- swapter: add the missing demo partners.ts entry so getPartnerIds surfaces it (Medium).
- nexchange: switch non-critical cleaners (address, txid, countryCode, nextCursor,
  network, contract_address) from asOptional to asMaybe so a single odd encoding
  degrades that field rather than aborting the query block (Medium).
- nym: make apiKey optional and return [] on a null/empty key so an unprovisioned
  partner entry no-ops like the other couch plugins instead of erroring every cycle (Medium).
- nym test: replace live-derived addresses and txids with clearly-synthetic placeholders;
  only field structure and amount math are load-bearing (High, privacy).
- revolut: percent-encode the pagination cursor so an opaque next_cursor
  containing + or / does not break paging after the first page (Medium).
- nexchange: treat a zero contract address (0x000...0) as the chain's native
  asset instead of minting a tokenId for the gas asset, which would mis-route
  its rates and volume (Medium, matches Banxa/Moonpay).
Confirmation, Exchanging, and Sending are in-flight phases; map them to
'processing' (matching the sibling swap plugins) instead of 'pending', so
active exchange volume is not under-reported in status-aware views.
asPreRevolutTx.state is read only to pre-filter for completed orders, so make
it asMaybe: a strict cleaner aborted the whole page on one odd row (including
pending/refunded rows that would be skipped anyway), exhausting retries and
stalling the window so the same bad row was re-hit every cycle.
Compute timeTo once before the paging loop instead of per request, so orders
arriving mid-walk cannot shift the window and page boundaries and skip or
duplicate rows before completion is decided.
@j0ntz
j0ntz force-pushed the jon/reports-server-umbrella branch from b26a08c to e522418 Compare July 30, 2026 20:59

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e522418. 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.

Nexchange processTx signature mismatch

Medium Severity

processNexchangeTx takes a currencyMap instead of pluginParams, so it does not match the uniform (rawTx, pluginParams) shape used by sibling plugins and required for upcoming backfill callers. A backfill invoking it with pluginParams would treat that object as the catalog and drop all chain/token enrichment.

Additional Locations (1)
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 e522418. Configure here.

Comment thread src/partners/nym.ts
export function processNymTx(
rawTx: unknown,
decimals: DecimalsMap = {}
): 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.

NYM processTx signature mismatch

Medium Severity

processNymTx takes a decimals map instead of pluginParams, so it breaks the uniform (rawTx, pluginParams) processor contract. Backfill callers that pass pluginParams would not get a real decimals lookup and would fall back to incomplete defaults or throw for newer assets.

Additional Locations (1)
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 e522418. Configure here.

Comment thread src/partners/nym.ts
} catch (e) {
log.error(`Skipping unparseable transaction: ${String(e)}`)
continue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

NYM silently skips bad orders

High Severity

Per-order try/catch around processNymTx logs and continues on failure. Unparseable or unpriceable orders are dropped while pagination can still complete and advance latestIsoDate, so those orders can fall outside the lookback window and never be retried.

Fix in Cursor Fix in Web

Triggered by learned rule: Throw-on-unknown enum values is intentional in partner plugins

Reviewed by Cursor Bugbot for commit e522418. 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.

1 participant