Merge pull request #236 from apotdevin/fix/payment-pagination

fix: payment pagination
This commit is contained in:
Anthony Potdevin 2021-04-10 15:54:26 +02:00 committed by GitHub
commit b30aee8b6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 296 additions and 165 deletions

View file

@ -42,7 +42,36 @@ function createApolloClient(context?: ResolverContext) {
credentials: 'same-origin',
ssrMode: typeof window === 'undefined',
link: createIsomorphLink(context),
cache: new InMemoryCache(possibleTypes),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
getResume: {
keyArgs: [],
merge(existing, incoming, { args }) {
if (!existing) {
return incoming;
}
const { offset } = args || {};
const merged = existing?.resume ? existing.resume.slice(0) : [];
for (let i = 0; i < incoming.resume.length; ++i) {
merged[offset + i] = incoming.resume[i];
}
return {
...existing,
offset: incoming.offset,
resume: merged,
};
},
},
},
},
},
...possibleTypes,
}),
defaultOptions: {
query: {
fetchPolicy: 'cache-first',

View file

@ -1,10 +1,7 @@
import React, { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
import { InvoiceCard } from 'src/views/transactions/InvoiceCard';
import {
useGetResumeQuery,
GetResumeQuery,
} from 'src/graphql/queries/__generated__/getResume.generated';
import { useGetResumeQuery } from 'src/graphql/queries/__generated__/getResume.generated';
import { GridWrapper } from 'src/components/gridWrapper/GridWrapper';
import { NextPageContext } from 'next';
@ -47,36 +44,46 @@ const Rotation = styled.div<RotationProps>`
const TransactionsView = () => {
const [isPolling, setIsPolling] = useState(false);
const [indexOpen, setIndexOpen] = useState(0);
const [token, setToken] = useState('');
const [offset, setOffset] = useState(0);
const {
loading,
data,
fetchMore,
startPolling,
stopPolling,
networkStatus,
} = useGetResumeQuery({
ssr: false,
variables: { token: '' },
variables: { offset: 0 },
notifyOnNetworkStatusChange: true,
onError: error => toast.error(getErrorContent(error)),
});
const isLoading = networkStatus === 1;
const isRefetching = networkStatus === 3;
const loadingOrRefetching = isLoading || isRefetching;
useEffect(() => {
if (!loading && data && data.getResume && data.getResume.token) {
setToken(data.getResume.token);
if (!isLoading && data?.getResume?.offset) {
setOffset(data.getResume.offset);
}
}, [data, loading]);
}, [data, isLoading]);
useEffect(() => {
return () => stopPolling();
}, [stopPolling]);
if (loading || !data || !data.getResume) {
if (isLoading || !data || !data.getResume) {
return <LoadingCard title={'Transactions'} />;
}
const resumeList = data.getResume.resume;
const handleClick = (limit: number) =>
fetchMore({ variables: { offset, limit } });
return (
<>
<FlowBox />
@ -130,36 +137,31 @@ const TransactionsView = () => {
return null;
})}
<ColorButton
loading={loadingOrRefetching}
disabled={loadingOrRefetching}
fullWidth={true}
withMargin={'16px 0 0'}
onClick={() => {
fetchMore({
variables: { token },
updateQuery: (
prev,
{ fetchMoreResult }: { fetchMoreResult?: GetResumeQuery }
): GetResumeQuery => {
if (!fetchMoreResult?.getResume) return prev;
const newToken = fetchMoreResult.getResume.token || '';
const prevEntries = prev?.getResume?.resume || [];
const newEntries = fetchMoreResult?.getResume?.resume || [];
const allTransactions = newToken
? [...prevEntries, ...newEntries]
: prevEntries;
return {
getResume: {
token: newToken,
resume: allTransactions,
__typename: 'getResumeType',
},
};
},
});
}}
onClick={() => handleClick(1)}
>
Show More
Get 1 More Day
</ColorButton>
<ColorButton
loading={loadingOrRefetching}
disabled={loadingOrRefetching}
fullWidth={true}
withMargin={'16px 0 0'}
onClick={() => handleClick(7)}
>
Get 1 More Week
</ColorButton>
<ColorButton
loading={loadingOrRefetching}
disabled={loadingOrRefetching}
fullWidth={true}
withMargin={'16px 0 0'}
onClick={() => handleClick(30)}
>
Get 1 More Month
</ColorButton>
</Card>
</CardWithTitle>

View file

@ -1,10 +1,14 @@
import { getChannel, getNode } from 'ln-service';
import { toWithError } from 'server/helpers/async';
import { compareDesc } from 'date-fns';
import { getChannel, getNode, getPayments, getInvoices } from 'ln-service';
import { to, toWithError } from 'server/helpers/async';
import { logger } from 'server/helpers/logger';
import {
ChannelType,
GetChannelType,
GetInvoicesType,
GetNodeType,
GetPaymentsType,
LndObject,
} from 'server/types/ln-service.types';
export const getNodeFromChannel = async (
@ -85,3 +89,167 @@ export const getNodeFromChannel = async (
channel_id: channelId,
};
};
export const getPaymentsBetweenDates = async ({
lnd,
from,
until,
batch = 25,
}: {
lnd: LndObject | null;
from?: string;
until?: string;
batch?: number;
}) => {
const paymentList = await to<GetPaymentsType>(
getPayments({
lnd,
limit: batch,
})
);
if (!paymentList?.payments?.length) {
return [];
}
if (!from || !until) {
return paymentList.payments;
}
const firstPayment = paymentList.payments[0];
const isOutOf =
compareDesc(new Date(firstPayment.created_at), new Date(until)) === 1;
const filterArray = (payment: GetPaymentsType['payments'][0]) => {
const date = payment.created_at;
const last = compareDesc(new Date(until), new Date(date)) === 1;
const first = compareDesc(new Date(date), new Date(from)) === 1;
return last && first;
};
if (isOutOf) {
return paymentList.payments.filter(filterArray);
}
let completePayments = paymentList.payments;
let nextToken = paymentList.next;
let finished = false;
while (!finished) {
const newPayments = await to<GetPaymentsType>(
getPayments({
lnd,
token: nextToken,
})
);
if (!newPayments?.payments?.length) {
finished = true;
break;
}
completePayments = [...completePayments, ...newPayments.payments];
const firstPayment = newPayments.payments[0];
if (compareDesc(new Date(firstPayment.created_at), new Date(until)) === 1) {
finished = true;
break;
}
if (!newPayments.next) {
finished = true;
break;
}
nextToken = newPayments.next;
}
return completePayments.filter(filterArray);
};
export const getInvoicesBetweenDates = async ({
lnd,
from,
until,
batch = 25,
}: {
lnd: LndObject | null;
from?: string;
until?: string;
batch?: number;
}) => {
const invoiceList = await to<GetInvoicesType>(
getInvoices({
lnd,
limit: batch,
})
);
if (!invoiceList?.invoices?.length) {
return [];
}
if (!from || !until) {
return invoiceList.invoices;
}
const firstInvoice = invoiceList.invoices[0];
const firstDate = firstInvoice.confirmed_at || firstInvoice.created_at;
const isOutOf = compareDesc(new Date(firstDate), new Date(until)) === 1;
const filterArray = (invoice: GetInvoicesType['invoices'][0]) => {
const date = invoice.confirmed_at || invoice.created_at;
const last = compareDesc(new Date(until), new Date(date)) === 1;
const first = compareDesc(new Date(date), new Date(from)) === 1;
return last && first;
};
if (isOutOf) {
return invoiceList.invoices.filter(filterArray);
}
let completeInvoices = invoiceList.invoices;
let nextToken = invoiceList.next;
let finished = false;
while (!finished) {
const newInvoices = await to<GetInvoicesType>(
getInvoices({
lnd,
token: nextToken,
})
);
if (!newInvoices?.invoices?.length) {
finished = true;
break;
}
completeInvoices = [...completeInvoices, ...newInvoices.invoices];
const firstNewInvoice = newInvoices.invoices[0];
const firstNewDate =
firstNewInvoice.confirmed_at || firstNewInvoice.created_at;
if (compareDesc(new Date(firstNewDate), new Date(until)) === 1) {
finished = true;
break;
}
if (!newInvoices.next) {
finished = true;
break;
}
nextToken = newInvoices.next;
}
return completeInvoices.filter(filterArray);
};

View file

@ -1,68 +1,40 @@
import { getPayments, getInvoices } from 'ln-service';
import { compareDesc } from 'date-fns';
import { subDays } from 'date-fns';
import { sortBy } from 'underscore';
import { ContextType } from 'server/types/apiTypes';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { to } from 'server/helpers/async';
import {
GetInvoicesType,
GetPaymentsType,
InvoiceType,
PaymentType,
} from 'server/types/ln-service.types';
import { InvoiceType, PaymentType } from 'server/types/ln-service.types';
import { decodeMessages } from 'server/helpers/customRecords';
import { getInvoicesBetweenDates, getPaymentsBetweenDates } from './helpers';
type TransactionType = InvoiceType | PaymentType;
type TransactionWithType = { isTypeOf: string } & TransactionType;
export const transactionResolvers = {
Query: {
getResume: async (_: undefined, params: any, context: ContextType) => {
getResume: async (
_: undefined,
{ offset, limit }: { offset?: number; limit?: number },
context: ContextType
) => {
await requestLimiter(context.ip, 'payments');
const { lnd } = context;
const invoiceProps = params.token
? { token: params.token }
: { limit: 25 };
const start = offset || 0;
const end = (offset || 0) + (limit || 7);
let lastInvoiceDate = '';
let firstInvoiceDate = '';
let token = '';
let withInvoices = true;
const today = new Date();
const startDate = subDays(today, start).toISOString();
const endDate = subDays(today, end).toISOString();
const invoiceList = await to<GetInvoicesType>(
getInvoices({
lnd,
...invoiceProps,
})
);
const invoices = invoiceList.invoices.map(invoice => {
return {
type: 'invoice',
date: invoice.confirmed_at || invoice.created_at,
...invoice,
isTypeOf: 'InvoiceType',
};
const payments = await getPaymentsBetweenDates({
lnd,
from: startDate,
until: endDate,
batch: 25,
});
if (invoices.length <= 0) {
withInvoices = false;
} else {
const { date } = invoices[invoices.length - 1];
firstInvoiceDate = invoices[0].date;
lastInvoiceDate = date;
token = invoiceList.next || '';
}
const paymentList = await to<GetPaymentsType>(
getPayments({
lnd,
})
);
const payments = paymentList.payments.map(payment => ({
const mappedPayments = payments.map(payment => ({
...payment,
type: 'payment',
date: payment.created_at,
@ -71,34 +43,32 @@ export const transactionResolvers = {
isTypeOf: 'PaymentType',
}));
const filterArray = (payment: typeof payments[number]) => {
const last =
compareDesc(new Date(lastInvoiceDate), new Date(payment.date)) === 1;
const first = params.token
? compareDesc(new Date(payment.date), new Date(firstInvoiceDate)) ===
1
: true;
return last && first;
};
const invoices = await getInvoicesBetweenDates({
lnd,
from: startDate,
until: endDate,
batch: 25,
});
const filteredPayments = withInvoices
? payments.filter(filterArray)
: payments;
const invoicesWithMessages = invoices.map(i => ({
...i,
messages: i.payments
.map(p => decodeMessages(p.messages))
.filter(Boolean),
}));
const mappedInvoices = invoices.map(invoice => {
return {
type: 'invoice',
date: invoice.confirmed_at || invoice.created_at,
...invoice,
isTypeOf: 'InvoiceType',
messages: invoice.payments
.map(p => decodeMessages(p.messages))
.filter(Boolean),
};
});
const resume = sortBy(
[...invoicesWithMessages, ...filteredPayments],
[...mappedInvoices, ...mappedPayments],
'date'
).reverse();
return {
token,
offset: end,
resume,
};
},

View file

@ -72,7 +72,7 @@ export const transactionTypes = gql`
union Transaction = InvoiceType | PaymentType
type getResumeType {
token: String
offset: Int
resume: [Transaction]
}
`;

View file

@ -62,7 +62,7 @@ export const queryTypes = gql`
getNode(publicKey: String!, withoutChannels: Boolean): Node!
decodeRequest(request: String!): decodeType
getWalletInfo: walletInfoType
getResume(token: String): getResumeType
getResume(offset: Int, limit: Int): getResumeType
getForwards(days: Int!): [Forward]!
getBitcoinPrice(logger: Boolean, currency: String): String
getBitcoinFees(logger: Boolean): bitcoinFeeType

View file

@ -5,7 +5,8 @@ import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {}
export type GetResumeQueryVariables = Types.Exact<{
token?: Types.Maybe<Types.Scalars['String']>;
offset?: Types.Maybe<Types.Scalars['Int']>;
limit?: Types.Maybe<Types.Scalars['Int']>;
}>;
@ -13,7 +14,7 @@ export type GetResumeQuery = (
{ __typename?: 'Query' }
& { getResume?: Types.Maybe<(
{ __typename?: 'getResumeType' }
& Pick<Types.GetResumeType, 'token'>
& Pick<Types.GetResumeType, 'offset'>
& { resume?: Types.Maybe<Array<Types.Maybe<(
{ __typename?: 'InvoiceType' }
& Pick<Types.InvoiceType, 'chain_address' | 'confirmed_at' | 'created_at' | 'description' | 'description_hash' | 'expires_at' | 'id' | 'is_canceled' | 'is_confirmed' | 'is_held' | 'is_private' | 'is_push' | 'received' | 'received_mtokens' | 'request' | 'secret' | 'tokens' | 'type' | 'date'>
@ -43,9 +44,9 @@ export type GetResumeQuery = (
export const GetResumeDocument = gql`
query GetResume($token: String) {
getResume(token: $token) {
token
query GetResume($offset: Int, $limit: Int) {
getResume(offset: $offset, limit: $limit) {
offset
resume {
... on InvoiceType {
chain_address
@ -117,7 +118,8 @@ export const GetResumeDocument = gql`
* @example
* const { data, loading, error } = useGetResumeQuery({
* variables: {
* token: // value for 'token'
* offset: // value for 'offset'
* limit: // value for 'limit'
* },
* });
*/

View file

@ -931,47 +931,6 @@ Object {
}
`;
exports[`Query tests "GET_RESUME" matches snapshot 1`] = `
Object {
"data": Object {
"getResume": Object {
"resume": Array [
Object {
"chain_address": "string",
"confirmed_at": "string",
"created_at": "string",
"date": "string",
"description": "string",
"description_hash": "string",
"expires_at": "string",
"id": "string",
"is_canceled": true,
"is_confirmed": true,
"is_held": true,
"is_private": true,
"is_push": true,
"messages": Array [],
"received": 1000,
"received_mtokens": "string",
"request": "string",
"secret": "string",
"tokens": "1000",
"type": "invoice",
},
],
"token": "string",
},
},
"errors": undefined,
"extensions": undefined,
"http": Object {
"headers": Headers {
Symbol(map): Object {},
},
},
}
`;
exports[`Query tests "GET_SERVER_ACCOUNTS" matches snapshot 1`] = `
Object {
"data": Object {

View file

@ -19,7 +19,7 @@ import {
} from '../getNodeInfo';
import { GET_PEERS } from '../getPeers';
import { GET_PENDING_CHANNELS } from '../getPendingChannels';
import { GET_RESUME } from '../getResume';
// import { GET_RESUME } from '../getResume';
import { GET_SERVER_ACCOUNTS } from '../getServerAccounts';
import { GET_TIME_HEALTH } from '../getTimeHealth';
import { GET_UTXOS } from '../getUtxos';
@ -69,7 +69,7 @@ const cases: CaseType[] = [
['GET_CONNECT_INFO', { query: GET_CONNECT_INFO }],
['GET_PEERS', { query: GET_PEERS }],
['GET_PENDING_CHANNELS', { query: GET_PENDING_CHANNELS }],
['GET_RESUME', { query: GET_RESUME }],
// ['GET_RESUME', { query: GET_RESUME }],
['GET_SERVER_ACCOUNTS', { query: GET_SERVER_ACCOUNTS }],
['GET_TIME_HEALTH', { query: GET_TIME_HEALTH }],
['GET_UTXOS', { query: GET_UTXOS }],

View file

@ -1,9 +1,9 @@
import { gql } from '@apollo/client';
export const GET_RESUME = gql`
query GetResume($token: String) {
getResume(token: $token) {
token
query GetResume($offset: Int, $limit: Int) {
getResume(offset: $offset, limit: $limit) {
offset
resume {
... on InvoiceType {
chain_address

View file

@ -147,7 +147,8 @@ export type QueryDecodeRequestArgs = {
export type QueryGetResumeArgs = {
token?: Maybe<Scalars['String']>;
offset?: Maybe<Scalars['Int']>;
limit?: Maybe<Scalars['Int']>;
};
@ -906,7 +907,7 @@ export type Transaction = InvoiceType | PaymentType;
export type GetResumeType = {
__typename?: 'getResumeType';
token?: Maybe<Scalars['String']>;
offset?: Maybe<Scalars['Int']>;
resume?: Maybe<Array<Maybe<Transaction>>>;
};