diff --git a/.eslintrc.json b/.eslintrc.json index 8fbf8109..2e5fbbda 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,15 +1,32 @@ { "extends": [ "standard-kit/prettier", - "standard-kit/prettier/node", "standard-kit/prettier/jsx", + "standard-kit/prettier/node", + "standard-kit/prettier/react", "standard-kit/prettier/typescript" ], + "globals": { + "fetch": true + }, + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "rules": { + "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/no-throw-literal": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/restrict-plus-operands": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "@typescript-eslint/switch-exhaustiveness-check": "error" + } + } + ], "parserOptions": { "project": "tsconfig.json" }, "plugins": ["simple-import-sort"], "rules": { - "simple-import-sort/sort": "error" + "simple-import-sort/imports": "error" } } diff --git a/.gitignore b/.gitignore index 1f0f26ad..068bd5f0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /cache/ /dist/ /lib/ +/clientConfig.json /config.json /twitterex.txt diff --git a/.vscode/launch.json b/.vscode/launch.json index c7915118..a07a23bc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -54,7 +54,7 @@ "args":[ "-r", "sucrase/register", - "${workspaceFolder}/src/indexCache.js", + "${workspaceFolder}/src/indexCache.ts", ], "skipFiles": [ "/**" diff --git a/config.sample.json b/config.sample.json deleted file mode 100644 index bd6b30bd..00000000 --- a/config.sample.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "couchDbFullpath": "http://admin:admin@localhost:5984", - "httpPort": "8000", - "soloAppId": null, - "soloPluginId": null, - "timeoutOverrideMins": null, - "cacheLookbackMonths": null -} diff --git a/package.json b/package.json index 1595cbbf..77a270ff 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "fix": "npm run lint -- --fix", "fio:promo": "node -r sucrase/register src/bin/fioPromo/fioPromo.ts", "precommit": "lint-staged && npm run prepare", - "prepare": "npm-run-all clean -p build.*", + "prepare": "./scripts/prepare.sh && npm-run-all clean -p build.*", "start": "node -r sucrase/register src/indexQuery.ts", "start:cache": "node -r sucrase/register src/indexCache.ts", "start:rates": "node -r sucrase/register src/indexRates.ts", @@ -41,6 +41,7 @@ "axios": "^0.21.2", "biggystring": "^3.0.2", "body-parser": "^1.19.0", + "cleaner-config": "^0.1.10", "cleaners": "^0.3.13", "commander": "^6.1.0", "cors": "^2.8.5", @@ -66,20 +67,21 @@ "@types/node-fetch": "^2.6.3", "@types/react": "^16.9.22", "@types/react-dom": "^16.9.5", - "@typescript-eslint/eslint-plugin": "^2.0.0", - "@typescript-eslint/parser": "^2.0.0", + "@types/react-router-dom": "^5.3.3", + "@typescript-eslint/eslint-plugin": "^5.36.2", + "@typescript-eslint/parser": "^5.36.2", "assert": "^2.0.0", "browserify-zlib": "^0.2.0", "chai": "^4.3.4", - "eslint": ">=6.2.2", - "eslint-config-standard-kit": ">=0.14.4", - "eslint-plugin-import": ">=2.18.0", - "eslint-plugin-node": ">=9.1.0", - "eslint-plugin-prettier": "^3.0.0", - "eslint-plugin-promise": ">=4.2.1", - "eslint-plugin-react": ">=7.14.2", - "eslint-plugin-simple-import-sort": ">=4.0.0", - "eslint-plugin-standard": ">=4.0.0", + "eslint": "^8.19.0", + "eslint-config-standard-kit": "0.15.1", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^3.1.4", + "eslint-plugin-promise": "^4.2.1", + "eslint-plugin-react": "^7.21.5", + "eslint-plugin-react-hooks": "^4.2.0", + "eslint-plugin-simple-import-sort": "^6.0.1", "events": "^3.3.0", "https-browserify": "^1.0.0", "husky": ">=3.0.0", @@ -94,7 +96,7 @@ "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "sucrase": "^3.20.0", - "typescript": "^3.8.2", + "typescript": "^4.8.4", "url": "^0.11.0", "util": "^0.12.4" } diff --git a/scripts/prepare.sh b/scripts/prepare.sh new file mode 100755 index 00000000..8242ca5a --- /dev/null +++ b/scripts/prepare.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# The Edge application uses WebView components extensively. +# These components need various JS files to operate, +# so this script prepares those. + +set -e +cd "$(dirname "$0")/.." + +# Assemble the clientConfig.json config file: +node -r sucrase/register ./src/bin/configure.ts diff --git a/src/apiAnalytics.ts b/src/apiAnalytics.ts index 6695dce4..e13dbbd7 100644 --- a/src/apiAnalytics.ts +++ b/src/apiAnalytics.ts @@ -30,7 +30,7 @@ interface AnalyticsResult { numAllTxs: number } appId: string - pluginId: string + partnerId: string start: number end: number } @@ -40,7 +40,7 @@ export const getAnalytics = ( start: number, end: number, appId: string, - pluginId: string, + partnerId: string, timePeriod: string ): AnalyticsResult => { // the creation of buckets @@ -51,7 +51,7 @@ export const getAnalytics = ( const dayArray: Bucket[] = [] const hourArray: Bucket[] = [] // monthly buckets creation - if (hasMonthBucket === true) { + if (hasMonthBucket) { let { y, m } = utcVariables(start) let monthStart = new Date(Date.UTC(y, m, 1, 0)) do { @@ -68,7 +68,7 @@ export const getAnalytics = ( } while (monthStart.getTime() <= end * 1000) } // daily buckets Creation - if (hasDayBucket === true) { + if (hasDayBucket) { let { y, m, d } = utcVariables(start) let dayStart = new Date(Date.UTC(y, m, d, 0)) do { @@ -85,7 +85,7 @@ export const getAnalytics = ( } while (dayStart.getTime() <= end * 1000) } // hourly buckets creation - if (hasHourBucket === true) { + if (hasHourBucket) { let { y, m, d, h } = utcVariables(start) let hourStart = new Date(Date.UTC(y, m, d, h)) do { @@ -108,19 +108,19 @@ export const getAnalytics = ( let hourPointer = 0 for (const tx of txs) { // month - if (hasMonthBucket === true) { + if (hasMonthBucket) { // advances pointer to bucket that matches current txs timestamp monthPointer = bucketScroller(monthArray, monthPointer, tx.timestamp) // adds usdvalue, currencycode, and currencypair to that bucket bucketAdder(monthArray[monthPointer], tx) } // day - if (hasDayBucket === true) { + if (hasDayBucket) { dayPointer = bucketScroller(dayArray, dayPointer, tx.timestamp) bucketAdder(dayArray[dayPointer], tx) } // hour - if (hasHourBucket === true) { + if (hasHourBucket) { hourPointer = bucketScroller(hourArray, hourPointer, tx.timestamp) bucketAdder(hourArray[hourPointer], tx) } @@ -134,7 +134,7 @@ export const getAnalytics = ( numAllTxs: txs.length }, appId, - pluginId, + partnerId, start: start, end: end } diff --git a/src/bin/bogReporter.ts b/src/bin/bogReporter.ts index 10e1dea9..f699c43c 100644 --- a/src/bin/bogReporter.ts +++ b/src/bin/bogReporter.ts @@ -1,11 +1,10 @@ import fetch from 'node-fetch' -import CONFIG from '../../config.json' +import { config } from '../config' -// @ts-ignore -const BITS_OF_GOLD_API_KEY = CONFIG.bog.apiKey +const BITS_OF_GOLD_API_KEY = config.bog.apiKey -const dateRegex = RegExp(/([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/) +const dateRegex = /([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))/ async function queryBog(): Promise { // Grab args and verify format @@ -53,7 +52,7 @@ async function queryBog(): Promise { ) } } catch (e) { - console.log(e.message) + console.log(String(e)) } } // Print totals @@ -71,7 +70,7 @@ async function queryFiatRate( method: 'GET' } ) - if (result.ok !== true) { + if (!result.ok) { throw new Error(`queryFiatRate failed with status code ${result.status}`) } const json = await result.json() @@ -88,7 +87,7 @@ async function queryCryptoRate( method: 'GET' } ) - if (result.ok !== true) { + if (!result.ok) { throw new Error(`queryCryptoRate failed with status code ${result.status}`) } const json = await result.json() diff --git a/src/bin/configure.ts b/src/bin/configure.ts new file mode 100644 index 00000000..53e1add4 --- /dev/null +++ b/src/bin/configure.ts @@ -0,0 +1,5 @@ +import { makeConfig } from 'cleaner-config' + +import { asClientConfig } from '../demo/clientConfig' + +export const clientConfig = makeConfig(asClientConfig, './clientConfig.json') diff --git a/src/bin/fioPromo/fioLookup.ts b/src/bin/fioPromo/fioLookup.ts index 7320c324..513bd033 100644 --- a/src/bin/fioPromo/fioLookup.ts +++ b/src/bin/fioPromo/fioLookup.ts @@ -38,7 +38,7 @@ const { } = defaultSettings const configFile: string = fs.readFileSync( - `${__dirname}/../../../config.json`, + path.join(__dirname, `/../../../config.json`), 'utf8' ) const config = JSON.parse(configFile) @@ -94,8 +94,7 @@ export const checkAddress = async ( error = '' break } catch (e) { - error = e - console.log(e) + console.log(String(e)) } } if (error !== '') throw error diff --git a/src/bin/fixPluginIds.ts b/src/bin/fixPluginIds.ts deleted file mode 100644 index ea12c661..00000000 --- a/src/bin/fixPluginIds.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { asArray, asMap, asObject, asString } from 'cleaners' -import js from 'jsonfile' -import nano from 'nano' - -import { asDbTx, DbTx } from '../types' -import { datelog } from '../util' - -const asApp = asObject({ - _id: asString, - _rev: asString, - appId: asString, - appName: asString, - pluginIds: asMap(asMap(asString)) -}) -const asApps = asArray(asApp) - -const asDbQueryResult = asObject({ docs: asArray(asDbTx), bookmark: asString }) - -const PLUGIN_ID_MAP = { - fox: 'foxExchange' -} - -const PAGINATION = 100 - -const config = js.readFileSync('./config.json') -const nanoDb = nano(config.couchDbFullpath) -const reportsApps = nanoDb.use('reports_apps') -const reportsProgress = nanoDb.use('reports_progresscache') -const reportsTransactions = nanoDb.use('reports_transactions') - -async function fixPluginIds(): Promise { - try { - // get the contents of all reports_apps docs - const query = { - selector: { - appId: { $exists: true } - }, - limit: 1000000 - } - const rawApps = await reportsApps.find(query) - const apps = asApps(rawApps.docs) - for (const app of apps) { - for (const pluginId in PLUGIN_ID_MAP) { - const partition = `${app.appId}_${pluginId}` - let bookmark - const queryResults: any[] = [] - while (true) { - const query = { - selector: { - _id: { $exists: true } - }, - bookmark, - limit: PAGINATION - } - const result = await reportsTransactions.partitionedFind( - partition, - query - ) - if ( - typeof result.bookmark === 'string' && - result.docs.length === PAGINATION - ) { - bookmark = result.bookmark - } else { - bookmark = undefined - } - try { - asDbQueryResult(result) - } catch (e) { - datelog(`Invalid Query Result for ${partition}`, e) - continue - } - queryResults.push(...result.docs) - if (result.docs.length < PAGINATION) break - } - if (queryResults.length === 0) { - datelog( - `Bad partition ${pluginId} does not exist within app ${app.appId}.` - ) - return - } else { - datelog( - `Gathered ${queryResults.length} docs from partition ${partition}` - ) - } - - for (let i = 0; i < queryResults.length; i += PAGINATION) { - const currentBatch: DbTx[] = queryResults.slice(i, i + PAGINATION) - const newTxs = currentBatch.map(tx => { - return { - ...tx, - _id: `${app.appId}_${PLUGIN_ID_MAP[pluginId]}:${tx.orderId}`, - _rev: undefined - } - }) - await reportsTransactions.bulk({ docs: newTxs }) - datelog( - `Successfully inserted ${i + - currentBatch.length} documents of new partition ${ - PLUGIN_ID_MAP[pluginId] - }` - ) - - const deleteTxs = currentBatch.map(tx => { - return { - ...tx, - _deleted: true - } - }) - await reportsTransactions.bulk({ docs: deleteTxs }) - datelog( - `Successfully deleted ${i + - currentBatch.length} documents of old partition ${pluginId}` - ) - } - - if (app.pluginIds[pluginId] != null) { - app.pluginIds[PLUGIN_ID_MAP[pluginId]] = app.pluginIds[pluginId] - delete app.pluginIds[pluginId] - await reportsApps.insert(app) - datelog( - `Successfully updated bad pluginId name ${pluginId} to ${PLUGIN_ID_MAP[pluginId]} in reports_apps` - ) - } - const progress = await reportsProgress.get(`${app.appId}:${pluginId}`) - const newCache = { - ...progress, - _id: `${app.appId}:${PLUGIN_ID_MAP[pluginId]}`, - _rev: undefined - } - await reportsProgress.insert(newCache) - datelog(`Successfully inserted new Cache.`) - await reportsProgress.destroy(progress._id, progress._rev) - datelog(`Successfully deleted old Cache.`) - } - } - } catch (e) { - datelog(e) - throw e - } -} -fixPluginIds().catch(e => datelog(e)) diff --git a/src/bin/migration.ts b/src/bin/migration.ts index 0c38b73b..8ff99a28 100644 --- a/src/bin/migration.ts +++ b/src/bin/migration.ts @@ -89,7 +89,6 @@ async function migration(): Promise { try { await reportsTransactions - // @ts-ignore .partitionedList(appAndPluginId, { include_docs: true }) .then(body => { body.rows.forEach(doc => { @@ -122,6 +121,7 @@ async function migration(): Promise { ) { return obj } + return false }) const reformattedTxs: DbTx[] = [] let offset = 0 @@ -151,7 +151,7 @@ async function migration(): Promise { usdValue: -1, rawTx: tx } - return standardTxReformat( + return await standardTxReformat( newTx, appAndPluginId, reportsTransactions @@ -196,7 +196,7 @@ async function migration(): Promise { usdValue: -1, rawTx: undefined } - return standardTxReformat( + return await standardTxReformat( newTx, appAndPluginId, reportsTransactions diff --git a/src/cacheEngine.ts b/src/cacheEngine.ts index 7236b7c3..b860a3b4 100644 --- a/src/cacheEngine.ts +++ b/src/cacheEngine.ts @@ -1,92 +1,27 @@ -import { asArray, asMap, asObject, asString } from 'cleaners' import startOfMonth from 'date-fns/startOfMonth' import sub from 'date-fns/sub' import nano from 'nano' -import config from '../config.json' +import { config } from './config' import { getAnalytic } from './dbutils' +import { initDbs } from './initDbs' +import { asApps } from './types' import { datelog, snooze } from './util' const CACHE_UPDATE_LOOKBACK_MONTHS = config.cacheLookbackMonths ?? 3 const BULK_WRITE_SIZE = 50 const UPDATE_FREQUENCY_MS = 1000 * 60 * 30 -const asApp = asObject({ - _id: asString, - appId: asString, - pluginIds: asMap(asMap(asString)) -}) -const asApps = asArray(asApp) const nanoDb = nano(config.couchDbFullpath) -const DB_NAMES = [ - { - name: 'reports_hour', - options: { partitioned: true }, - indexes: [ - { - index: { fields: ['timestamp'] }, - ddoc: 'timestamp-index', - name: 'timestamp-index', - type: 'json' as 'json', - partitioned: true - } - ] - }, - { - name: 'reports_day', - options: { partitioned: true }, - indexes: [ - { - index: { fields: ['timestamp'] }, - ddoc: 'timestamp-index', - name: 'timestamp-index', - type: 'json' as 'json', - partitioned: true - } - ] - }, - { - name: 'reports_month', - options: { partitioned: true }, - indexes: [ - { - index: { fields: ['timestamp'] }, - ddoc: 'timestamp-index', - name: 'timestamp-index', - type: 'json' as 'json', - partitioned: true - } - ] - } -] const TIME_PERIODS = ['hour', 'day', 'month'] export async function cacheEngine(): Promise { datelog('Starting Cache Engine') console.time('cacheEngine') - // get a list of all databases within couchdb - const result = await nanoDb.db.list() - datelog(result) - // if database does not exist, create it - for (const dbName of DB_NAMES) { - if (result.includes(dbName.name) === false) { - await nanoDb.db.create(dbName.name, dbName.options) - } - if (dbName.indexes !== undefined) { - const currentDb = nanoDb.db.use(dbName.name) - for (const dbIndex of dbName.indexes) { - try { - await currentDb.get(`_design/${dbIndex.ddoc}`) - datelog(`${dbName.name} already has '${dbIndex.name}' index.`) - } catch { - await currentDb.createIndex(dbIndex) - datelog(`Created '${dbIndex.name}' index for ${dbName.name}.`) - } - } - } - } + + await initDbs() const reportsApps = nanoDb.use('reports_apps') const reportsTransactions = nanoDb.use('reports_transactions') @@ -112,24 +47,29 @@ export async function cacheEngine(): Promise { selector: { appId: { $exists: true } }, - fields: ['_id', 'appId', 'pluginIds'], limit: 1000000 } const rawApps = await reportsApps.find(query) const apps = asApps(rawApps.docs) for (const app of apps) { - if (config.soloAppId != null && config.soloAppId !== app.appId) continue - - const keys = Object.keys(app.pluginIds) + if (config.soloAppIds != null && !config.soloAppIds.includes(app.appId)) { + continue + } + const partnerIds = Object.keys(app.partnerIds) - for (const key of keys) { - if (config.soloPluginId != null && config.soloPluginId !== key) continue + for (const partnerId of partnerIds) { + if ( + config.soloPartnerIds != null && + !config.soloPartnerIds.includes(partnerId) + ) { + continue + } for (const timePeriod of TIME_PERIODS) { const data = await getAnalytic( start, end, app.appId, - [key], + [partnerId], timePeriod, reportsTransactions ) @@ -137,7 +77,7 @@ export async function cacheEngine(): Promise { if (data.length > 0) { const cacheResult = data[0].result[timePeriod].map(bucket => { return { - _id: `${app.appId}_${key}:${bucket.isoDate}`, + _id: `${app.appId}_${partnerId}:${bucket.isoDate}`, timestamp: bucket.start, usdValue: bucket.usdValue, numTxs: bucket.numTxs, @@ -169,7 +109,7 @@ export async function cacheEngine(): Promise { } datelog( - `Update cache db ${timePeriod} cache for ${app.appId}_${key}. length = ${cacheResult.length}` + `Update cache db ${timePeriod} cache for ${app.appId}_${partnerId}. length = ${cacheResult.length}` ) for ( @@ -190,7 +130,7 @@ export async function cacheEngine(): Promise { } datelog( - `Finished updating ${timePeriod} cache for ${app.appId}_${key}` + `Finished updating ${timePeriod} cache for ${app.appId}_${partnerId}` ) } catch (e) { datelog('Error doing bulk cache update', e) diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 00000000..9bfc4083 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,17 @@ +import { makeConfig } from 'cleaner-config' +import { asArray, asNumber, asObject, asOptional, asString } from 'cleaners' + +export const asConfig = asObject({ + couchDbFullpath: asOptional( + asString, + 'http://username:password@localhost:5984' + ), + httpPort: asOptional(asNumber, 8008), + bog: asOptional(asObject({ apiKey: asString }), { apiKey: '' }), + soloAppIds: asOptional(asArray(asString), null), + soloPartnerIds: asOptional(asArray(asString), null), + timeoutOverrideMins: asOptional(asNumber, 1200), + cacheLookbackMonths: asOptional(asNumber, 24) +}) + +export const config = makeConfig(asConfig, 'config.json') diff --git a/src/dbutils.ts b/src/dbutils.ts index d4e58717..9ee6c132 100644 --- a/src/dbutils.ts +++ b/src/dbutils.ts @@ -1,9 +1,9 @@ import { asArray, asNumber, asObject, asString } from 'cleaners' import nano from 'nano' -import config from '../config.json' import { getAnalytics } from './apiAnalytics' -import { AnalyticsResult } from './demo/components/Graphs' +import { config } from './config' +import { AnalyticsResult, asCacheQuery } from './types' import { datelog, promiseTimeout } from './util' const BATCH_ADVANCE = 100 @@ -54,7 +54,7 @@ export const getAnalytic = async ( start: number, end: number, appId: string, - pluginIds: string[], + partnerIds: string[], timePeriod: string, transactionDatabase: any ): Promise => { @@ -71,24 +71,24 @@ export const getAnalytic = async ( 'timestamp', 'usdValue' ], - use_index: 'status-usdvalue-timestamp-index', + use_index: 'timestamp-p', sort: ['timestamp'], limit: 1000000 } const results: any[] = [] const promises: Array> = [] try { - for (const pluginId of pluginIds) { - const appAndPluginId = `${appId}_${pluginId}` + for (const partnerId of partnerIds) { + const appAndPartnerId = `${appId}_${partnerId}` const result = transactionDatabase - .partitionedFind(appAndPluginId, query) + .partitionedFind(appAndPartnerId, query) .then(data => { const analytic = getAnalytics( asDbReq(data).docs, start, end, appId, - pluginId, + appAndPartnerId, timePeriod ) if (analytic.result.numAllTxs > 0) results.push(analytic) @@ -117,7 +117,7 @@ export const cacheAnalytic = async ( start: number, end: number, appId: string, - pluginIds: string[], + partnerIds: string[], timePeriod: string ): Promise => { const nanoDb = nano(config.couchDbFullpath) @@ -129,12 +129,12 @@ export const cacheAnalytic = async ( if (timePeriod.includes('day')) timePeriods.push('day') if (timePeriod.includes('month')) timePeriods.push('month') const analyticResultArray: AnalyticsResult[] = [] - for (const pluginId of pluginIds) { + for (const partnerId of partnerIds) { const analyticResult: AnalyticsResult = { start, end, app: appId, - pluginId, + partnerId, result: { hour: [], day: [], month: [], numAllTxs: 0 } } let startForDayTimePeriod @@ -157,9 +157,10 @@ export const cacheAnalytic = async ( limit: 1000000 } try { - const appAndPluginId = `${appId}_${pluginId}` + const appAndPluginId = `${appId}_${partnerId}` const result = await database.partitionedFind(appAndPluginId, query) - analyticResult.result[timePeriod] = result.docs.map(cacheObj => { + const cacheResults = asCacheQuery(result) + analyticResult.result[timePeriod] = cacheResults.docs.map(cacheObj => { analyticResult.result.numAllTxs += cacheObj.numTxs return { start: cacheObj.timestamp, @@ -170,8 +171,8 @@ export const cacheAnalytic = async ( currencyPairs: cacheObj.currencyPairs } }) - console.time(`${pluginId} ${timePeriod} cache fetched`) - console.timeEnd(`${pluginId} ${timePeriod} cache fetched`) + console.time(`${partnerId} ${timePeriod} cache fetched`) + console.timeEnd(`${partnerId} ${timePeriod} cache fetched`) } catch (e) { console.log(e) return `Internal server error.` diff --git a/src/demo/clientConfig.ts b/src/demo/clientConfig.ts new file mode 100644 index 00000000..fa95edfe --- /dev/null +++ b/src/demo/clientConfig.ts @@ -0,0 +1,5 @@ +import { asObject, asOptional, asString } from 'cleaners' + +export const asClientConfig = asObject({ + apiHost: asOptional(asString, 'http://localhost:8008') +}) diff --git a/src/demo/clientUtil.ts b/src/demo/clientUtil.ts new file mode 100644 index 00000000..99b4c83c --- /dev/null +++ b/src/demo/clientUtil.ts @@ -0,0 +1,271 @@ +import { asArray, asString } from 'cleaners' +import add from 'date-fns/add' +import eachQuarterOfInterval from 'date-fns/eachQuarterOfInterval' +import startOfDay from 'date-fns/startOfDay' +import startOfHour from 'date-fns/startOfHour' +import startOfMonth from 'date-fns/startOfMonth' +import sub from 'date-fns/sub' +import { getTimezoneOffset } from 'date-fns-tz' +import fetch from 'node-fetch' + +import clientConfigRaw from '../../clientConfig.json' +import { AnalyticsResult, asAnalyticsResult, Bucket } from '../types' +import { asClientConfig } from './clientConfig' +import { Data, DataPlusSevenDayAve } from './components/Graphs' +import Partners from './partners' + +const clientConfig = asClientConfig(clientConfigRaw) + +export const SIX_DAYS = 6 + +export const apiHost = clientConfig.apiHost ?? '' + +const asGetPartnerIds = asArray(asString) + +export const addObject = (origin: any, destination: any): void => { + Object.keys(origin).forEach(originKey => { + if (destination[originKey] == null) { + destination[originKey] = origin[originKey] + } else destination[originKey] += origin[originKey] + }) +} + +export type SetDataKeys = 'setData1' | 'setData2' | 'setData3' + +export type PresetDates = Record + +export const getPresetDates = function(): PresetDates { + const DATE = new Date(Date.now()) + const HOUR_RANGE_END = add(startOfHour(DATE), { hours: 1 }) + const DAY_RANGE_END = add(startOfDay(DATE), { days: 1 }) + // const TRUE_DAY_RANGE_END = sub(DAY_RANGE_END, { + // minutes: DAY_RANGE_END.getTimezoneOffset() + // }) + const MONTH_RANGE_END = add(startOfMonth(DATE), { months: 1 }) + const HOUR_RANGE_START = sub(HOUR_RANGE_END, { hours: 36 }) + const DAY_RANGE_START = sub(DAY_RANGE_END, { days: 75 }) + // const TRUE_DAY_RANGE_START = sub(DAY_RANGE_START, { + // minutes: DAY_RANGE_START.getTimezoneOffset() + // }) + const MONTH_RANGE_START = sub(MONTH_RANGE_END, { months: 4 }) + const MONTH_RANGE_ARRAY = [[MONTH_RANGE_START, MONTH_RANGE_END]] + for (let i = 0; i < 7; i++) { + const currentEnd = new Date(MONTH_RANGE_ARRAY[0][0]) + const currentStart = sub(currentEnd, { months: 3 }) + MONTH_RANGE_ARRAY.unshift([currentStart, currentEnd]) + } + return { + setData1: [ + [ + HOUR_RANGE_START.toISOString(), + new Date(HOUR_RANGE_END.getTime() - 1).toISOString() + ] + ], + setData2: [ + [ + DAY_RANGE_START.toISOString(), + new Date(DAY_RANGE_END.getTime() - 1).toISOString() + ] + ], + setData3: MONTH_RANGE_ARRAY.map(array => { + const start = sub(array[0], { + minutes: array[0].getTimezoneOffset() + }).toISOString() + const end = sub(array[1], { + minutes: array[1].getTimezoneOffset(), + seconds: 1 + }).toISOString() + + return [start, end] + }) + } +} + +export const getCustomData = async ( + appId: string, + pluginIds: string[], + start: string, + end: string, + timePeriod: string = 'hourdaymonth' +): Promise => { + const endPoint = `${apiHost}/v1/analytics/` + let trueTimePeriod = timePeriod + if ( + new Date(end).getTime() - new Date(start).getTime() > + 1000 * 60 * 60 * 24 * 7 && + timePeriod === 'hourdaymonth' + ) { + trueTimePeriod = 'daymonth' + } + const query = { start, end, appId, pluginIds, timePeriod: trueTimePeriod } + const response = await fetch(endPoint, { + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST', + body: JSON.stringify(query) + }) + const json = await response.json() + const out = asArray(asAnalyticsResult)(json) + return out +} + +export const getTimeRange = (start: string, end: string): string => { + const timeRange = new Date(end).getTime() - new Date(start).getTime() + if (timeRange < 1000 * 60 * 60 * 24 * 3) { + return 'hour' + } else if (timeRange < 1000 * 60 * 60 * 24 * 75) { + return 'day' + } else { + return 'month' + } +} + +export const createQuarterBuckets = (analytics: AnalyticsResult): Bucket[] => { + const localTimezoneDbName = Intl.DateTimeFormat().resolvedOptions().timeZone // Use 'Intl' object to get local timezone name + // The 'getTimezoneOffset' helper requires two parameters to account for DST, and it returns offset in milliseconds + const timezoneOffsetStart = getTimezoneOffset( + localTimezoneDbName, + new Date(analytics.start * 1000) + ) + const timezoneOffsetEnd = getTimezoneOffset( + localTimezoneDbName, + new Date(analytics.end * 1000) + ) + + const quarterIntervals = eachQuarterOfInterval({ + start: new Date(analytics.start * 1000 - timezoneOffsetStart), + end: new Date(analytics.end * 1000 - timezoneOffsetEnd) + }) + const buckets = quarterIntervals.map(date => { + const timezoneOffset = getTimezoneOffset(localTimezoneDbName, date) + const realTimestamp = date.getTime() + timezoneOffset + return { + start: realTimestamp / 1000, + usdValue: 0, + numTxs: 0, + isoDate: new Date(realTimestamp).toISOString(), + currencyCodes: {}, + currencyPairs: {} + } + }) + let i = 0 + for (const month of analytics.result.month) { + const { usdValue, numTxs, currencyPairs, currencyCodes } = month + if (i + 1 < buckets.length && month.start >= buckets[i + 1].start) { + i++ + } + buckets[i].usdValue += usdValue + buckets[i].numTxs += numTxs + addObject(currencyPairs, buckets[i].currencyPairs) + addObject(currencyCodes, buckets[i].currencyCodes) + } + return buckets +} + +interface AppIdResponse { + appId: string + redirect: boolean +} + +interface PartnerIdsResponse { + partnerIds: string[] +} + +export const getAppId = async (apiKey: string): Promise => { + const url = `${apiHost}/v1/getAppId?apiKey=${apiKey}` + const response = await fetch(url) + const appId = await response.json() + let redirect = false + if (appId == null || appId === '') { + redirect = true + } + return { appId, redirect } +} + +export const getPartnerIds = async ( + appId: string +): Promise => { + const partners = Object.keys(Partners) + const url = `${apiHost}/v1/getPluginIds?appId=${appId}` + const response = await fetch(url) + const json = await response.json() + const ids = asGetPartnerIds(json) + const partnerIds = ids.filter(pluginId => partners.includes(pluginId)) + return { partnerIds } +} +interface GraphTotals { + totalTxs: number + totalUsd: number + partnerId?: string +} + +export const calculateGraphTotals = ( + analyticsResult: AnalyticsResult +): GraphTotals => { + if (analyticsResult.result.month.length > 0) { + return addGraphTotals(analyticsResult, 'month') + } + if (analyticsResult.result.day.length > 0) { + return addGraphTotals(analyticsResult, 'day') + } + if (analyticsResult.result.hour.length > 0) { + return addGraphTotals(analyticsResult, 'hour') + } + return { totalTxs: 0, totalUsd: 0 } +} + +const addGraphTotals = ( + analyticsResult: AnalyticsResult, + timePeriod: string +): GraphTotals => { + const totalTxs = analyticsResult.result[timePeriod].reduce( + (a: number, b: Bucket) => a + b.numTxs, + 0 + ) + const totalUsd = analyticsResult.result[timePeriod].reduce( + (a: number, b: Bucket) => a + b.usdValue, + 0 + ) + return { totalTxs, totalUsd } +} + +export const movingAveDataSort = ( + data: Data[] +): Array<{ date: string; allUsd: number }> => { + const aveArray: Array<{ date: string; allUsd: number }> = [] + if (!Array.isArray(data)) return aveArray + for (let i = 0; i < data.length; i++) { + let sevenDaySum: number = 0 + for (let j = i; j >= i - SIX_DAYS; j--) { + if (j < 0) { + continue + } + const currentData: Data = data[j] + sevenDaySum += currentData.allUsd + } + if (typeof sevenDaySum !== 'number' || isNaN(sevenDaySum)) return [] + const sevenDayAve: number = Math.round(sevenDaySum / 7) + aveArray.push({ date: data[i].date, allUsd: sevenDayAve }) + } + return aveArray +} + +export const sevenDayDataMerge = (data: Data[]): DataPlusSevenDayAve[] => { + const sevenDayDataArr: DataPlusSevenDayAve[] = [] + const sevenDayData = movingAveDataSort(data) + if (!Array.isArray(data) || sevenDayData.length === 0) { + return sevenDayDataArr + } + data.forEach(object => { + const sevenDayIndex = sevenDayData.findIndex( + obj => obj.date === object.date + ) + const sevenDayAve = sevenDayData[sevenDayIndex].allUsd + sevenDayDataArr.push({ + ...object, + sevenDayAve: sevenDayAve + }) + }) + return sevenDayDataArr.slice(SIX_DAYS) +} diff --git a/src/demo/components/ApiKeyScreen.tsx b/src/demo/components/ApiKeyScreen.tsx index c0124315..f9d2f8fe 100644 --- a/src/demo/components/ApiKeyScreen.tsx +++ b/src/demo/components/ApiKeyScreen.tsx @@ -1,10 +1,10 @@ -import React from 'react' -import { Redirect, withRouter } from 'react-router-dom' +import * as React from 'react' +import { Redirect, RouteComponentProps, withRouter } from 'react-router-dom' -interface ApiKeyScreenProps { +interface ApiKeyScreenProps extends RouteComponentProps { apiKeyMessage: string - handleApiKeyChange: any - getAppId: any + handleApiKeyChange: (apiKey: string) => void + getAppId: () => Promise | void appId: string } @@ -36,7 +36,7 @@ const apiKeyButton = { marginLeft: '20px' } -const ApiKeyScreen: any = (props: ApiKeyScreenProps) => { +const ApiKeyScreen = (props: ApiKeyScreenProps): React.ReactElement => { if (typeof props.appId === 'string' && props.appId.length > 0) { return } @@ -46,9 +46,12 @@ const ApiKeyScreen: any = (props: ApiKeyScreenProps) => {
  • props.handleApiKeyChange(e)} + onChange={e => props.handleApiKeyChange(e.target.value)} /> -
  • diff --git a/src/demo/components/Buttons.tsx b/src/demo/components/Buttons.tsx index 3d361896..800dc485 100644 --- a/src/demo/components/Buttons.tsx +++ b/src/demo/components/Buttons.tsx @@ -52,7 +52,10 @@ interface buttonProps { onClick: () => void } -export class MainButton extends PureComponent { +export class MainButton extends PureComponent< + { label: string; onClick: () => void }, + {} +> { render(): JSX.Element { return (