From dce5795fa8b5a4bf3fb1ee8dfd9bf36b2cf71c18 Mon Sep 17 00:00:00 2001 From: AP Date: Sat, 23 Nov 2019 19:49:20 +0100 Subject: [PATCH] feat: add report queries --- package.json | 3 + .../query/report/ForwardChannels.ts | 15 ++++ src/schemas/query/index.ts | 4 +- src/schemas/query/report/ForwardChannels.ts | 58 +++++++++++++++ .../query/report/ForwardReport.interface.ts | 42 +++++++++++ src/schemas/query/report/ForwardReport.ts | 71 +++++++++++++++++++ src/schemas/query/report/Helpers.ts | 58 +++++++++++++++ src/schemas/query/report/index.ts | 7 ++ yarn.lock | 15 ++++ 9 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 src/schemaTypes/query/report/ForwardChannels.ts create mode 100644 src/schemas/query/report/ForwardChannels.ts create mode 100644 src/schemas/query/report/ForwardReport.interface.ts create mode 100644 src/schemas/query/report/ForwardReport.ts create mode 100644 src/schemas/query/report/Helpers.ts create mode 100644 src/schemas/query/report/index.ts diff --git a/package.json b/package.json index eb442c43..4f6e4fec 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,16 @@ "@types/graphql-depth-limit": "^1.1.2", "@types/graphql-iso-date": "^3.3.3", "@types/node-fetch": "^2.5.3", + "@types/underscore": "^1.9.4", "apollo-server": "^2.9.7", + "date-fns": "^2.8.1", "dotenv": "^8.2.0", "graphql": "^14.5.8", "graphql-depth-limit": "^1.1.0", "graphql-iso-date": "^3.6.1", "graphql-rate-limit": "^2.0.1", "ln-service": "^46.6.0", + "underscore": "^1.9.1", "winston": "^3.2.1" }, "devDependencies": { diff --git a/src/schemaTypes/query/report/ForwardChannels.ts b/src/schemaTypes/query/report/ForwardChannels.ts new file mode 100644 index 00000000..7abdaac8 --- /dev/null +++ b/src/schemaTypes/query/report/ForwardChannels.ts @@ -0,0 +1,15 @@ +import { GraphQLObjectType, GraphQLString } from "graphql"; + +export const ForwardChannelsType = new GraphQLObjectType({ + name: "forwardChannelType", + fields: () => { + return { + incoming: { + type: GraphQLString + }, + outgoing: { + type: GraphQLString + } + }; + } +}); diff --git a/src/schemas/query/index.ts b/src/schemas/query/index.ts index 886aee79..f9632d56 100644 --- a/src/schemas/query/index.ts +++ b/src/schemas/query/index.ts @@ -2,10 +2,12 @@ import { channelQueries } from "./channels"; import { generalQueries } from "./general"; import { invoiceQueries } from "./invoices"; import { dataQueries } from "./data"; +import { reportQueries } from "./report"; export const query = { ...channelQueries, ...generalQueries, ...invoiceQueries, - ...dataQueries + ...dataQueries, + ...reportQueries }; diff --git a/src/schemas/query/report/ForwardChannels.ts b/src/schemas/query/report/ForwardChannels.ts new file mode 100644 index 00000000..290e55b2 --- /dev/null +++ b/src/schemas/query/report/ForwardChannels.ts @@ -0,0 +1,58 @@ +import { GraphQLString } from "graphql"; +import { getForwards as getLnForwards } from "ln-service"; +import { logger } from "../../../helpers/logger"; +import { requestLimiter } from "../../../helpers/rateLimiter"; +import { subHours, subDays } from "date-fns"; +import { countArray } from "./Helpers"; +import { ForwardCompleteProps } from "./ForwardReport.interface"; +import { ForwardChannelsType } from "../../../schemaTypes/query/report/ForwardChannels"; + +export const getForwardChannelsReport = { + type: ForwardChannelsType, + args: { + time: { + type: GraphQLString + } + }, + resolve: async (root: any, params: any, context: any) => { + await requestLimiter( + context.ip, + params, + "getForwardChannelsReport", + 1, + "1s" + ); + const { lnd } = context; + + let startDate = new Date(); + const endDate = new Date(); + + if (params.time === "month") { + startDate = subDays(endDate, 30); + } else if (params.time === "week") { + startDate = subDays(endDate, 7); + } else { + startDate = subHours(endDate, 24); + } + + try { + const forwardsList: ForwardCompleteProps = await getLnForwards({ + lnd: lnd, + after: startDate, + before: endDate, + limit: 10000 + }); + + const incomingCount = countArray(forwardsList.forwards, true); + const outgoingCount = countArray(forwardsList.forwards, false); + + return { + incoming: JSON.stringify(incomingCount), + outgoing: JSON.stringify(outgoingCount) + }; + } catch (error) { + logger.error("Error getting forward channel report: %o", error); + throw new Error("Failed to get forward channel report."); + } + } +}; diff --git a/src/schemas/query/report/ForwardReport.interface.ts b/src/schemas/query/report/ForwardReport.interface.ts new file mode 100644 index 00000000..4df4098e --- /dev/null +++ b/src/schemas/query/report/ForwardReport.interface.ts @@ -0,0 +1,42 @@ +export interface ForwardProps { + created_at: string; + fee: number; + fee_mtokens: string; + incoming_channel: string; + mtokens: string; + outgoing_channel: string; + tokens: number; +} + +export interface ForwardCompleteProps { + forwards: ForwardProps[]; + next: string; +} + +export interface ListProps { + [key: string]: ForwardProps[]; +} + +export interface ReduceObjectProps { + fee: number; + tokens: number; +} + +export interface FinalProps { + fee: number; + tokens: number; + amount: number; +} + +export interface FinalList { + [key: string]: FinalProps; +} + +export interface CountProps { + [key: string]: number; +} + +export interface ChannelCounts { + name: string; + count: number; +} diff --git a/src/schemas/query/report/ForwardReport.ts b/src/schemas/query/report/ForwardReport.ts new file mode 100644 index 00000000..c304c5ce --- /dev/null +++ b/src/schemas/query/report/ForwardReport.ts @@ -0,0 +1,71 @@ +import { GraphQLString } from "graphql"; +import { getForwards as getLnForwards } from "ln-service"; +import { logger } from "../../../helpers/logger"; +import { requestLimiter } from "../../../helpers/rateLimiter"; +import { groupBy } from "underscore"; +import { + subHours, + subDays, + differenceInHours, + differenceInCalendarDays +} from "date-fns"; +import { reduceForwardArray } from "./Helpers"; +import { ForwardCompleteProps } from "./ForwardReport.interface"; + +export const getForwardReport = { + type: GraphQLString, + args: { + time: { + type: GraphQLString + } + }, + resolve: async (root: any, params: any, context: any) => { + await requestLimiter(context.ip, params, "getForwardReport", 1, "1s"); + const { lnd } = context; + + let startDate = new Date(); + const endDate = new Date(); + let days = 7; + + if (params.time === "month") { + startDate = subDays(endDate, 30); + days = 30; + } else if (params.time === "week") { + startDate = subDays(endDate, 7); + } else { + startDate = subHours(endDate, 24); + } + + try { + const forwardsList: ForwardCompleteProps = await getLnForwards({ + lnd: lnd, + after: startDate, + before: endDate, + limit: 10000 + }); + + if (params.time === "month" || params.time === "week") { + const orderedDay = groupBy(forwardsList.forwards, item => { + return ( + days - differenceInCalendarDays(endDate, new Date(item.created_at)) + ); + }); + + const reducedOrderedDay = reduceForwardArray(orderedDay); + + return JSON.stringify(reducedOrderedDay); + } else { + const orderedHour = groupBy(forwardsList.forwards, item => { + return 24 - differenceInHours(endDate, new Date(item.created_at)); + }); + + const reducedOrderedHour = reduceForwardArray(orderedHour); + + return JSON.stringify(reducedOrderedHour); + } + } catch (error) { + logger.error("Error getting forward report: %o", error); + throw new Error("Failed to get forward report."); + } + } +}; diff --git a/src/schemas/query/report/Helpers.ts b/src/schemas/query/report/Helpers.ts new file mode 100644 index 00000000..b29184e3 --- /dev/null +++ b/src/schemas/query/report/Helpers.ts @@ -0,0 +1,58 @@ +import { reduce, sortBy } from "underscore"; +import { + ForwardProps, + ReduceObjectProps, + FinalProps, + FinalList, + ListProps, + CountProps, + ChannelCounts +} from "./ForwardReport.interface"; + +export const reduceForwardArray = (list: ListProps): FinalList => { + let reducedOrder: FinalList = {}; + for (const key in list) { + if (list.hasOwnProperty(key)) { + const element: ForwardProps[] = list[key]; + const reducedArray: ReduceObjectProps = reduce(element, (a, b) => { + return { + fee: a.fee + b.fee, + tokens: a.tokens + b.tokens + }; + }); + reducedOrder[key] = { + amount: element.length, + ...reducedArray + } as FinalProps; + } + } + + return reducedOrder; +}; + +export const countArray = ( + list: ForwardProps[], + type: boolean +): ChannelCounts[] => { + const count: CountProps = {}; + list + .map(item => { + return type ? item.incoming_channel : item.outgoing_channel; + }) + .forEach(channel => { + count[channel] = (count[channel] || 0) + 1; + }); + + const mapped: ChannelCounts[] = []; + for (const key in count) { + if (count.hasOwnProperty(key)) { + const element = count[key]; + mapped.push({ + name: key, + count: element + }); + } + } + sortBy(mapped, "count"); + return mapped; +}; diff --git a/src/schemas/query/report/index.ts b/src/schemas/query/report/index.ts new file mode 100644 index 00000000..08210eeb --- /dev/null +++ b/src/schemas/query/report/index.ts @@ -0,0 +1,7 @@ +import { getForwardReport } from "./ForwardReport"; +import { getForwardChannelsReport } from "./ForwardChannels"; + +export const reportQueries = { + getForwardReport, + getForwardChannelsReport +}; diff --git a/yarn.lock b/yarn.lock index 01845bb2..51a3415b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -341,6 +341,11 @@ dependencies: source-map "^0.6.1" +"@types/underscore@^1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.9.4.tgz#22d1a3e6b494608e430221ec085fa0b7ccee7f33" + integrity sha512-CjHWEMECc2/UxOZh0kpiz3lEyX2Px3rQS9HzD20lxMvx571ivOBQKeLnqEjxUY0BMgp6WJWo/pQLRBwMW5v4WQ== + "@types/webpack-env@^1.14.1": version "1.14.1" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.14.1.tgz#0d8a53f308f017c53a5ddc3d07f4d6fa76b790d7" @@ -1752,6 +1757,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-fns@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.8.1.tgz#2109362ccb6c87c3ca011e9e31f702bc09e4123b" + integrity sha512-EL/C8IHvYRwAHYgFRse4MGAPSqlJVlOrhVYZ75iQBKrnv+ZedmYsgwH3t+BCDuZDXpoo07+q9j4qgSSOa7irJg== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -5292,6 +5302,11 @@ typescript@^3.6.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ== +underscore@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" + integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"