refactor: ♻️ forward channels

This commit is contained in:
Anthony Potdevin 2020-11-15 11:55:34 +01:00
parent 72860f334e
commit 05fd6b2573
No known key found for this signature in database
GPG key ID: 4403F1DFBE779457
10 changed files with 144 additions and 334 deletions

View file

@ -1,10 +1,8 @@
import { getForwardChannelsReport } from './resolvers/getForwardChannelsReport';
import { getInOut } from './resolvers/getInOut';
import { getChannelReport } from './resolvers/getChannelReport';
export const widgetResolvers = {
Query: {
getForwardChannelsReport,
getInOut,
getChannelReport,
},

View file

@ -1,157 +0,0 @@
import { getForwards, getWalletInfo, getClosedChannels } from 'ln-service';
import { subHours, subDays } from 'date-fns';
import { sortBy } from 'underscore';
import { ContextType } from 'server/types/apiTypes';
import { getNodeFromChannel } from 'server/helpers/getNodeFromChannel';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { to } from 'server/helpers/async';
import {
GetForwardsType,
GetWalletInfoType,
GetClosedChannelsType,
} from 'server/types/ln-service.types';
import { countArray, countRoutes } from './helpers';
export const getForwardChannelsReport = async (
_: undefined,
params: any,
context: ContextType
) => {
await requestLimiter(context.ip, 'forwardChannels');
const { lnd } = context;
let startDate = new Date();
const endDate = new Date();
if (params.time === 'week') {
startDate = subDays(endDate, 7);
} else if (params.time === 'month') {
startDate = subDays(endDate, 30);
} else if (params.time === 'quarter_year') {
startDate = subDays(endDate, 90);
} else if (params.time === 'half_year') {
startDate = subDays(endDate, 180);
} else if (params.time === 'year') {
startDate = subDays(endDate, 360);
} else {
startDate = subHours(endDate, 24);
}
const closedChannels = await to<GetClosedChannelsType>(
getClosedChannels({
lnd,
})
);
const getRouteAlias = (array: any[], publicKey: string) =>
Promise.all(
array.map(async channel => {
const nodeAliasIn = await getNodeFromChannel(
channel.in,
publicKey,
lnd,
closedChannels?.channels.find(c => c.id === channel.in)
);
const nodeAliasOut = await getNodeFromChannel(
channel.out,
publicKey,
lnd,
closedChannels?.channels.find(c => c.id === channel.out)
);
return {
aliasIn: nodeAliasIn.alias,
colorIn: nodeAliasIn.color,
aliasOut: nodeAliasOut.alias,
colorOut: nodeAliasOut.color,
...channel,
};
})
);
const getAlias = (array: any[], publicKey: string) =>
Promise.all(
array.map(async channel => {
const nodeAlias = await getNodeFromChannel(
channel.name,
publicKey,
lnd,
closedChannels?.channels.find(c => c.id === channel.name)
);
return {
alias: nodeAlias.alias,
color: nodeAlias.color,
...channel,
};
})
);
const forwardsList = await to<GetForwardsType>(
getForwards({
lnd,
after: startDate,
before: endDate,
})
);
const walletInfo = await to<GetWalletInfoType>(
getWalletInfo({
lnd,
})
);
let forwards = forwardsList.forwards;
let next = forwardsList.next;
let finishedFetching = false;
if (!next || !forwards || forwards.length <= 0) {
finishedFetching = true;
}
while (!finishedFetching) {
if (next) {
const moreForwards = await to<GetForwardsType>(
getForwards({ lnd, token: next })
);
forwards = [...forwards, ...moreForwards.forwards];
if (moreForwards.next) {
next = moreForwards.next;
} else {
finishedFetching = true;
}
} else {
finishedFetching = true;
}
}
if (params.type === 'route') {
const mapped = forwards.map(forward => {
return {
route: `${forward.incoming_channel} - ${forward.outgoing_channel}`,
...forward,
};
});
const grouped = countRoutes(mapped);
const routeAlias = await getRouteAlias(grouped, walletInfo.public_key);
const sortedRoute = sortBy(routeAlias, params.order).reverse().slice(0, 10);
return JSON.stringify(sortedRoute);
}
if (params.type === 'incoming') {
const incomingCount = countArray(forwards, true);
const incomingAlias = await getAlias(incomingCount, walletInfo.public_key);
const sortedInCount = sortBy(incomingAlias, params.order)
.reverse()
.slice(0, 10);
return JSON.stringify(sortedInCount);
}
const outgoingCount = countArray(forwards, false);
const outgoingAlias = await getAlias(outgoingCount, walletInfo.public_key);
const sortedOutCount = sortBy(outgoingAlias, params.order)
.reverse()
.slice(0, 10);
return JSON.stringify(sortedOutCount);
};

View file

@ -1,5 +1,3 @@
import { reduce, groupBy } from 'underscore';
import { ForwardType } from 'server/types/ln-service.types';
import { InOutProps, InOutListProps } from './interface';
export const reduceInOutArray = (list: InOutListProps) => {
@ -7,8 +5,7 @@ export const reduceInOutArray = (list: InOutListProps) => {
for (const key in list) {
if (Object.prototype.hasOwnProperty.call(list, key)) {
const element: InOutProps[] = list[key];
const reducedArray: InOutProps = reduce(
element,
const reducedArray: InOutProps = element.reduce(
(a, b) => ({
tokens: a.tokens + b.tokens,
}),
@ -23,62 +20,3 @@ export const reduceInOutArray = (list: InOutListProps) => {
}
return reducedOrder;
};
export const countArray = (list: ForwardType[], type: boolean) => {
const inOrOut = type ? 'incoming_channel' : 'outgoing_channel';
const grouped = groupBy(list, inOrOut);
const channelInfo = [];
for (const key in grouped) {
if (Object.prototype.hasOwnProperty.call(grouped, key)) {
const element = grouped[key];
const fee = element
.map(forward => forward.fee)
.reduce((p: number, c: number) => p + c);
const tokens = element
.map(forward => forward.tokens)
.reduce((p: number, c: number) => p + c);
channelInfo.push({
name: key,
amount: element.length,
fee,
tokens,
});
}
}
return channelInfo;
};
export const countRoutes = (list: ForwardType[]) => {
const grouped = groupBy(list, 'route');
const channelInfo = [];
for (const key in grouped) {
if (Object.prototype.hasOwnProperty.call(grouped, key)) {
const element = grouped[key];
const fee = element
.map(forward => forward.fee)
.reduce((p: number, c: number) => p + c);
const tokens = element
.map(forward => forward.tokens)
.reduce((p: number, c: number) => p + c);
channelInfo.push({
route: key,
in: element[0].incoming_channel,
out: element[0].outgoing_channel,
amount: element.length,
fee,
tokens,
});
}
}
return channelInfo;
};

View file

@ -1,51 +0,0 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type GetForwardChannelsReportQueryVariables = Types.Exact<{
time?: Types.Maybe<Types.Scalars['String']>;
order?: Types.Maybe<Types.Scalars['String']>;
type?: Types.Maybe<Types.Scalars['String']>;
}>;
export type GetForwardChannelsReportQuery = (
{ __typename?: 'Query' }
& Pick<Types.Query, 'getForwardChannelsReport'>
);
export const GetForwardChannelsReportDocument = gql`
query GetForwardChannelsReport($time: String, $order: String, $type: String) {
getForwardChannelsReport(time: $time, order: $order, type: $type)
}
`;
/**
* __useGetForwardChannelsReportQuery__
*
* To run a query within a React component, call `useGetForwardChannelsReportQuery` and pass it any options that fit your needs.
* When your component renders, `useGetForwardChannelsReportQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetForwardChannelsReportQuery({
* variables: {
* time: // value for 'time'
* order: // value for 'order'
* type: // value for 'type'
* },
* });
*/
export function useGetForwardChannelsReportQuery(baseOptions?: Apollo.QueryHookOptions<GetForwardChannelsReportQuery, GetForwardChannelsReportQueryVariables>) {
return Apollo.useQuery<GetForwardChannelsReportQuery, GetForwardChannelsReportQueryVariables>(GetForwardChannelsReportDocument, baseOptions);
}
export function useGetForwardChannelsReportLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetForwardChannelsReportQuery, GetForwardChannelsReportQueryVariables>) {
return Apollo.useLazyQuery<GetForwardChannelsReportQuery, GetForwardChannelsReportQueryVariables>(GetForwardChannelsReportDocument, baseOptions);
}
export type GetForwardChannelsReportQueryHookResult = ReturnType<typeof useGetForwardChannelsReportQuery>;
export type GetForwardChannelsReportLazyQueryHookResult = ReturnType<typeof useGetForwardChannelsReportLazyQuery>;
export type GetForwardChannelsReportQueryResult = Apollo.QueryResult<GetForwardChannelsReportQuery, GetForwardChannelsReportQueryVariables>;

View file

@ -1,7 +0,0 @@
import { gql } from '@apollo/client';
export const GET_FORWARD_CHANNELS_REPORT = gql`
query GetForwardChannelsReport($time: String, $order: String, $type: String) {
getForwardChannelsReport(time: $time, order: $order, type: $type)
}
`;

View file

@ -7,28 +7,23 @@ import { ResponsiveSingle } from 'src/components/generic/Styled';
import { ReportType, ReportDuration, FlowReportType } from './ForwardReport';
interface ButtonProps {
isTime: ReportDuration;
isType: ReportType;
days: number;
order: ReportType;
setDays: (days: number) => void;
setIsTime: (text: ReportDuration) => void;
setIsType: (text: ReportType) => void;
setOrder: (text: ReportType) => void;
}
export const ButtonRow: React.FC<ButtonProps> = ({
isTime,
setIsTime,
days,
setDays,
isType,
setIsType,
order,
setOrder,
}) => {
const timeButton = (time: ReportDuration, title: string, days: number) => (
const timeButton = (title: string, buttonDays: number) => (
<SingleButton
withPadding={'4px 8px'}
onClick={() => {
setIsTime(time);
setDays(days);
}}
selected={isTime === time}
onClick={() => setDays(buttonDays)}
selected={days === buttonDays}
>
{title}
</SingleButton>
@ -37,8 +32,8 @@ export const ButtonRow: React.FC<ButtonProps> = ({
const typeButton = (type: ReportType, title: string) => (
<SingleButton
withPadding={'4px 8px'}
onClick={() => setIsType(type)}
selected={isType === type}
onClick={() => setOrder(type)}
selected={order === type}
>
{title}
</SingleButton>
@ -47,12 +42,12 @@ export const ButtonRow: React.FC<ButtonProps> = ({
return (
<ResponsiveSingle>
<MultiButton>
{timeButton('day', '1D', 1)}
{timeButton('week', '1W', 7)}
{timeButton('month', '1M', 30)}
{timeButton('quarter_year', '3M', 90)}
{timeButton('half_year', '6M', 180)}
{timeButton('year', '1Y', 360)}
{timeButton('1D', 1)}
{timeButton('1W', 7)}
{timeButton('1M', 30)}
{timeButton('3M', 90)}
{timeButton('6M', 180)}
{timeButton('1Y', 360)}
</MultiButton>
<MultiButton>
{typeButton('amount', 'Amount')}

View file

@ -2,11 +2,12 @@ import React, { useState } from 'react';
import { toast } from 'react-toastify';
import { GitCommit, ArrowDown, ArrowUp } from 'react-feather';
import styled from 'styled-components';
import { useGetForwardChannelsReportQuery } from 'src/graphql/queries/__generated__/getForwardChannelsReport.generated';
import {
MultiButton,
SingleButton,
} from 'src/components/buttons/multiButton/MultiButton';
import { useGetForwardsPastDaysQuery } from 'src/graphql/queries/__generated__/getForwardsPastDays.generated';
import { Forward } from 'src/graphql/types';
import { getErrorContent } from '../../../../utils/error';
import {
DarkSubTitle,
@ -16,7 +17,8 @@ import { LoadingCard } from '../../../../components/loading/LoadingCard';
import { getPrice } from '../../../../components/price/Price';
import { useConfigState } from '../../../../context/ConfigContext';
import { usePriceState } from '../../../../context/PriceContext';
import { ReportType, ReportDuration } from './ForwardReport';
import { ReportType } from './ForwardReport';
import { orderForwardChannels } from './helpers';
import { CardContent } from '.';
const ChannelRow = styled.div`
@ -39,11 +41,12 @@ const LastTableLine = styled(TableLine)`
`;
type Props = {
isTime: ReportDuration;
isType: ReportType;
days: number;
order: ReportType;
};
type ParsedRouteType = {
route: string;
aliasIn: string;
aliasOut: string;
fee: number;
@ -59,16 +62,16 @@ type ParsedChannelType = {
amount: number;
};
export const ForwardChannelsReport = ({ isTime, isType }: Props) => {
export const ForwardChannelsReport = ({ days, order }: Props) => {
const [type, setType] = useState<'route' | 'incoming' | 'outgoing'>('route');
const { currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, displayValues, priceContext);
const { data, loading } = useGetForwardChannelsReportQuery({
const { data, loading } = useGetForwardsPastDaysQuery({
ssr: false,
variables: { time: isTime, order: isType, type },
variables: { days },
onError: error => toast.error(getErrorContent(error)),
});
@ -76,14 +79,15 @@ export const ForwardChannelsReport = ({ isTime, isType }: Props) => {
return <LoadingCard noCard={true} title={'Forward Report'} />;
}
// TODO: JSON.parse is really bad... Absolutely no type safety at all
const parsed: (ParsedChannelType | ParsedRouteType)[] = JSON.parse(
data.getForwardChannelsReport || '[]'
const forwardArray = orderForwardChannels(
type,
order,
data.getForwardsPastDays as Forward[]
);
const getFormatString = (amount: number | string) => {
if (typeof amount === 'string') return amount;
if (isType !== 'amount') {
if (order !== 'amount') {
return format({ amount });
}
return amount;
@ -94,7 +98,7 @@ export const ForwardChannelsReport = ({ isTime, isType }: Props) => {
<ChannelRow key={index}>
<TableLine>{channel.aliasIn}</TableLine>
<TableLine>{channel.aliasOut}</TableLine>
<LastTableLine>{getFormatString(channel[isType])}</LastTableLine>
<LastTableLine>{getFormatString(channel[order])}</LastTableLine>
</ChannelRow>
));
@ -115,7 +119,7 @@ export const ForwardChannelsReport = ({ isTime, isType }: Props) => {
<ChannelRow key={index}>
<TableLine>{`${channel.alias}`}</TableLine>
<DarkSubTitle>{`${channel.name}`}</DarkSubTitle>
<LastTableLine>{getFormatString(channel[isType])}</LastTableLine>
<LastTableLine>{getFormatString(channel[order])}</LastTableLine>
</ChannelRow>
));
@ -184,14 +188,14 @@ export const ForwardChannelsReport = ({ isTime, isType }: Props) => {
}
};
if (parsed.length <= 0) {
if (forwardArray.length <= 0) {
return null;
}
return (
<CardContent>
{renderTitle()}
{renderContent(parsed)}
{renderContent(forwardArray)}
</CardContent>
);
};

View file

@ -36,10 +36,10 @@ export type FlowReportType = 'tokens' | 'amount';
interface Props {
days: number;
isType: ReportType;
order: ReportType;
}
export const ForwardReport = ({ days, isType }: Props) => {
export const ForwardReport = ({ days, order }: Props) => {
const { theme, currency, displayValues } = useConfigState();
const priceContext = usePriceState();
const format = getPrice(currency, displayValues, priceContext);
@ -72,7 +72,7 @@ export const ForwardReport = ({ days, isType }: Props) => {
}
const getLabelString = (value: number) => {
if (isType === 'amount') {
if (order === 'amount') {
return numeral(value).format('0,0');
}
return format({ amount: value });
@ -84,7 +84,7 @@ export const ForwardReport = ({ days, isType }: Props) => {
);
const total = getLabelString(
reduced.map(x => x[isType]).reduce((a, c) => a + c, 0)
reduced.map(x => x[order]).reduce((a, c) => a + c, 0)
);
const renderContent = () => {
@ -101,14 +101,14 @@ export const ForwardReport = ({ days, isType }: Props) => {
height={110}
padding={{
top: 20,
left: isType === 'tokens' ? 80 : 50,
left: order === 'tokens' ? 80 : 50,
right: 50,
bottom: 10,
}}
containerComponent={
<VictoryVoronoiContainer
voronoiDimension="x"
labels={({ datum }) => `${getLabelString(datum[isType])}`}
labels={({ datum }) => `${getLabelString(datum[order])}`}
labelComponent={<VictoryTooltip orientation={'bottom'} />}
/>
}
@ -131,13 +131,13 @@ export const ForwardReport = ({ days, isType }: Props) => {
axis: { stroke: 'transparent' },
}}
tickFormat={a =>
isType === 'tokens' ? format({ amount: a, breakNumber: true }) : a
order === 'tokens' ? format({ amount: a, breakNumber: true }) : a
}
/>
<VictoryBar
data={reduced}
x="period"
y={isType}
y={order}
style={{
data: {
fill: chartBarColor[theme],

View file

@ -1,5 +1,6 @@
import { differenceInCalendarDays, differenceInHours, subDays } from 'date-fns';
import groupBy from 'lodash.groupby';
import sortBy from 'lodash.sortby';
import { Forward } from 'src/graphql/types';
type ListProps = {
@ -58,3 +59,94 @@ export const orderAndReducedArray = (time: number, forwardArray: Forward[]) => {
return reducedOrderedDay;
};
const countRoutes = (list: Forward[]) => {
const grouped = groupBy(list, 'route');
const channelInfo = [];
for (const key in grouped) {
if (Object.prototype.hasOwnProperty.call(grouped, key)) {
const element = grouped[key];
const fee = element
.map(forward => forward.fee)
.reduce((p: number, c: number) => p + c);
const tokens = element
.map(forward => forward.tokens)
.reduce((p: number, c: number) => p + c);
channelInfo.push({
aliasIn: element[0].incoming_node?.alias || 'Unknown',
aliasOut: element[0].outgoing_node?.alias || 'Unknown',
route: key,
amount: element.length,
fee,
tokens,
});
}
}
return channelInfo;
};
const countArray = (list: Forward[], type: boolean) => {
const inOrOut = type ? 'incoming_channel' : 'outgoing_channel';
const grouped = groupBy(list, inOrOut);
const channelInfo = [];
for (const key in grouped) {
if (Object.prototype.hasOwnProperty.call(grouped, key)) {
const element = grouped[key];
const fee = element
.map(forward => forward.fee)
.reduce((p: number, c: number) => p + c);
const tokens = element
.map(forward => forward.tokens)
.reduce((p: number, c: number) => p + c);
const alias = type
? element[0].incoming_node?.alias || 'Unknown'
: element[0].outgoing_node?.alias || 'Unknown';
channelInfo.push({
alias,
name: key,
amount: element.length,
fee,
tokens,
});
}
}
return channelInfo;
};
export const orderForwardChannels = (
type: string,
order: string,
forwardArray: Forward[]
) => {
if (type === 'route') {
const mapped = forwardArray.map(forward => {
return {
route: `${forward.incoming_channel} - ${forward.outgoing_channel}`,
...forward,
};
});
const grouped = countRoutes(mapped);
const sortedRoute = sortBy(grouped, order).reverse().slice(0, 10);
return sortedRoute;
}
if (type === 'incoming') {
const incomingCount = countArray(forwardArray, true);
const sortedInCount = sortBy(incomingCount, order).reverse().slice(0, 10);
return sortedInCount;
}
const outgoingCount = countArray(forwardArray, false);
const sortedOutCount = sortBy(outgoingCount, order).reverse().slice(0, 10);
return sortedOutCount;
};

View file

@ -8,7 +8,7 @@ import {
Separation,
} from '../../../../components/generic/Styled';
import { mediaWidths } from '../../../../styles/Themes';
import { ForwardReport, ReportDuration, ReportType } from './ForwardReport';
import { ForwardReport, ReportType } from './ForwardReport';
import { ForwardChannelsReport } from './ForwardChannelReport';
import { ButtonRow } from './Buttons';
@ -24,9 +24,8 @@ export const CardContent = styled.div`
`;
export const ForwardBox = () => {
const [isTime, setIsTime] = useState<ReportDuration>('month');
const [days, setDays] = useState<number>(30);
const [isType, setIsType] = useState<ReportType>('amount');
const [order, setOrder] = useState<ReportType>('amount');
return (
<CardWithTitle>
@ -35,15 +34,14 @@ export const ForwardBox = () => {
</CardTitle>
<Card mobileCardPadding={'8px'}>
<ButtonRow
isTime={isTime}
isType={isType}
days={days}
order={order}
setDays={setDays}
setIsTime={setIsTime}
setIsType={setIsType}
setOrder={setOrder}
/>
<ForwardReport days={days} isType={isType} />
<ForwardReport days={days} order={order} />
<Separation />
<ForwardChannelsReport isTime={isTime} isType={isType} />
<ForwardChannelsReport days={days} order={order} />
</Card>
</CardWithTitle>
);