diff --git a/pages/tools.tsx b/pages/tools.tsx index 7696ff81..066b1bde 100644 --- a/pages/tools.tsx +++ b/pages/tools.tsx @@ -2,12 +2,14 @@ import React from 'react'; import { GridWrapper } from 'src/components/gridWrapper/GridWrapper'; import { withApollo } from 'config/client'; import { Bakery } from 'src/views/tools/bakery/Bakery'; +import { Accounting } from 'src/views/tools/accounting/Accounting'; import { BackupsView } from '../src/views/tools/backups/Backups'; import { MessagesView } from '../src/views/tools/messages/Messages'; import { WalletVersion } from '../src/views/tools/WalletVersion'; const ToolsView = () => ( <> + diff --git a/server/schema/bos/resolvers.ts b/server/schema/bos/resolvers.ts index 6e69e183..67d41bba 100644 --- a/server/schema/bos/resolvers.ts +++ b/server/schema/bos/resolvers.ts @@ -1,10 +1,13 @@ import { ContextType } from 'server/types/apiTypes'; import { getLnd } from 'server/helpers/helpers'; -import { rebalance } from 'balanceofsatoshis/swaps'; import { to } from 'server/helpers/async'; import { logger } from 'server/helpers/logger'; import { AuthType } from 'src/context/AccountContext'; +import { rebalance } from 'balanceofsatoshis/swaps'; +import { getAccountingReport } from 'balanceofsatoshis/balances'; +import request from '@alexbosworth/request'; + type RebalanceType = { auth: AuthType; avoid?: String[]; @@ -19,21 +22,79 @@ type RebalanceType = { target?: Number; }; +type AccountingType = { + auth: AuthType; + category?: String; + currency?: String; + fiat?: String; + month?: String; + year?: String; +}; + export const bosResolvers = { + Query: { + getAccountingReport: async ( + _: undefined, + params: AccountingType, + context: ContextType + ) => { + const { auth, ...settings } = params; + const lnd = getLnd(auth, context); + + const response = await to( + getAccountingReport({ + lnd, + logger, + request, + is_csv: true, + ...settings, + }) + ); + + return response; + }, + }, Mutation: { bosRebalance: async ( _: undefined, params: RebalanceType, context: ContextType ) => { - const { auth, ...extraparams } = params; + const { + auth, + avoid, + in_through, + is_avoiding_high_inbound, + max_fee, + max_fee_rate, + max_rebalance, + node, + out_channels, + out_through, + target, + } = params; const lnd = getLnd(auth, context); + const filteredParams = { + ...(avoid.length > 0 && { avoid }), + ...(in_through && { in_through }), + ...(is_avoiding_high_inbound && { is_avoiding_high_inbound }), + ...(max_fee > 0 && { max_fee }), + ...(max_fee_rate > 0 && { max_fee_rate }), + ...(max_rebalance > 0 && { max_rebalance }), + ...(node && { node }), + ...(out_channels.length > 0 && { out_channels }), + ...(out_through && { out_through }), + ...(target && { target }), + }; + + logger.info('Rebalance Params: %o', filteredParams); + const response = await to( rebalance({ lnd, logger, - ...extraparams, + ...filteredParams, }) ); diff --git a/server/schema/types.ts b/server/schema/types.ts index 204eddcb..2d22e9de 100644 --- a/server/schema/types.ts +++ b/server/schema/types.ts @@ -36,6 +36,14 @@ export const generalTypes = gql` export const queryTypes = gql` type Query { + getAccountingReport( + auth: authType! + category: String + currency: String + fiat: String + month: String + year: String + ): String! getVolumeHealth(auth: authType!): channelsHealth getTimeHealth(auth: authType!): channelsTimeHealth getFeeHealth(auth: authType!): channelsFeeHealth diff --git a/server/tests/__mocks__/balanceofsatoshis/balances.ts b/server/tests/__mocks__/balanceofsatoshis/balances.ts new file mode 100644 index 00000000..a4de4ee6 --- /dev/null +++ b/server/tests/__mocks__/balanceofsatoshis/balances.ts @@ -0,0 +1,3 @@ +export const getAccountingReport = jest + .fn() + .mockReturnValue(Promise.resolve({})); diff --git a/src/components/buttons/multiButton/MultiButton.tsx b/src/components/buttons/multiButton/MultiButton.tsx index 9c7a39a6..f4660991 100644 --- a/src/components/buttons/multiButton/MultiButton.tsx +++ b/src/components/buttons/multiButton/MultiButton.tsx @@ -21,6 +21,7 @@ const StyledSingleButton = styled.button` background-color: transparent; color: ${multiSelectColor}; flex-grow: 1; + transition: background-color 0.5s ease; ${({ selected, buttonColor }) => selected diff --git a/src/graphql/queries/__generated__/getAccountingReport.generated.tsx b/src/graphql/queries/__generated__/getAccountingReport.generated.tsx new file mode 100644 index 00000000..d4f3e0ff --- /dev/null +++ b/src/graphql/queries/__generated__/getAccountingReport.generated.tsx @@ -0,0 +1,92 @@ +import gql from 'graphql-tag'; +import * as ApolloReactCommon from '@apollo/react-common'; +import * as ApolloReactHooks from '@apollo/react-hooks'; +import * as Types from '../../types'; + +export type GetAccountingReportQueryVariables = Types.Exact<{ + auth: Types.AuthType; + category?: Types.Maybe; + currency?: Types.Maybe; + fiat?: Types.Maybe; + month?: Types.Maybe; + year?: Types.Maybe; +}>; + +export type GetAccountingReportQuery = { __typename?: 'Query' } & Pick< + Types.Query, + 'getAccountingReport' +>; + +export const GetAccountingReportDocument = gql` + query GetAccountingReport( + $auth: authType! + $category: String + $currency: String + $fiat: String + $month: String + $year: String + ) { + getAccountingReport( + auth: $auth + category: $category + currency: $currency + fiat: $fiat + month: $month + year: $year + ) + } +`; + +/** + * __useGetAccountingReportQuery__ + * + * To run a query within a React component, call `useGetAccountingReportQuery` and pass it any options that fit your needs. + * When your component renders, `useGetAccountingReportQuery` 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 } = useGetAccountingReportQuery({ + * variables: { + * auth: // value for 'auth' + * category: // value for 'category' + * currency: // value for 'currency' + * fiat: // value for 'fiat' + * month: // value for 'month' + * year: // value for 'year' + * }, + * }); + */ +export function useGetAccountingReportQuery( + baseOptions?: ApolloReactHooks.QueryHookOptions< + GetAccountingReportQuery, + GetAccountingReportQueryVariables + > +) { + return ApolloReactHooks.useQuery< + GetAccountingReportQuery, + GetAccountingReportQueryVariables + >(GetAccountingReportDocument, baseOptions); +} +export function useGetAccountingReportLazyQuery( + baseOptions?: ApolloReactHooks.LazyQueryHookOptions< + GetAccountingReportQuery, + GetAccountingReportQueryVariables + > +) { + return ApolloReactHooks.useLazyQuery< + GetAccountingReportQuery, + GetAccountingReportQueryVariables + >(GetAccountingReportDocument, baseOptions); +} +export type GetAccountingReportQueryHookResult = ReturnType< + typeof useGetAccountingReportQuery +>; +export type GetAccountingReportLazyQueryHookResult = ReturnType< + typeof useGetAccountingReportLazyQuery +>; +export type GetAccountingReportQueryResult = ApolloReactCommon.QueryResult< + GetAccountingReportQuery, + GetAccountingReportQueryVariables +>; diff --git a/src/graphql/queries/getAccountingReport.ts b/src/graphql/queries/getAccountingReport.ts new file mode 100644 index 00000000..313ebef4 --- /dev/null +++ b/src/graphql/queries/getAccountingReport.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag'; + +export const GET_ACCOUNTING_REPORT = gql` + query GetAccountingReport( + $auth: authType! + $category: String + $currency: String + $fiat: String + $month: String + $year: String + ) { + getAccountingReport( + auth: $auth + category: $category + currency: $currency + fiat: $fiat + month: $month + year: $year + ) + } +`; diff --git a/src/graphql/types.ts b/src/graphql/types.ts index 84b35e1d..474a27d3 100644 --- a/src/graphql/types.ts +++ b/src/graphql/types.ts @@ -45,6 +45,7 @@ export type PermissionsType = { export type Query = { __typename?: 'Query'; + getAccountingReport: Scalars['String']; getVolumeHealth?: Maybe; getTimeHealth?: Maybe; getFeeHealth?: Maybe; @@ -90,6 +91,15 @@ export type Query = { getLatestVersion?: Maybe; }; +export type QueryGetAccountingReportArgs = { + auth: AuthType; + category?: Maybe; + currency?: Maybe; + fiat?: Maybe; + month?: Maybe; + year?: Maybe; +}; + export type QueryGetVolumeHealthArgs = { auth: AuthType; }; diff --git a/src/utils/helpers.tsx b/src/utils/helpers.tsx index 98ec4c01..2da6ead4 100644 --- a/src/utils/helpers.tsx +++ b/src/utils/helpers.tsx @@ -93,12 +93,16 @@ export const getPercent = ( return Math.round(percent); }; -export const saveToPc = (jsonData: string, filename: string) => { +export const saveToPc = ( + jsonData: string, + filename: string, + isCsv?: boolean +) => { const fileData = jsonData; const blob = new Blob([fileData], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); - link.download = `${filename}.txt`; + link.download = isCsv ? `${filename}.csv` : `${filename}.txt`; link.href = url; link.click(); }; diff --git a/src/views/balance/AdvancedBalance.tsx b/src/views/balance/AdvancedBalance.tsx index ebebe9ce..5b440933 100644 --- a/src/views/balance/AdvancedBalance.tsx +++ b/src/views/balance/AdvancedBalance.tsx @@ -231,7 +231,7 @@ export const AdvancedBalance = () => { {hasAvoid ? : } - + {hasInChannel ? ( {state.in_through.alias} ) : null} @@ -247,7 +247,7 @@ export const AdvancedBalance = () => { {!hasOutChannels && ( - + {hasOutChannel ? ( {state.out_through.alias} ) : null} @@ -427,7 +427,14 @@ export const AdvancedBalance = () => { - {renderButton(() => isDetailedSet(false), 'Auto', !isDetailed)} + {renderButton( + () => { + dispatch({ type: 'clearFilters' }); + isDetailedSet(false); + }, + 'Auto', + !isDetailed + )} {renderButton(() => isDetailedSet(true), 'Detailed', isDetailed)} diff --git a/src/views/tools/Tools.styled.tsx b/src/views/tools/Tools.styled.tsx index 6a7d63bf..9da23c6a 100644 --- a/src/views/tools/Tools.styled.tsx +++ b/src/views/tools/Tools.styled.tsx @@ -1,4 +1,5 @@ import styled from 'styled-components'; +import { ResponsiveLine } from 'src/components/generic/Styled'; export const NoWrap = styled.div` margin-right: 16px; @@ -22,3 +23,7 @@ export const Column = styled.div` justify-content: center; align-items: center; `; + +export const ToolsResponsiveLine = styled(ResponsiveLine)` + margin-bottom: 8px; +`; diff --git a/src/views/tools/WalletVersion.tsx b/src/views/tools/WalletVersion.tsx index dd91fa96..b9debd80 100644 --- a/src/views/tools/WalletVersion.tsx +++ b/src/views/tools/WalletVersion.tsx @@ -7,6 +7,7 @@ import { Card, Sub4Title, Separation, + DarkSubTitle, } from '../../components/generic/Styled'; import { useStatusState } from '../../context/StatusContext'; import { LoadingCard } from '../../components/loading/LoadingCard'; @@ -30,7 +31,10 @@ export const WalletVersion = () => { if (minorVersion < 10) { return ( - Update to LND version 0.10.0 or higher to see your wallet build info. + + Update to LND version 0.10.0 or higher to see your wallet build + info. + ); } diff --git a/src/views/tools/accounting/Accounting.tsx b/src/views/tools/accounting/Accounting.tsx new file mode 100644 index 00000000..a3922084 --- /dev/null +++ b/src/views/tools/accounting/Accounting.tsx @@ -0,0 +1,221 @@ +import * as React from 'react'; +import { + CardWithTitle, + SubTitle, + Card, + SingleLine, + DarkSubTitle, + Separation, +} from 'src/components/generic/Styled'; +import { useGetAccountingReportLazyQuery } from 'src/graphql/queries/__generated__/getAccountingReport.generated'; +import { useAccountState } from 'src/context/AccountContext'; +import { ColorButton } from 'src/components/buttons/colorButton/ColorButton'; +import { + MultiButton, + SingleButton, +} from 'src/components/buttons/multiButton/MultiButton'; +import { X } from 'react-feather'; +import { saveToPc } from 'src/utils/helpers'; +import { ToolsResponsiveLine } from '../Tools.styled'; + +type ReportType = + | 'chain-fees' + | 'chain-receives' + | 'chain-sends' + | 'forwards' + | 'invoices' + | 'payments'; +// type FiatType = 'eur' | 'usd'; +type YearType = 2017 | 2018 | 2019 | 2020; +type MonthType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | null; + +type StateType = { + type: ReportType; + // fiat?: FiatType; + year?: YearType; + month?: MonthType; +}; + +export type ActionType = + | { + type: 'type'; + report: ReportType; + } + // | { + // type: 'fiat'; + // fiat: FiatType; + // } + | { + type: 'year'; + year: YearType; + } + | { + type: 'month'; + month: MonthType; + }; + +const initialState: StateType = { + type: 'invoices', + // fiat: 'eur', + year: 2020, + month: null, +}; + +const reducer = (state: StateType, action: ActionType): StateType => { + switch (action.type) { + case 'type': + return { ...state, type: action.report }; + // case 'fiat': + // return { ...state, fiat: action.fiat }; + case 'year': + return { ...state, year: action.year }; + case 'month': + return { ...state, month: action.month }; + default: + return state; + } +}; + +export const Accounting = () => { + const { auth } = useAccountState(); + const [showDetails, setShowDetails] = React.useState(false); + const [state, dispatch] = React.useReducer(reducer, initialState); + + const [getReport, { data, loading }] = useGetAccountingReportLazyQuery(); + + React.useEffect(() => { + if (!loading && data && data.getAccountingReport) { + saveToPc( + data.getAccountingReport, + `accounting-${state.type}-${state.year || ''}-${state.month || ''}`, + true + ); + } + }, [data, loading]); + + const reportButton = (report: ReportType, title: string) => ( + !loading && dispatch({ type: 'type', report })} + > + {title} + + ); + + // const fiatButton = (fiat: FiatType, title: string) => ( + // !loading && dispatch({ type: 'fiat', fiat })} + // > + // {title} + // + // ); + + const yearButton = (year: YearType) => ( + !loading && dispatch({ type: 'year', year })} + > + {year} + + ); + + const monthButton = (month: MonthType) => ( + !loading && dispatch({ type: 'month', month })} + > + {month ? month : 'All'} + + ); + + const renderDetails = () => ( + <> + + + Type + + {reportButton('chain-fees', 'Chain Fees')} + {reportButton('chain-receives', 'Chain Received')} + {reportButton('chain-sends', 'Chain Sent')} + {reportButton('forwards', 'Forwards')} + {/* {reportButton('payments', 'Payments')} */} + {reportButton('invoices', 'Invoices')} + + + {/* + Fiat + + {fiatButton('eur', 'Euro')} + {fiatButton('usd', 'US Dollar')} + + */} + + Year + + {yearButton(2017)} + {yearButton(2018)} + {yearButton(2019)} + {yearButton(2020)} + + + + Month + + {monthButton(null)} + {monthButton(1)} + {monthButton(2)} + {monthButton(3)} + {monthButton(4)} + {monthButton(5)} + {monthButton(6)} + {monthButton(7)} + {monthButton(8)} + {monthButton(9)} + {monthButton(10)} + {monthButton(11)} + {monthButton(12)} + + + + getReport({ + variables: { + auth, + // fiat: state.fiat, + category: state.type, + year: state.year.toString(), + ...(state.month && { month: state.month.toString() }), + }, + }) + } + fullWidth={true} + withMargin={'16px 0 0'} + > + Generate + + + ); + + return ( + + Accounting + + + Report + + showDetails ? setShowDetails(false) : setShowDetails(true) + } + > + {showDetails ? : 'Create'} + + + {showDetails && renderDetails()} + + + ); +};