diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ef4adf6..9c545c52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - 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 - 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 diff --git a/package.json b/package.json index 66f75176..2fe2c399 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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": { diff --git a/src/apiAnalytics.ts b/src/apiAnalytics.ts index 117b4195..6743b067 100644 --- a/src/apiAnalytics.ts +++ b/src/apiAnalytics.ts @@ -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 } @@ -49,6 +51,7 @@ export const getAnalytics = ( isoDate: monthStart.toISOString(), usdValue: 0, numTxs: 0, + revenueUsd: 0, currencyCodes: {}, currencyPairs: {} }) @@ -66,6 +69,7 @@ export const getAnalytics = ( isoDate: dayStart.toISOString(), usdValue: 0, numTxs: 0, + revenueUsd: 0, currencyCodes: {}, currencyPairs: {} }) @@ -83,6 +87,7 @@ export const getAnalytics = ( isoDate: hourStart.toISOString(), usdValue: 0, numTxs: 0, + revenueUsd: 0, currencyCodes: {}, currencyPairs: {} }) @@ -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 diff --git a/src/cacheEngine.ts b/src/cacheEngine.ts index ab260f77..abe966b4 100644 --- a/src/cacheEngine.ts +++ b/src/cacheEngine.ts @@ -101,7 +101,8 @@ export async function cacheEngine(): Promise { 'depositCurrency', 'payoutCurrency', 'timestamp', - 'usdValue' + 'usdValue', + 'revenueUsd' ], use_index: 'timestamp-p', sort: ['timestamp'], @@ -145,6 +146,7 @@ export async function cacheEngine(): Promise { timestamp: bucket.start, usdValue: bucket.usdValue, numTxs: bucket.numTxs, + revenueUsd: bucket.revenueUsd, currencyCodes: bucket.currencyCodes, currencyPairs: bucket.currencyPairs } diff --git a/src/dbutils.ts b/src/dbutils.ts index e59f9a66..79c949a4 100644 --- a/src/dbutils.ts +++ b/src/dbutils.ts @@ -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' @@ -15,7 +15,8 @@ export const asDbReq = asObject({ depositCurrency: asString, payoutCurrency: asString, timestamp: asNumber, - usdValue: asNumber + usdValue: asNumber, + revenueUsd: asOptional(asNumber) }) ) }) @@ -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 diff --git a/src/demo/clientUtil.ts b/src/demo/clientUtil.ts index 5ddf1477..d1abac14 100644 --- a/src/demo/clientUtil.ts +++ b/src/demo/clientUtil.ts @@ -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: {} diff --git a/src/demoV2/README.md b/src/demoV2/README.md new file mode 100644 index 00000000..bec5b013 --- /dev/null +++ b/src/demoV2/README.md @@ -0,0 +1,88 @@ +# Edge Reports v2 dashboard + +An isolated redesign of the reports dashboard, served at `/v2/`. It is fully +separate from the v1 demo (`src/demo`): its own entry point, its own parcel +bundle (`dist/v2/`), and its own URL. v1 routes, components and build output are +untouched. Where behavior overlaps, code is duplicated into v2 rather than +refactoring anything v1 depends on. + +## What it is + +The full rendering and interaction engine is ported from the design prototype: +one summary card, a Providers section and a Currency pairs section, each with a +trend chart (stacked bars or lines), a share card (donut + ranked bars, hover +linked), and a detail table (the pair table paginated). Global filters (range, +type, providers, pairs) scope every card. Top-8-plus-Other color rules keep the +chart, share card and table in agreement. + +The only thing that changed from the prototype is the data source: the baked-in +sample generator is replaced by the real `/v1` reporting API. + +## Data flow + +- `GET /v1/getAppId?apiKey=` — validates the key. Returns plain text on a bad + key (400); v2 checks the response before parsing and redirects to the + key-entry screen instead of hanging (the v1 spinner-forever bug). +- `GET /v2/config?apiKey=` — per-provider rev-share rates and fiat/swap + classification. Isolated v2 route; no v1 route touched. +- `GET /v1/getPluginIds?apiKey=` — the app's registered providers. +- `POST /v1/analytics` — one call for all providers, `timePeriod: "day"`, last + 24 months. v2 rebuckets to month client-side for the longer presets. + +Auth reuses v1's simple apiKey model: the key lives in the `apiKey` cookie (a +`?apiKey=` query param also seeds it). No new auth system. + +### Est. revenue + +Where the partner's API reports Edge's actual fee per order (e.g. Revolut's +`partner_fee`, pre-converted to USD), the plugin stores it on the transaction as +`revenueUsd` with `revenueSource: 'reported'`, the cache engine sums it into the +analytics buckets, and the dashboard uses it directly, marked with a check in +the provider table. That figure is a fact about the order and never recomputed. + +For partners that report no fee, revenue is estimated at read time as +`volume * revShareRate`. The rate lives on the app doc in `reports_apps`, as an +optional `revShareRate` beside that partner's `apiKeys`: + +```json +"partnerIds": { + "moonpay": { "apiKeys": { "apiKey": "..." }, "revShareRate": 0.008 } +} +``` + +Per app and per partner, because the rate is a property of the deal. Estimating +at read time rather than at ingest means correcting a rate fixes history +immediately, while reported figures stay immutable. The rates are deliberately +not in source or `config.json`: they are commercial terms and this repo is +public. A partner with neither reported fees nor a rate contributes 0. + +## Local testing + +Run against a local CouchDB (never point at production). + +```bash +# 1) Build v1 + v2 bundles (v2 lands in dist/v2/). +npm run build.dist + +# 2) Serve the API + built dashboards. +npm run start.api # http://localhost:8008 + +# 3) Open the dashboard and enter an API key registered in reports_apps. +open http://localhost:8008/v2/ +``` + +The dashboard needs an app registered in the `reports_apps` database: a document +whose `_id` is the API key, with an `appId` and a `partnerIds` map. That is what +`getAppId`, `getPluginIds` and `validateApiKey` read. Populate transaction data +with the normal engines (`npm run start` to query partners, `npm run start.cache` +to build the hour/day/month cache buckets that `/v1/analytics` serves). + +For live-reload development of the v2 UI only: `npm run demo.v2` (Parcel dev +server on :1235). Point it at a running API via same-origin or a proxy; the +`build.dist` + `start.api` path above is the end-to-end test. + +Partner credentials belong in the `reports_apps` document, not in `config.json` +and not in any tracked file. Writing real keys into production is a human ops +step. A null apiKey makes a partner plugin skip silently and return no rows, so +"no data" and "never wired" look the same; check the key is populated before +concluding a plugin is broken. diff --git a/src/demoV2/index.html b/src/demoV2/index.html new file mode 100644 index 00000000..3ee2ce7e --- /dev/null +++ b/src/demoV2/index.html @@ -0,0 +1,1561 @@ + + +Edge Reports v2 + + + +
+
+ +

Edge Reports

+

Enter your API key to view the dashboard.

+ + + +
+
+
+ + +
Loading reports…
+ +
+
+
+ +
+

Edge Reports

+
v2 dashboard
+
+
+ + +
+ + +
+
+ +
+
+
+ + + +
+
+ +
+ +
+ + +
+
+
+
+
+ +
+ +
+ + +
+
+
+
+
+ +
+ + +
+
+
+ + +
+

Providers

+
+
+ +
+
+

Volume by provider

+
+
+ + + +
+
+ + +
+ +
+
+ +
+
+
+
+ +
+
+

Provider share

+ of volume in range · same top 8 + other as the chart above +
+ +
+ +
+ +
+
+

Provider detail

+ +
+ +
+
+
+
+
+ + +
+

Currency pairs

+
+
+ +
+
+

Volume by pair

+ across selected providers +
+
+ + +
+ +
+
+ +
+
+
+
+ +
+
+

Pair share

+ of volume in range · same top 8 + other as the chart above +
+ +
+ +
+ +
+
+

Pair detail

+ +
+ +
+
+
+
+
+
+ +
+

What changed vs the current site, and why

+
    +
  • Two entity groups, one top-level filter row, every filter scopes every card. The page is grouped into Providers and Currency pairs, and all four filters (range, type, providers, pairs) apply to the whole page including the summary. One data caveat: the API records volume per pair but not transactions per pair, so under a pair filter each provider's txs (and avg tx) are estimated by scaling its tx count by that pair share of its volume.
  • +
  • Revenue is a first-class metric. The current site only shows gross volume. Where the partner's API reports Edge's actual fee (e.g. Revolut), buckets carry the summed reported revenue and the dashboard uses it directly, marked with a check. Everywhere else revenue is estimated as volume times the app doc's per-partner rev-share rate.
  • +
  • Trend + share pairing. Each group gets a time-series card (stacked bars or lines per entity), a share card (donut for at-a-glance proportion, ranked horizontal bars for exact comparison), and a detail table (the pair table is paginated with a rows-per-page control). No dual-axis charts anywhere; the metric toggle swaps the whole view instead.
  • +
  • Hover to focus, filter only up top. Legend pills highlight their series on hover (everything else dims) instead of mutating the global filter; the donut and ranked bars in each share card are hover-linked (a segment pops out and its row highlights, and vice versa). Share cards show exactly the same top 8 + Other split as their trend chart, and every trend and share card has its own local "include other" checkbox that hides the folded tail from that card only (share totals and percentages recompute over what is shown).
  • +
  • Number formatting. Compact in tiles, axes and bars ($40.7M), full precision in tooltips and the table. Clean rounded axis ticks.
  • +
  • Color discipline. 8 CVD-validated series colors per group, assigned to the top 8 entities of the current view (its range, filters and metric), so the trend chart, share card and detail table always agree on who is "Other". The tail folds into gray. The 40-swatch legend with five near-black providers goes away.
  • +
  • Range selection. One preset dropdown (rows, not a calendar grid) with a custom range in the footer; granularity picks itself from range length.
  • +
  • The mini-chart wall becomes a sortable detail table with share bars, sparklines and avg tx size, and doubles as the accessible table view. Inactive providers hidden behind a toggle.
  • +
+

Data source

+
    +
  • Live data from the /v1 reporting API: getAppId (apiKey auth), getPluginIds (provider list) and one analytics POST (daily buckets, last 24 months) for all providers at once. Revenue uses the bucket's partner-reported revenueUsd when the provider reports one, and otherwise is estimated client-side from the per-partner rev-share rates on the app doc (served by /v2/config).
  • +
  • A bad or missing API key now redirects to the key-entry screen instead of hanging: getAppId returns plain text on 400, so the response is checked before parsing and fetch errors are surfaced.
  • +
  • Pair transactions are not recorded by the API (only pair volume), so under a pair filter each provider's txs and avg tx are estimated by scaling its tx count by that pair's share of its volume.
  • +
+
+
+
+ + diff --git a/src/indexApi.ts b/src/indexApi.ts index 76fe4e9d..391ff73e 100644 --- a/src/indexApi.ts +++ b/src/indexApi.ts @@ -9,6 +9,7 @@ import { checkTxsRouter } from './routes/v1/checkTxs' import { getAppIdRouter } from './routes/v1/getAppId' import { getPluginIdsRouter } from './routes/v1/getPluginIds' import { getTxInfoRouter } from './routes/v1/getTxInfo' +import { getConfigRouter } from './routes/v2/getConfig' import { HttpError } from './util/httpErrors' export const nanoDb = nano(config.couchDbFullpath) @@ -30,6 +31,11 @@ async function main(): Promise { app.use('/v1/getPluginIds/', getPluginIdsRouter) app.use('/v1/getTxInfo/', getTxInfoRouter) + // v2 dashboard config (isolated; v1 routes untouched). The static handler + // above serves the built /v2/ bundle from dist; this API path falls through + // it because no matching file exists in dist. + app.use('/v2/config/', getConfigRouter) + // Error router app.use(function(err, _req, res, _next) { console.error(err.stack) diff --git a/src/partners/revolut.ts b/src/partners/revolut.ts index 23625746..ec7d79ca 100644 --- a/src/partners/revolut.ts +++ b/src/partners/revolut.ts @@ -84,7 +84,15 @@ const asRevolutOrder = asObject({ status: asRevolutStatus, payment: asMaybe(asString), wallet: asMaybe(asString), - transaction_hash: asMaybe(asString) + transaction_hash: asMaybe(asString), + // Edge's actual cut, pre-converted by Revolut to the partner's settlement + // currency (USD across the full live history). asMaybe throughout: FAILED + // rows commonly omit the whole block. + fees_partner_currency: asMaybe( + asObject({ + partner_fee: asMaybe(asRevolutAmount) + }) + ) }) type RevolutOrder = ReturnType @@ -333,6 +341,17 @@ export function processRevolutTx( const { isoDate, timestamp } = smartIsoDateFromTimestamp(tx.created_at) const payout = resolveRevolutAsset(tx.crypto.currencyId) + // Actual revenue, only when Revolut reports it in USD and the order settled: + // an unsettled attempt's fee is not revenue, and a non-USD settlement + // currency would need a conversion this plugin cannot do honestly. + const partnerFee = tx.fees_partner_currency?.partner_fee + const revenueUsd = + tx.status === 'COMPLETED' && + partnerFee != null && + partnerFee.currency === 'USD' + ? partnerFee.amount + : undefined + // Revolut Ramp only reports on-ramp orders, so fiat is always the deposit // side and crypto always the payout side. `wallet` is the user's receiving // address and `transaction_hash` the payout transaction. @@ -360,6 +379,8 @@ export function processRevolutTx( timestamp, isoDate, usdValue: -1, + revenueUsd, + revenueSource: revenueUsd != null ? 'reported' : undefined, rawTx } } diff --git a/src/routes/v1/getTxInfo.ts b/src/routes/v1/getTxInfo.ts index a1c8ba41..94658890 100644 --- a/src/routes/v1/getTxInfo.ts +++ b/src/routes/v1/getTxInfo.ts @@ -92,7 +92,10 @@ getTxInfoRouter.get('/', async function(req, res) { const rows = results.docs .map(doc => asMaybe(asDbTx)(doc)) - .filter((item): item is DbTx => item != null) + // Narrow on the cleaner's own return type: DbTx's revenue keys are + // optional-key relaxed, so it is no longer a subtype of that return type + // and cannot serve as the predicate target. + .filter((item): item is NonNullable => item != null) const txs: TxInfo[] = rows.map(row => ({ providerId: getProviderId(row), diff --git a/src/routes/v2/getConfig.ts b/src/routes/v2/getConfig.ts new file mode 100644 index 00000000..aa8cd437 --- /dev/null +++ b/src/routes/v2/getConfig.ts @@ -0,0 +1,111 @@ +import { asMap, asMaybe, asNumber, asObject, asOptional } from 'cleaners' +import Router from 'express-promise-router' + +import { reportsApps } from '../../indexApi' + +/** + * Rev-share rates live on the app doc in `reports_apps`, as an optional + * `revShareRate` beside each partner's `apiKeys`. The rate is a property of the + * app-partner deal, so it is per app AND per partner, and it sits with the + * credentials that define the relationship: onboarding a partner is one doc + * edit, with no separate rates map to forget. The rates are commercial terms + * and this repo is public, so they are never committed or placed in + * config.json. A partner without one contributes 0 estimated revenue. + * + * Tolerant cleaner: only the fields this route consumes, and a malformed + * partner entry degrades to no rate rather than failing the whole request. + */ +const asAppRevShareRates = asObject({ + partnerIds: asMap(asMaybe(asObject({ revShareRate: asOptional(asNumber) }))) +}) + +const getRevShareRates = async ( + apiKey: string +): Promise<{ [partnerId: string]: number }> => { + // The app doc's _id IS the apiKey (same lookup validateApiKey uses), so a + // missing doc doubles as auth failure and is thrown to the caller. + const doc = await reportsApps.get(apiKey) + const { partnerIds } = asAppRevShareRates(doc) + const rates: { [partnerId: string]: number } = {} + for (const partnerId of Object.keys(partnerIds)) { + const rate = partnerIds[partnerId]?.revShareRate + if (rate != null) rates[partnerId] = rate + } + return rates +} + +/** + * Static fiat/swap classification per pluginId, duplicated into v2 so the + * dashboard can offer its fiat-buy/sell vs swap filter. The reporting API does + * not expose exchange type. Derived from src/partners/* `exchangeType`, plus the + * newer ramp/swap plugins not yet in that registry. Unknown pluginIds default to + * 'swap' on the client. + */ +const providerTypes: { [pluginId: string]: 'fiat' | 'swap' } = { + // fiat (on/off ramp) + banxa: 'fiat', + bitaccess: 'fiat', + bitrefill: 'fiat', + bitsofgold: 'fiat', + bity: 'fiat', + ioniagiftcard: 'fiat', + ioniavisarewards: 'fiat', + kado: 'fiat', + libertyx: 'fiat', + moonpay: 'fiat', + paybis: 'fiat', + paytrie: 'fiat', + revolut: 'fiat', + safello: 'fiat', + simplex: 'fiat', + transak: 'fiat', + wyre: 'fiat', + xanpool: 'fiat', + // swap + bridgeless: 'swap', + changehero: 'swap', + changelly: 'swap', + changenow: 'swap', + coinswitch: 'swap', + exolix: 'swap', + faast: 'swap', + foxexchange: 'swap', + godex: 'swap', + letsexchange: 'swap', + lifi: 'swap', + maya: 'swap', + nym: 'swap', + rango: 'swap', + shapeshift: 'swap', + sideshift: 'swap', + swapter: 'swap', + swapuz: 'swap', + switchain: 'swap', + thorchain: 'swap', + totle: 'swap' +} + +export const getConfigRouter = Router() + +getConfigRouter.get('/', async function(req, res) { + const apiKey = req.query.apiKey + if (typeof apiKey !== 'string' || apiKey === '') { + res.status(400).send(`Missing Request fields.`) + return + } + + // Same auth as v1: the app doc keyed by apiKey. An unrecognized key 401s so + // the v2 client redirects to its key-entry screen instead of hanging. + let revShareRates: { [partnerId: string]: number } + try { + revShareRates = await getRevShareRates(apiKey) + } catch { + res.status(401).send(`Invalid API Key`) + return + } + + res.json({ + revShareRates, + providerTypes + }) +}) diff --git a/src/types.ts b/src/types.ts index 9f8d0582..946dadb9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -148,6 +148,21 @@ export const asStandardTx = asObject({ isoDate: asString, timestamp: asNumber, usdValue: asNumber, + /** + * Edge's actual revenue on this order in USD, when the partner's API reports + * it (e.g. Revolut's partner_fee, pre-converted to USD by Revolut). Stored at + * ingest as a fact about the order, never recomputed. Absent when the partner + * does not report one; the v2 dashboard then estimates revenue at read time + * as usdValue * the app doc's per-partner revShareRate, so a corrected rate + * fixes history immediately while reported figures stay immutable. + */ + revenueUsd: asOptional(asNumber), + /** + * How revenueUsd was obtained. 'reported' is the only value written at + * ingest; 'estimated' exists so read-time consumers can tag derived figures + * without inventing a second vocabulary. + */ + revenueSource: asOptional(asValue('reported', 'estimated')), rawTx: asUnknown }) @@ -179,7 +194,15 @@ export const asStandardPluginParams = asObject({ const asPartnerInfo = asObject({ pluginId: asOptional(asString), - apiKeys: asMap(asString) + apiKeys: asMap(asString), + /** + * Revenue-share rate for this app-partner relationship (fraction of volume), + * used by the v2 dashboard to estimate revenue when the partner's API does + * not report actual fees. Lives here, beside the credentials that define the + * relationship, because the rate is a property of the deal: per app AND per + * partner. Never committed to source; this repo is public. + */ + revShareRate: asOptional(asNumber) }) export const asApp = asObject({ @@ -195,6 +218,9 @@ const asCacheEntry = asObject({ timestamp: asNumber, usdValue: asNumber, numTxs: asNumber, + // Sum of reported revenueUsd across the bucket's txs. Optional: cache docs + // written before this field existed lack it, and rebuilding fills it in. + revenueUsd: asOptional(asNumber), currencyCodes: asObject(asNumber), currencyPairs: asObject(asNumber) }) @@ -207,6 +233,7 @@ export const asBucket = asObject({ start: asNumber, usdValue: asNumber, numTxs: asNumber, + revenueUsd: asOptional(asNumber), isoDate: asString, currencyCodes: asObject(asNumber), currencyPairs: asObject(asNumber) @@ -266,8 +293,31 @@ export type AnalyticsResult = ReturnType export type CurrencyCodeMappings = ReturnType export type DbCurrencyCodeMappings = ReturnType -export type DbTx = ReturnType -export type StandardTx = ReturnType +// Same optional-key relaxation as StandardTx (asDbTx spreads its shape). +export type DbTx = Omit< + ReturnType, + 'revenueUsd' | 'revenueSource' +> & { + revenueUsd?: number + revenueSource?: string +} +/** + * `revenueUsd`/`revenueSource` are truly optional KEYS, not just + * possibly-undefined values: only partners whose APIs report an actual fee set + * them, and requiring every other plugin to spell out two explicit undefineds + * would churn the whole partner directory for no information. The cleaner + * still validates both fields when present. + */ +export type StandardTx = Omit< + ReturnType, + 'revenueUsd' | 'revenueSource' +> & { + revenueUsd?: number + // Widened to string at the type level because asObject's shape inference + // widens asValue literals anyway; the cleaner still enforces + // 'reported' | 'estimated' at runtime. + revenueSource?: string +} export type PluginParams = ReturnType & { log: ScopedLog } diff --git a/test/revolut.test.ts b/test/revolut.test.ts index 39aca6d5..ab1955cb 100644 --- a/test/revolut.test.ts +++ b/test/revolut.test.ts @@ -254,6 +254,53 @@ describe('processRevolutTx', function() { expect(standardTx.payoutTokenId).to.equal(undefined) }) + it('stores the partner-reported USD fee as revenue on a settled order', function() { + const standardTx = processRevolutTx( + makeOrder({ + fees_partner_currency: { + partner_fee: { amount: 1.51, currency: 'USD' } + } + }), + pluginParams + ) + expect(standardTx.revenueUsd).to.equal(1.51) + expect(standardTx.revenueSource).to.equal('reported') + }) + + it('records no revenue on an unsettled attempt even when a fee is present', function() { + // A failed attempt's fee is not revenue. + const standardTx = processRevolutTx( + makeOrder({ + status: 'FAILED', + fees_partner_currency: { + partner_fee: { amount: 1.51, currency: 'USD' } + } + }), + pluginParams + ) + expect(standardTx.revenueUsd).to.equal(undefined) + expect(standardTx.revenueSource).to.equal(undefined) + }) + + it('records no revenue when the settlement currency is not USD', function() { + // The plugin cannot convert honestly, so it abstains rather than guesses. + const standardTx = processRevolutTx( + makeOrder({ + fees_partner_currency: { + partner_fee: { amount: 1.3, currency: 'EUR' } + } + }), + pluginParams + ) + expect(standardTx.revenueUsd).to.equal(undefined) + }) + + it('records no revenue when the fee block is absent', function() { + const standardTx = processRevolutTx(makeOrder(), pluginParams) + expect(standardTx.revenueUsd).to.equal(undefined) + expect(standardTx.revenueSource).to.equal(undefined) + }) + it('throws on a structurally invalid order', function() { expect(() => processRevolutTx({ id: 'no-amounts' }, pluginParams) diff --git a/test/testData.json b/test/testData.json index 69fff085..6fc634dc 100644 --- a/test/testData.json +++ b/test/testData.json @@ -1164,6 +1164,7 @@ "isoDate": "2020-07-01T00:00:00.000Z", "usdValue": 18949.855647399967, "numTxs": 165, + "revenueUsd": 0, "currencyCodes": { "DOGE": 4353.434238976961, "ETH": 1039.0955238959054, @@ -1319,6 +1320,7 @@ "isoDate": "2011-03-01T00:00:00.000Z", "usdValue": 9520, "numTxs": 5, + "revenueUsd": 0, "currencyCodes": { "DOGE": 4760, "ETH": 220, @@ -1339,6 +1341,7 @@ "isoDate": "2011-03-13T00:00:00.000Z", "usdValue": 9480, "numTxs": 4, + "revenueUsd": 0, "currencyCodes": { "DOGE": 4740, "ETH": 200, @@ -1356,6 +1359,7 @@ "isoDate": "2011-03-14T00:00:00.000Z", "usdValue": 40, "numTxs": 1, + "revenueUsd": 0, "currencyCodes": { "ETH": 20, "DOGE": 20 @@ -1371,6 +1375,7 @@ "isoDate": "2011-03-13T07:00:00.000Z", "usdValue": 2480, "numTxs": 3, + "revenueUsd": 0, "currencyCodes": { "DOGE": 1240, "ETH": 200, @@ -1388,6 +1393,7 @@ "isoDate": "2011-03-13T08:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1396,6 +1402,7 @@ "isoDate": "2011-03-13T09:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1404,6 +1411,7 @@ "isoDate": "2011-03-13T10:00:00.000Z", "usdValue": 7000, "numTxs": 1, + "revenueUsd": 0, "currencyCodes": { "BTC": 3500, "DOGE": 3500 @@ -1417,6 +1425,7 @@ "isoDate": "2011-03-13T11:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1425,6 +1434,7 @@ "isoDate": "2011-03-13T12:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1433,6 +1443,7 @@ "isoDate": "2011-03-13T13:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1441,6 +1452,7 @@ "isoDate": "2011-03-13T14:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1449,6 +1461,7 @@ "isoDate": "2011-03-13T15:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1457,6 +1470,7 @@ "isoDate": "2011-03-13T16:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1465,6 +1479,7 @@ "isoDate": "2011-03-13T17:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1473,6 +1488,7 @@ "isoDate": "2011-03-13T18:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1481,6 +1497,7 @@ "isoDate": "2011-03-13T19:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1489,6 +1506,7 @@ "isoDate": "2011-03-13T20:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1497,6 +1515,7 @@ "isoDate": "2011-03-13T21:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1505,6 +1524,7 @@ "isoDate": "2011-03-13T22:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1513,6 +1533,7 @@ "isoDate": "2011-03-13T23:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1521,6 +1542,7 @@ "isoDate": "2011-03-14T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1529,6 +1551,7 @@ "isoDate": "2011-03-14T01:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1537,6 +1560,7 @@ "isoDate": "2011-03-14T02:00:00.000Z", "usdValue": 40, "numTxs": 1, + "revenueUsd": 0, "currencyCodes": { "ETH": 20, "DOGE": 20 @@ -1599,6 +1623,7 @@ "isoDate": "2024-02-27T00:00:00.000Z", "usdValue": 9900.43, "numTxs": 1, + "revenueUsd": 0, "currencyCodes": { "BTC": 4950.215, "ETH": 4950.215 @@ -1612,6 +1637,7 @@ "isoDate": "2024-02-28T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1620,6 +1646,7 @@ "isoDate": "2024-02-29T00:00:00.000Z", "usdValue": 20800, "numTxs": 2, + "revenueUsd": 0, "currencyCodes": { "BTC": 10400, "DOGE": 10400 @@ -1633,6 +1660,7 @@ "isoDate": "2024-03-01T00:00:00.000Z", "usdValue": 2, "numTxs": 1, + "revenueUsd": 0, "currencyCodes": { "BTC": 1, "DOGE": 1 @@ -1646,6 +1674,7 @@ "isoDate": "2024-03-02T00:00:00.000Z", "usdValue": 1000, "numTxs": 1, + "revenueUsd": 0, "currencyCodes": { "ETH": 500, "DOGE": 500 @@ -1659,6 +1688,7 @@ "isoDate": "2024-03-03T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} } @@ -1669,6 +1699,7 @@ "isoDate": "2024-02-27T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1677,6 +1708,7 @@ "isoDate": "2024-02-27T01:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1685,6 +1717,7 @@ "isoDate": "2024-02-27T02:00:00.000Z", "usdValue": 9900.43, "numTxs": 1, + "revenueUsd": 0, "currencyCodes": { "BTC": 4950.215, "ETH": 4950.215 @@ -1698,6 +1731,7 @@ "isoDate": "2024-02-27T03:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1706,6 +1740,7 @@ "isoDate": "2024-02-27T04:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1714,6 +1749,7 @@ "isoDate": "2024-02-27T05:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1722,6 +1758,7 @@ "isoDate": "2024-02-27T06:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1730,6 +1767,7 @@ "isoDate": "2024-02-27T07:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1738,6 +1776,7 @@ "isoDate": "2024-02-27T08:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1746,6 +1785,7 @@ "isoDate": "2024-02-27T09:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1754,6 +1794,7 @@ "isoDate": "2024-02-27T10:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1762,6 +1803,7 @@ "isoDate": "2024-02-27T11:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1770,6 +1812,7 @@ "isoDate": "2024-02-27T12:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1778,6 +1821,7 @@ "isoDate": "2024-02-27T13:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1786,6 +1830,7 @@ "isoDate": "2024-02-27T14:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1794,6 +1839,7 @@ "isoDate": "2024-02-27T15:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1802,6 +1848,7 @@ "isoDate": "2024-02-27T16:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1810,6 +1857,7 @@ "isoDate": "2024-02-27T17:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1818,6 +1866,7 @@ "isoDate": "2024-02-27T18:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1826,6 +1875,7 @@ "isoDate": "2024-02-27T19:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1834,6 +1884,7 @@ "isoDate": "2024-02-27T20:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1842,6 +1893,7 @@ "isoDate": "2024-02-27T21:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1850,6 +1902,7 @@ "isoDate": "2024-02-27T22:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1858,6 +1911,7 @@ "isoDate": "2024-02-27T23:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1866,6 +1920,7 @@ "isoDate": "2024-02-28T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1874,6 +1929,7 @@ "isoDate": "2024-02-28T01:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1882,6 +1938,7 @@ "isoDate": "2024-02-28T02:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1890,6 +1947,7 @@ "isoDate": "2024-02-28T03:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1898,6 +1956,7 @@ "isoDate": "2024-02-28T04:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1906,6 +1965,7 @@ "isoDate": "2024-02-28T05:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1914,6 +1974,7 @@ "isoDate": "2024-02-28T06:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1922,6 +1983,7 @@ "isoDate": "2024-02-28T07:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1930,6 +1992,7 @@ "isoDate": "2024-02-28T08:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1938,6 +2001,7 @@ "isoDate": "2024-02-28T09:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1946,6 +2010,7 @@ "isoDate": "2024-02-28T10:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1954,6 +2019,7 @@ "isoDate": "2024-02-28T11:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1962,6 +2028,7 @@ "isoDate": "2024-02-28T12:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1970,6 +2037,7 @@ "isoDate": "2024-02-28T13:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1978,6 +2046,7 @@ "isoDate": "2024-02-28T14:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1986,6 +2055,7 @@ "isoDate": "2024-02-28T15:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -1994,6 +2064,7 @@ "isoDate": "2024-02-28T16:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2002,6 +2073,7 @@ "isoDate": "2024-02-28T17:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2010,6 +2082,7 @@ "isoDate": "2024-02-28T18:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2018,6 +2091,7 @@ "isoDate": "2024-02-28T19:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2026,6 +2100,7 @@ "isoDate": "2024-02-28T20:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2034,6 +2109,7 @@ "isoDate": "2024-02-28T21:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2042,6 +2118,7 @@ "isoDate": "2024-02-28T22:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2050,6 +2127,7 @@ "isoDate": "2024-02-28T23:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2058,6 +2136,7 @@ "isoDate": "2024-02-29T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2066,6 +2145,7 @@ "isoDate": "2024-02-29T01:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2074,6 +2154,7 @@ "isoDate": "2024-02-29T02:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2082,6 +2163,7 @@ "isoDate": "2024-02-29T03:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2090,6 +2172,7 @@ "isoDate": "2024-02-29T04:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2098,6 +2181,7 @@ "isoDate": "2024-02-29T05:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2106,6 +2190,7 @@ "isoDate": "2024-02-29T06:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2114,6 +2199,7 @@ "isoDate": "2024-02-29T07:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2122,6 +2208,7 @@ "isoDate": "2024-02-29T08:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2130,6 +2217,7 @@ "isoDate": "2024-02-29T09:00:00.000Z", "usdValue": 20800, "numTxs": 2, + "revenueUsd": 0, "currencyCodes": { "BTC": 10400, "DOGE": 10400 @@ -2143,6 +2231,7 @@ "isoDate": "2024-02-29T10:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2151,6 +2240,7 @@ "isoDate": "2024-02-29T11:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2159,6 +2249,7 @@ "isoDate": "2024-02-29T12:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2167,6 +2258,7 @@ "isoDate": "2024-02-29T13:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2175,6 +2267,7 @@ "isoDate": "2024-02-29T14:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2183,6 +2276,7 @@ "isoDate": "2024-02-29T15:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2191,6 +2285,7 @@ "isoDate": "2024-02-29T16:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2199,6 +2294,7 @@ "isoDate": "2024-02-29T17:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2207,6 +2303,7 @@ "isoDate": "2024-02-29T18:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2215,6 +2312,7 @@ "isoDate": "2024-02-29T19:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2223,6 +2321,7 @@ "isoDate": "2024-02-29T20:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2231,6 +2330,7 @@ "isoDate": "2024-02-29T21:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2239,6 +2339,7 @@ "isoDate": "2024-02-29T22:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2247,6 +2348,7 @@ "isoDate": "2024-02-29T23:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2255,6 +2357,7 @@ "isoDate": "2024-03-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2263,6 +2366,7 @@ "isoDate": "2024-03-01T01:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2271,6 +2375,7 @@ "isoDate": "2024-03-01T02:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2279,6 +2384,7 @@ "isoDate": "2024-03-01T03:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2287,6 +2393,7 @@ "isoDate": "2024-03-01T04:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2295,6 +2402,7 @@ "isoDate": "2024-03-01T05:00:00.000Z", "usdValue": 2, "numTxs": 1, + "revenueUsd": 0, "currencyCodes": { "BTC": 1, "DOGE": 1 @@ -2308,6 +2416,7 @@ "isoDate": "2024-03-01T06:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2316,6 +2425,7 @@ "isoDate": "2024-03-01T07:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2324,6 +2434,7 @@ "isoDate": "2024-03-01T08:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2332,6 +2443,7 @@ "isoDate": "2024-03-01T09:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2340,6 +2452,7 @@ "isoDate": "2024-03-01T10:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2348,6 +2461,7 @@ "isoDate": "2024-03-01T11:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2356,6 +2470,7 @@ "isoDate": "2024-03-01T12:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2364,6 +2479,7 @@ "isoDate": "2024-03-01T13:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2372,6 +2488,7 @@ "isoDate": "2024-03-01T14:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2380,6 +2497,7 @@ "isoDate": "2024-03-01T15:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2388,6 +2506,7 @@ "isoDate": "2024-03-01T16:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2396,6 +2515,7 @@ "isoDate": "2024-03-01T17:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2404,6 +2524,7 @@ "isoDate": "2024-03-01T18:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2412,6 +2533,7 @@ "isoDate": "2024-03-01T19:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2420,6 +2542,7 @@ "isoDate": "2024-03-01T20:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2428,6 +2551,7 @@ "isoDate": "2024-03-01T21:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2436,6 +2560,7 @@ "isoDate": "2024-03-01T22:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2444,6 +2569,7 @@ "isoDate": "2024-03-01T23:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2452,6 +2578,7 @@ "isoDate": "2024-03-02T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2460,6 +2587,7 @@ "isoDate": "2024-03-02T01:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2468,6 +2596,7 @@ "isoDate": "2024-03-02T02:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2476,6 +2605,7 @@ "isoDate": "2024-03-02T03:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2484,6 +2614,7 @@ "isoDate": "2024-03-02T04:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2492,6 +2623,7 @@ "isoDate": "2024-03-02T05:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2500,6 +2632,7 @@ "isoDate": "2024-03-02T06:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2508,6 +2641,7 @@ "isoDate": "2024-03-02T07:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2516,6 +2650,7 @@ "isoDate": "2024-03-02T08:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2524,6 +2659,7 @@ "isoDate": "2024-03-02T09:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2532,6 +2668,7 @@ "isoDate": "2024-03-02T10:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2540,6 +2677,7 @@ "isoDate": "2024-03-02T11:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2548,6 +2686,7 @@ "isoDate": "2024-03-02T12:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2556,6 +2695,7 @@ "isoDate": "2024-03-02T13:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2564,6 +2704,7 @@ "isoDate": "2024-03-02T14:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2572,6 +2713,7 @@ "isoDate": "2024-03-02T15:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2580,6 +2722,7 @@ "isoDate": "2024-03-02T16:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2588,6 +2731,7 @@ "isoDate": "2024-03-02T17:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2596,6 +2740,7 @@ "isoDate": "2024-03-02T18:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2604,6 +2749,7 @@ "isoDate": "2024-03-02T19:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2612,6 +2758,7 @@ "isoDate": "2024-03-02T20:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2620,6 +2767,7 @@ "isoDate": "2024-03-02T21:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2628,6 +2776,7 @@ "isoDate": "2024-03-02T22:00:00.000Z", "usdValue": 1000, "numTxs": 1, + "revenueUsd": 0, "currencyCodes": { "ETH": 500, "DOGE": 500 @@ -2641,6 +2790,7 @@ "isoDate": "2024-03-02T23:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2649,6 +2799,7 @@ "isoDate": "2024-03-03T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} } @@ -2684,6 +2835,7 @@ "isoDate": "2022-12-01T00:00:00.000Z", "usdValue": 920, "numTxs": 1, + "revenueUsd": 0, "currencyCodes": { "ETH": 460, "BTC": 460 @@ -2697,6 +2849,7 @@ "isoDate": "2023-01-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2705,6 +2858,7 @@ "isoDate": "2023-02-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2713,6 +2867,7 @@ "isoDate": "2023-03-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2721,6 +2876,7 @@ "isoDate": "2023-04-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2729,6 +2885,7 @@ "isoDate": "2023-05-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2737,6 +2894,7 @@ "isoDate": "2023-06-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2745,6 +2903,7 @@ "isoDate": "2023-07-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2753,6 +2912,7 @@ "isoDate": "2023-08-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2761,6 +2921,7 @@ "isoDate": "2023-09-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2769,6 +2930,7 @@ "isoDate": "2023-10-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2777,6 +2939,7 @@ "isoDate": "2023-11-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2785,6 +2948,7 @@ "isoDate": "2023-12-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }, @@ -2793,6 +2957,7 @@ "isoDate": "2024-01-01T00:00:00.000Z", "usdValue": 3, "numTxs": 1, + "revenueUsd": 0, "currencyCodes": { "BTC": 1.5, "DOGE": 1.5 @@ -2806,6 +2971,7 @@ "isoDate": "2024-02-01T00:00:00.000Z", "usdValue": 0, "numTxs": 0, + "revenueUsd": 0, "currencyCodes": {}, "currencyPairs": {} }