Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3dce03f
Feat: nexchange plugin
MMrj9 Feb 26, 2026
98dcb27
Throw instead of pricing tokens as native gas tokens
paullinator May 28, 2026
cb7944e
Add Xgram
paullinator May 29, 2026
0ca9ce9
Infer LetsExchange native network when API omits network fields
paullinator Jun 9, 2026
41287a3
Add missing partner asset and payment mappings
paullinator Jun 23, 2026
1ff3e86
Load full ChangeNow currency list to resolve delisted assets
paullinator Jun 29, 2026
3f9845d
Add Banxa historical fallback for delisted ZEC
paullinator Jul 13, 2026
9d7d8c3
Disable getTxInfo endpoint
paullinator Jul 29, 2026
1b95c0b
Add Banxa Klarna Checkout payment type mapping
paullinator Jul 29, 2026
397ad8a
Fix broken mocha test suite
j0ntz Jul 31, 2026
e2a6e17
Add CI job to run the test suite
j0ntz Jul 31, 2026
0748c28
Add Swapter partner plugin
j0ntz Jul 31, 2026
8955457
Add NYM Swap (nymswap) reporting plugin
j0ntz Jul 31, 2026
1df0e59
Add Revolut fiat payment provider
j0ntz Jul 31, 2026
e429c6e
Add v2 dashboard config endpoint and rev-share rates
j0ntz Jul 31, 2026
42dd971
Add isolated v2 reports dashboard
j0ntz Jul 31, 2026
ee96687
Track partner-reported revenue through to the dashboard
j0ntz Jul 31, 2026
ede909a
Derive v2 provider types from the v1 partner registry
j0ntz Aug 1, 2026
b921135
Document the blocked partner reporting APIs
j0ntz Jul 31, 2026
1912adf
Harden nexchange cleaners and native-asset detection
j0ntz Aug 10, 2026
8afccaf
Fix revenue persistence, v2 day alignment, and key exposure in the URL
j0ntz Aug 10, 2026
dbf7230
Finish the v2 UTC migration and stop logging raw partner payloads
j0ntz Aug 10, 2026
b4e29e1
Bound partner pagination so termination is not the partner's decision
j0ntz Aug 10, 2026
e8f750f
Keep the revenueSource union, and drop em-dashes from committed text
j0ntz Aug 10, 2026
769e244
Quarantine unprocessable partner transactions instead of stalling
j0ntz Aug 11, 2026
df0f788
Report LetsExchange orders whose network cannot be resolved
j0ntz Aug 11, 2026
13ca789
Separate shared failures from per-order ones, and name the dropped rows
j0ntz Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Test
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout the latest code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: 18
- name: Install dependencies
run: npm install --ignore-scripts
- name: Generate client config
run: node -r sucrase/register src/bin/configure.ts
- name: Run tests
run: npm test
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@

## Unreleased

- added: CI job that runs the mocha test suite on every pull request
- added: Add Revolut fiat payment provider
- added: Add Swapter reporting
- added: Add NYM Swap (nymswap) reporting
- added: Isolated v2 reports dashboard at /v2/ (real /v1 API data, apiKey auth with redirect on a bad key)
- added: Partner-reported revenue (revenueUsd/revenueSource) on StandardTx, summed through the analytics cache; Revolut reports it, and the v2 dashboard uses reported figures where present with volume x revShareRate as the estimate elsewhere
- added: Document the verified reporting-API status and unblock path for nexchange, Simplex and Bridgeless
- changed: Update sideshift plugin with new optional API fields
- changed: Query both old and new Sideshift affiliate accounts and merge completed orders to preserve full shift history across an affiliate-account rotation
- changed: Add signature header support to Exolix
- changed: Add index for orderId
- changed: Add EVM chainId, pluginId, and tokenId fields to StandardTx
- changed: Update Lifi to provide chainId, pluginId, and tokenId
- changed: Use rates V3 for transactions with pluginId/tokenId
- fixed: Classify banxa2, banxa3, gebo and Ionia gift cards as fiat in the v2 dashboard filter, by projecting the provider types from the v1 partner registry instead of a second hand-written copy
- fixed: Tolerate unexpected types on nexchange's nullable response fields, and treat a zero contract address as the chain's native asset instead of minting a tokenId for the gas asset
- fixed: Moonpay by adding Revolut payment type
- fixed: Use v2 rates API
- fixed: Repair the broken mocha test suite (correct util.test.ts import and stale analytics fixtures) so npm test passes

## 0.2.0

Expand Down
144 changes: 144 additions & 0 deletions docs/blocked-partner-reporting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Blocked partner reporting APIs

Three partners have no working reporting plugin, and none of them is blocked on
work this repo can do alone. This records what each one actually needs, with the
evidence, so the next person does not repeat the investigation.

Everything below was established by a live request against the partner's own
API, not from documentation. Requests were read-only and used the credentials
already in `edge-react-gui/env.json`. Every claim here is reproducible with the
commands shown.

Last verified 2026-07-31.

## Summary

| Partner | Plugin state | Blocked on | Who unblocks it |
| --- | --- | --- | --- |
| nexchange | Ships here, but unverifiable | API key lacks the reporting scope | nexchange support |
| Simplex | `src/partners/simplex.ts`, wrong host and auth | A server-side reporting key that does not exist yet | Simplex partner support |
| Bridgeless | No plugin | The partner API cannot express an incremental report | Bridgeless engineering |

## nexchange

The plugin ships in this branch, descended from
[#217](https://github.com/EdgeApp/edge-reports-server/pull/217). Two ports of it
existed for a while; this branch carries one copy, so there is nothing left to
deduplicate.

Two fixes found while porting are applied here, and are worth carrying into any
other surviving copy:

- Use `asMaybe` rather than `asOptional(asEither(asString, asNull), null)` on the
nullable response fields. `asOptional` still throws on an unexpected type, so
one odd row aborts the whole page, exhausts the retries and stalls the window.
- Treat a zero contract address (`0x000…0`) as the chain's native asset. Without
it `createTokenId` mints a non-null tokenId for the gas asset and mis-routes
its rates and volume. Banxa and Moonpay special-case this the same way.

The remaining blocker is credentials, and it is independent of whose code lands.
The key in `env.json` (`NEXCHANGE_INIT.apiKey`) authenticates but is not
authorised for the reporting resource:

```
GET https://api.n.exchange/en/api/v1/audit-orders
-> HTTP 403 {"detail":"API key is not authorised for this resource"}
```

A 403 rather than a 401 is the point: the key is recognised, the scope is
missing. There is no self-serve key console.

**Ask:** email support@n.exchange or the account manager for a reporting-scoped
key, and ask them to confirm the exact header and path for audit-orders. The
public v1/v2 OpenAPI specs document `Authorization: ApiKey <key>` and do not
mention `x-api-key` at all, while the ported plugin sends `x-api-key`, so the
header is worth confirming in the same message.

## Simplex

The shipped plugin is wrong in three independent ways, and only the first two can
be fixed without a key.

**Host and path.** `turnkey.api.simplex.com/transactions` is not the reporting
API any more. The current one is `/reporting/v1/payments`, and it is not on the
turnkey host:

```
GET https://turnkey.api.simplex.com/reporting/v1/payments
-> HTTP 403 {"message":"Missing Authentication Token"} # API Gateway for "no such route"

GET https://sandbox.test-simplexcc.com/reporting/v1/payments
-> HTTP 401 {"message":"Authorization header is missing"} # route exists, wants auth
```

**Auth scheme.** The plugin sends `X-API-KEY`. The reporting API ignores that
header entirely and wants `Authorization: ApiKey <key>`. The error text
distinguishes the two cases exactly:

```
-H 'x-api-key: <anything>' -> 401 "Authorization header is missing"
-H 'Authorization: Bearer <anything>' -> 401 "Authorization Apikey has invalid format"
-H 'Authorization: <anything>' -> 401 "Authorization Apikey has invalid format"
-H 'Authorization: ApiKey <anything>' -> 401 "Invalid API key"
```

Only the last one gets past format validation to key validation, which is what
identifies it as the correct scheme.

**Credential.** This is the blocker. `env.json` holds only the publishable
checkout credentials (`PLUGIN_API_KEYS.simplex.publicKey`, a `pk_live_` value,
plus the `jwtTokenProvider` flow). Reporting needs a separate server-side
partner key that Simplex issues on request and locks to a source IP. There is no
dashboard that mints one.

The plugin is not rewritten here because the response body has never been seen.
Writing cleaners for an unobserved payload would be inventing the schema, and a
half-migrated plugin is worse than the current one.

**Ask:** request a server-side reporting ApiKey from Simplex partner support,
supply the reports-server egress IP for their allowlist, and ask for one sample
`/reporting/v1/payments` response. The sample alone unblocks the rewrite; the key
unblocks verification. The IP lock also answers the key-exposure concern that got
Simplex reporting disabled in the first place.

## Bridgeless

Bridgeless is a public Cosmos-SDK L1, so there is no key to obtain. The
unauthenticated LCD at `https://rpc-api.node0.mainnet.bridgeless.com` does carry
Edge's attribution: Edge is referral 2 (`BRIDGELESS_INIT.referralId` in
`env.json`), and the module confirms it:

```
GET /cosmos/bridge/referrals/2
-> {"referral":{"id":2,"withdrawal_address":"bridge1rcu…","commission_rate":"0.66"}}

GET /cosmos/bridge/referrals/rewards/2
-> accrued commission per token, currently 4 tokens with a non-zero to_claim
```

So aggregate commission is readable today. Per-order reporting is not, for three
reasons that compound:

- **No timestamp.** A bridge transaction record carries `deposit_block` and
`deposit_chain_id` but no time of any kind. `StandardTx` requires `isoDate` and
`timestamp`, so every record would need a block lookup on its own origin chain
to be datable. The Cosmos tx index cannot supply it either: bridge state does
not move through user transactions, and sampled blocks contain none.
- **No referral filter.** `/cosmos/bridge/transactions` returns the whole set
(100,210 records) with no server-side filter, so attribution means scanning
everything and filtering `referral_id == 2` client-side.
- **No incremental cursor.** The store is hash-ordered, not append-ordered:
offsets 0, 50,000 and 100,000 return interleaved chains and unrelated block
heights. There is no watermark to resume from, so every run is a full rescan.

Together those make a plugin infeasible rather than merely expensive. A 20,000
record sample (20% of the store) contained exactly **one** record with
`referral_id == 2`, so the current shape would rescan 100,000+ untyped records
per cycle to recover a handful of Edge orders, and still could not date them.

**Ask:** ask Bridgeless engineering for any one of these, in preference order:
a `referral_id` filter plus a timestamp on `/cosmos/bridge/transactions`; an
append-ordered or height-keyed cursor so the scan can resume; or an off-chain
partner reporting endpoint. Also worth asking why on-chain `referral_id == 2` is
so rare while `/referrals/rewards/2` shows real accrued commission, since that
gap suggests attribution is recorded somewhere this endpoint does not expose.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"scripts": {
"build.lib": "sucrase -q -t typescript,imports,jsx -d ./lib ./src",
"build.types": "tsc",
"build.dist": "parcel build",
"build.dist": "parcel build && parcel build src/demoV2/index.html --dist-dir dist/v2 --public-url ./",
"clean": "rimraf lib dist .parcel-cache",
"configure": "configure",
"fix": "npm run lint -- --fix",
Expand All @@ -27,6 +27,7 @@
"stats": "node -r sucrase/register src/bin/partitionStats.ts",
"test": "mocha -r sucrase/register 'test/**/*.test.ts'",
"demo": "parcel serve src/demo/index.html",
"demo.v2": "parcel serve src/demoV2/index.html --dist-dir dist/v2 --public-url ./ -p 1235",
"lint": "eslint --ext .js,.jsx,.ts,.tsx ."
},
"lint-staged": {
Expand Down
7 changes: 7 additions & 0 deletions src/apiAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ interface DbTx {
payoutCurrency: string
timestamp: number
usdValue: number
revenueUsd?: number
}

interface Bucket {
start: number
usdValue: number
numTxs: number
revenueUsd: number
isoDate: string
currencyCodes: { [currencyCode: string]: number }
currencyPairs: { [currencyPair: string]: number }
Expand Down Expand Up @@ -49,6 +51,7 @@ export const getAnalytics = (
isoDate: monthStart.toISOString(),
usdValue: 0,
numTxs: 0,
revenueUsd: 0,
currencyCodes: {},
currencyPairs: {}
})
Expand All @@ -66,6 +69,7 @@ export const getAnalytics = (
isoDate: dayStart.toISOString(),
usdValue: 0,
numTxs: 0,
revenueUsd: 0,
currencyCodes: {},
currencyPairs: {}
})
Expand All @@ -83,6 +87,7 @@ export const getAnalytics = (
isoDate: hourStart.toISOString(),
usdValue: 0,
numTxs: 0,
revenueUsd: 0,
currencyCodes: {},
currencyPairs: {}
})
Expand Down Expand Up @@ -160,6 +165,8 @@ const bucketAdder = (bucket: Bucket, tx: DbTx): void => {
bucket.numTxs++
// usdValue
bucket.usdValue += tx.usdValue != null ? tx.usdValue : 0
// reported revenue (partners that do not report one contribute 0)
bucket.revenueUsd += tx.revenueUsd != null ? tx.revenueUsd : 0
// currencyCode
if (bucket.currencyCodes[tx.depositCurrency] == null) {
bucket.currencyCodes[tx.depositCurrency] = 0
Expand Down
4 changes: 3 additions & 1 deletion src/cacheEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ export async function cacheEngine(): Promise<void> {
'depositCurrency',
'payoutCurrency',
'timestamp',
'usdValue'
'usdValue',
'revenueUsd'
],
use_index: 'timestamp-p',
sort: ['timestamp'],
Expand Down Expand Up @@ -145,6 +146,7 @@ export async function cacheEngine(): Promise<void> {
timestamp: bucket.start,
usdValue: bucket.usdValue,
numTxs: bucket.numTxs,
revenueUsd: bucket.revenueUsd,
currencyCodes: bucket.currencyCodes,
currencyPairs: bucket.currencyPairs
}
Expand Down
6 changes: 4 additions & 2 deletions src/dbutils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { asArray, asNumber, asObject, asString } from 'cleaners'
import { asArray, asNumber, asObject, asOptional, asString } from 'cleaners'
import nano from 'nano'

import { config } from './config'
Expand All @@ -15,7 +15,8 @@ export const asDbReq = asObject({
depositCurrency: asString,
payoutCurrency: asString,
timestamp: asNumber,
usdValue: asNumber
usdValue: asNumber,
revenueUsd: asOptional(asNumber)
})
)
})
Expand Down Expand Up @@ -106,6 +107,7 @@ export const cacheAnalytic = async (
start: cacheObj.timestamp,
usdValue: cacheObj.usdValue,
numTxs: cacheObj.numTxs,
revenueUsd: cacheObj.revenueUsd,
isoDate: new Date(cacheObj.timestamp).toISOString(),
currencyCodes: cacheObj.currencyCodes,
currencyPairs: cacheObj.currencyPairs
Expand Down
1 change: 1 addition & 0 deletions src/demo/clientUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export const createQuarterBuckets = (analytics: AnalyticsResult): Bucket[] => {
start: realTimestamp / 1000,
usdValue: 0,
numTxs: 0,
revenueUsd: undefined,
isoDate: new Date(realTimestamp).toISOString(),
currencyCodes: {},
currencyPairs: {}
Expand Down
20 changes: 20 additions & 0 deletions src/demo/partners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ export default {
type: 'fiat',
color: '#7214F5'
},
nexchange: {
type: 'swap',
color: '#1D31B6'
},
nymswap: {
type: 'swap',
color: '#FB6E4E'
},
paybis: {
type: 'fiat',
color: '#FFB400'
Expand All @@ -109,6 +117,10 @@ export default {
type: 'swap',
color: '#5891EE'
},
revolut: {
type: 'fiat',
color: '#191C33'
},
safello: {
type: 'fiat',
color: deprecated
Expand All @@ -121,6 +133,10 @@ export default {
type: 'swap',
color: '#E35852'
},
swapter: {
type: 'swap',
color: '#00C9A7'
},
swapuz: {
type: 'swap',
color: '#56BD7C'
Expand Down Expand Up @@ -152,5 +168,9 @@ export default {
xanpool: {
type: 'fiat',
color: '#46228B'
},
xgram: {
type: 'swap',
color: '#0FA7B1'
}
} as const
Loading
Loading