diff --git a/schema.gql b/schema.gql index c8efe838..46313043 100644 --- a/schema.gql +++ b/schema.gql @@ -231,9 +231,20 @@ type ClosedChannel { transaction_vout: Float! } +enum ConfigFields { + BACKUPS + CHANNELS_PUSH + HEALTHCHECKS + ONCHAIN_PUSH + PRIVATE_CHANNELS_PUSH +} + type ConfigState { backup_state: Boolean! + channels_push_enabled: Boolean! healthcheck_ping_state: Boolean! + onchain_push_enabled: Boolean! + private_channels_push_enabled: Boolean! } type CreateBoltzReverseSwapType { @@ -429,8 +440,7 @@ type Mutation { removeTwofaSecret(token: String!): Boolean! sendMessage(maxFee: Float, message: String!, messageType: String, publicKey: String!, tokens: Float): Float! sendToAddress(address: String!, fee: Float, sendAll: Boolean, target: Float, tokens: Float): ChainAddressSend! - toggleAutoBackups: Boolean! - toggleHealthPings: Boolean! + toggleConfig(field: ConfigFields!): Boolean! updateFees(base_fee_tokens: Float, cltv_delta: Float, fee_rate: Float, max_htlc_mtokens: String, min_htlc_mtokens: String, transaction_id: String, transaction_vout: Float): Boolean! updateMultipleFees(channels: [UpdateRoutingFeesParams!]!): Boolean! updateTwofaSecret(secret: String!, token: String!): Boolean! diff --git a/src/client/src/graphql/mutations/__generated__/toggleAutoBackups.generated.tsx b/src/client/src/graphql/mutations/__generated__/toggleAutoBackups.generated.tsx deleted file mode 100644 index d1d678ae..00000000 --- a/src/client/src/graphql/mutations/__generated__/toggleAutoBackups.generated.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as Types from '../../types'; - -import { gql } from '@apollo/client'; -import * as Apollo from '@apollo/client'; -const defaultOptions = {} as const; -export type ToggleAutoBackupsMutationVariables = Types.Exact<{ - [key: string]: never; -}>; - -export type ToggleAutoBackupsMutation = { - __typename?: 'Mutation'; - toggleAutoBackups: boolean; -}; - -export const ToggleAutoBackupsDocument = gql` - mutation ToggleAutoBackups { - toggleAutoBackups - } -`; -export type ToggleAutoBackupsMutationFn = Apollo.MutationFunction< - ToggleAutoBackupsMutation, - ToggleAutoBackupsMutationVariables ->; - -/** - * __useToggleAutoBackupsMutation__ - * - * To run a mutation, you first call `useToggleAutoBackupsMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useToggleAutoBackupsMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [toggleAutoBackupsMutation, { data, loading, error }] = useToggleAutoBackupsMutation({ - * variables: { - * }, - * }); - */ -export function useToggleAutoBackupsMutation( - baseOptions?: Apollo.MutationHookOptions< - ToggleAutoBackupsMutation, - ToggleAutoBackupsMutationVariables - > -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useMutation< - ToggleAutoBackupsMutation, - ToggleAutoBackupsMutationVariables - >(ToggleAutoBackupsDocument, options); -} -export type ToggleAutoBackupsMutationHookResult = ReturnType< - typeof useToggleAutoBackupsMutation ->; -export type ToggleAutoBackupsMutationResult = - Apollo.MutationResult; -export type ToggleAutoBackupsMutationOptions = Apollo.BaseMutationOptions< - ToggleAutoBackupsMutation, - ToggleAutoBackupsMutationVariables ->; diff --git a/src/client/src/graphql/mutations/__generated__/toggleConfig.generated.tsx b/src/client/src/graphql/mutations/__generated__/toggleConfig.generated.tsx new file mode 100644 index 00000000..86e4d704 --- /dev/null +++ b/src/client/src/graphql/mutations/__generated__/toggleConfig.generated.tsx @@ -0,0 +1,62 @@ +import * as Types from '../../types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type ToggleConfigMutationVariables = Types.Exact<{ + field: Types.ConfigFields; +}>; + +export type ToggleConfigMutation = { + __typename?: 'Mutation'; + toggleConfig: boolean; +}; + +export const ToggleConfigDocument = gql` + mutation ToggleConfig($field: ConfigFields!) { + toggleConfig(field: $field) + } +`; +export type ToggleConfigMutationFn = Apollo.MutationFunction< + ToggleConfigMutation, + ToggleConfigMutationVariables +>; + +/** + * __useToggleConfigMutation__ + * + * To run a mutation, you first call `useToggleConfigMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useToggleConfigMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [toggleConfigMutation, { data, loading, error }] = useToggleConfigMutation({ + * variables: { + * field: // value for 'field' + * }, + * }); + */ +export function useToggleConfigMutation( + baseOptions?: Apollo.MutationHookOptions< + ToggleConfigMutation, + ToggleConfigMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation< + ToggleConfigMutation, + ToggleConfigMutationVariables + >(ToggleConfigDocument, options); +} +export type ToggleConfigMutationHookResult = ReturnType< + typeof useToggleConfigMutation +>; +export type ToggleConfigMutationResult = + Apollo.MutationResult; +export type ToggleConfigMutationOptions = Apollo.BaseMutationOptions< + ToggleConfigMutation, + ToggleConfigMutationVariables +>; diff --git a/src/client/src/graphql/mutations/__generated__/toggleHealthPings.generated.tsx b/src/client/src/graphql/mutations/__generated__/toggleHealthPings.generated.tsx deleted file mode 100644 index 9e88666d..00000000 --- a/src/client/src/graphql/mutations/__generated__/toggleHealthPings.generated.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as Types from '../../types'; - -import { gql } from '@apollo/client'; -import * as Apollo from '@apollo/client'; -const defaultOptions = {} as const; -export type ToggleHealthPingsMutationVariables = Types.Exact<{ - [key: string]: never; -}>; - -export type ToggleHealthPingsMutation = { - __typename?: 'Mutation'; - toggleHealthPings: boolean; -}; - -export const ToggleHealthPingsDocument = gql` - mutation ToggleHealthPings { - toggleHealthPings - } -`; -export type ToggleHealthPingsMutationFn = Apollo.MutationFunction< - ToggleHealthPingsMutation, - ToggleHealthPingsMutationVariables ->; - -/** - * __useToggleHealthPingsMutation__ - * - * To run a mutation, you first call `useToggleHealthPingsMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useToggleHealthPingsMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [toggleHealthPingsMutation, { data, loading, error }] = useToggleHealthPingsMutation({ - * variables: { - * }, - * }); - */ -export function useToggleHealthPingsMutation( - baseOptions?: Apollo.MutationHookOptions< - ToggleHealthPingsMutation, - ToggleHealthPingsMutationVariables - > -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useMutation< - ToggleHealthPingsMutation, - ToggleHealthPingsMutationVariables - >(ToggleHealthPingsDocument, options); -} -export type ToggleHealthPingsMutationHookResult = ReturnType< - typeof useToggleHealthPingsMutation ->; -export type ToggleHealthPingsMutationResult = - Apollo.MutationResult; -export type ToggleHealthPingsMutationOptions = Apollo.BaseMutationOptions< - ToggleHealthPingsMutation, - ToggleHealthPingsMutationVariables ->; diff --git a/src/client/src/graphql/mutations/toggleAutoBackups.ts b/src/client/src/graphql/mutations/toggleAutoBackups.ts deleted file mode 100644 index 838de5da..00000000 --- a/src/client/src/graphql/mutations/toggleAutoBackups.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { gql } from '@apollo/client'; - -export const TOGGLE_AUTO_BACKUPS = gql` - mutation ToggleAutoBackups { - toggleAutoBackups - } -`; diff --git a/src/client/src/graphql/mutations/toggleConfig.ts b/src/client/src/graphql/mutations/toggleConfig.ts new file mode 100644 index 00000000..5996f2ea --- /dev/null +++ b/src/client/src/graphql/mutations/toggleConfig.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const TOGGLE_CONFIG = gql` + mutation ToggleConfig($field: ConfigFields!) { + toggleConfig(field: $field) + } +`; diff --git a/src/client/src/graphql/mutations/toggleHealthPings.ts b/src/client/src/graphql/mutations/toggleHealthPings.ts deleted file mode 100644 index 98e0c313..00000000 --- a/src/client/src/graphql/mutations/toggleHealthPings.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { gql } from '@apollo/client'; - -export const TOGGLE_HEALTH_PINGS = gql` - mutation ToggleHealthPings { - toggleHealthPings - } -`; diff --git a/src/client/src/graphql/queries/__generated__/getConfigState.generated.tsx b/src/client/src/graphql/queries/__generated__/getConfigState.generated.tsx index 9ee831b9..9ef44ad1 100644 --- a/src/client/src/graphql/queries/__generated__/getConfigState.generated.tsx +++ b/src/client/src/graphql/queries/__generated__/getConfigState.generated.tsx @@ -13,6 +13,9 @@ export type GetConfigStateQuery = { __typename?: 'ConfigState'; backup_state: boolean; healthcheck_ping_state: boolean; + onchain_push_enabled: boolean; + channels_push_enabled: boolean; + private_channels_push_enabled: boolean; }; }; @@ -21,6 +24,9 @@ export const GetConfigStateDocument = gql` getConfigState { backup_state healthcheck_ping_state + onchain_push_enabled + channels_push_enabled + private_channels_push_enabled } } `; diff --git a/src/client/src/graphql/queries/getConfigState.ts b/src/client/src/graphql/queries/getConfigState.ts index 3d99d44b..fdcc6e39 100644 --- a/src/client/src/graphql/queries/getConfigState.ts +++ b/src/client/src/graphql/queries/getConfigState.ts @@ -5,6 +5,9 @@ export const GET_CONFIG_STATE = gql` getConfigState { backup_state healthcheck_ping_state + onchain_push_enabled + channels_push_enabled + private_channels_push_enabled } } `; diff --git a/src/client/src/graphql/types.ts b/src/client/src/graphql/types.ts index 7a833107..46242d14 100644 --- a/src/client/src/graphql/types.ts +++ b/src/client/src/graphql/types.ts @@ -275,10 +275,21 @@ export type ClosedChannel = { transaction_vout: Scalars['Float']; }; +export enum ConfigFields { + Backups = 'BACKUPS', + ChannelsPush = 'CHANNELS_PUSH', + Healthchecks = 'HEALTHCHECKS', + OnchainPush = 'ONCHAIN_PUSH', + PrivateChannelsPush = 'PRIVATE_CHANNELS_PUSH', +} + export type ConfigState = { __typename?: 'ConfigState'; backup_state: Scalars['Boolean']; + channels_push_enabled: Scalars['Boolean']; healthcheck_ping_state: Scalars['Boolean']; + onchain_push_enabled: Scalars['Boolean']; + private_channels_push_enabled: Scalars['Boolean']; }; export type CreateBoltzReverseSwapType = { @@ -493,8 +504,7 @@ export type Mutation = { removeTwofaSecret: Scalars['Boolean']; sendMessage: Scalars['Float']; sendToAddress: ChainAddressSend; - toggleAutoBackups: Scalars['Boolean']; - toggleHealthPings: Scalars['Boolean']; + toggleConfig: Scalars['Boolean']; updateFees: Scalars['Boolean']; updateMultipleFees: Scalars['Boolean']; updateTwofaSecret: Scalars['Boolean']; @@ -651,6 +661,10 @@ export type MutationSendToAddressArgs = { tokens?: InputMaybe; }; +export type MutationToggleConfigArgs = { + field: ConfigFields; +}; + export type MutationUpdateFeesArgs = { base_fee_tokens?: InputMaybe; cltv_delta?: InputMaybe; diff --git a/src/client/src/views/settings/Amboss.tsx b/src/client/src/views/settings/Amboss.tsx index 5fbb2a84..c4f0ec78 100644 --- a/src/client/src/views/settings/Amboss.tsx +++ b/src/client/src/views/settings/Amboss.tsx @@ -9,11 +9,13 @@ import { SubTitle, } from '../../components/generic/Styled'; import styled from 'styled-components'; -import { useToggleAutoBackupsMutation } from '../../graphql/mutations/__generated__/toggleAutoBackups.generated'; import { getErrorContent } from '../../utils/error'; import { toast } from 'react-toastify'; import { useGetConfigStateQuery } from '../../graphql/queries/__generated__/getConfigState.generated'; -import { useToggleHealthPingsMutation } from '../../graphql/mutations/__generated__/toggleHealthPings.generated'; +import { useToggleConfigMutation } from '../../graphql/mutations/__generated__/toggleConfig.generated'; +import { ConfigFields } from '../../graphql/types'; +import { VFC } from 'react'; +import { LoadingCard } from '../../components/loading/LoadingCard'; const NoWrapText = styled.div` white-space: nowrap; @@ -22,68 +24,31 @@ const NoWrapText = styled.div` const InputTitle = styled(NoWrapText)``; -const AutoBackups = () => { - const { data, loading } = useGetConfigStateQuery({ - onError: err => toast.error(getErrorContent(err)), - }); - - const [toggle, { loading: toggleLoading }] = useToggleAutoBackupsMutation({ +const ConfigFieldToggle: VFC<{ + title: string; + enabled: boolean; + field: ConfigFields; +}> = ({ title, enabled, field }) => { + const [toggle, { loading }] = useToggleConfigMutation({ refetchQueries: ['GetConfigState'], onError: err => toast.error(getErrorContent(err)), }); - const enabled = data?.getConfigState.backup_state || false; - return ( - Auto Backups - + {title} + toggle({ variables: { field } })} > Yes - No - - - - ); -}; - -const HealthPings = () => { - const { data, loading } = useGetConfigStateQuery({ - onError: err => toast.error(getErrorContent(err)), - }); - - const [toggle, { loading: toggleLoading }] = useToggleHealthPingsMutation({ - refetchQueries: ['GetConfigState'], - onError: err => toast.error(getErrorContent(err)), - }); - - const enabled = data?.getConfigState.healthcheck_ping_state || false; - - return ( - - Healthcheck Pings - - - Yes - - toggle({ variables: { field } })} > No @@ -93,12 +58,55 @@ const HealthPings = () => { }; export const AmbossSettings = () => { + const { data, loading } = useGetConfigStateQuery({ + onError: err => toast.error(getErrorContent(err)), + }); + + if (loading) { + return ; + } + + if (!data?.getConfigState) { + return null; + } + + const { + backup_state, + channels_push_enabled, + healthcheck_ping_state, + onchain_push_enabled, + private_channels_push_enabled, + } = data.getConfigState; + return ( Amboss - - + + + + + ); diff --git a/src/server/config/configuration.ts b/src/server/config/configuration.ts index 74296e09..00213aac 100644 --- a/src/server/config/configuration.ts +++ b/src/server/config/configuration.ts @@ -47,6 +47,7 @@ type SubscriptionsConfig = { type AmbossConfig = { disableHealthCheckPings: boolean; + disableBalancePushes: boolean; }; type ConfigType = { @@ -132,6 +133,7 @@ export default (): ConfigType => { const amboss = { disableHealthCheckPings: process.env.DISABLE_HEALTHCHECK_PINGS === 'true', + disableBalancePushes: process.env.DISABLE_BALANCE_PUSHES === 'true', }; const config: ConfigType = { diff --git a/src/server/modules/api/amboss/amboss.gql.ts b/src/server/modules/api/amboss/amboss.gql.ts index 3be4b846..b358b1f3 100644 --- a/src/server/modules/api/amboss/amboss.gql.ts +++ b/src/server/modules/api/amboss/amboss.gql.ts @@ -90,3 +90,9 @@ export const pingHealthCheckMutation = gql` healthCheck(signature: $signature, timestamp: $timestamp) } `; + +export const pushBalancesMutation = gql` + mutation PushBalances($input: BalancePushInput!) { + pushBalances(input: $input) + } +`; diff --git a/src/server/modules/api/amboss/amboss.service.ts b/src/server/modules/api/amboss/amboss.service.ts index 268e087d..90043251 100644 --- a/src/server/modules/api/amboss/amboss.service.ts +++ b/src/server/modules/api/amboss/amboss.service.ts @@ -7,10 +7,15 @@ import { getNetwork } from 'src/server/utils/network'; import { Logger } from 'winston'; import { AccountsService } from '../../accounts/accounts.service'; import { FetchService } from '../../fetch/fetch.service'; -import { pingHealthCheckMutation, saveBackupMutation } from './amboss.gql'; +import { + pingHealthCheckMutation, + pushBalancesMutation, + saveBackupMutation, +} from './amboss.gql'; import { auto, map, each } from 'async'; import { NodeService } from '../../node/node.service'; import { UserConfigService } from '../userConfig/userConfig.service'; +import { getSHA256Hash } from 'src/server/utils/crypto'; const ONE_MINUTE = 60 * 1000; @@ -63,10 +68,37 @@ export class AmbossService { } } + async pushBalancesToAmboss( + timestamp: string, + signature: string, + onchainBalance: string, + channels: { balance: string; capacity: string; chan_id: string }[] + ) { + const { data, error } = await this.fetchService.graphqlFetchWithProxy( + this.ambossUrl, + pushBalancesMutation, + { input: { signature, timestamp, channels, onchainBalance } } + ); + + if (!data?.pushBalances || error) { + this.logger.error('Error pushing balances to Amboss', { + error, + data, + }); + throw new Error('Error pushing balances to Amboss'); + } + } + @Interval(ONE_MINUTE) async ping() { + const isProduction = this.configService.get('isProduction'); const disabled = this.configService.get('amboss.disableHealthCheckPings'); + if (!isProduction) { + this.logger.silly('Health check pings are only sent in production'); + return; + } + if (disabled) { this.logger.silly('Healthchecks are disabled in the server.'); return; @@ -150,17 +182,7 @@ export class AmbossService { pingAmboss: [ 'checkAvailable', async ({ checkAvailable }) => { - const isProduction = this.configService.get('isProduction'); - await each(checkAvailable, async node => { - if (!isProduction) { - this.logger.silly( - 'Health check pings are only sent in production', - { node: node.name } - ); - return; - } - if (node.network !== 'btc') { this.logger.silly( 'Health check pings are only sent for mainnet', @@ -193,4 +215,208 @@ export class AmbossService { this.logger.error(error.message); }); } + + @Interval(ONE_MINUTE) + async pushBalances() { + const isProduction = this.configService.get('isProduction'); + const disabled = this.configService.get('amboss.disableBalancePushes'); + + if (!isProduction) { + this.logger.silly('Balance pushes are only sent in production'); + return; + } + + if (disabled) { + this.logger.silly('Balance pushes are disabled in the server.'); + return; + } + + const { + onchainPushEnabled, + channelPushEnabled, + privateChannelPushEnabled, + } = this.userConfigService.getConfig(); + + if ( + !channelPushEnabled && + !privateChannelPushEnabled && + !onchainPushEnabled + ) { + this.logger.silly('Balance pushes are disabled.'); + return; + } + + await auto({ + // Get Authenticated LND objects for each node + getNodes: async () => { + const accounts = this.accountsService.getAllAccounts(); + + const validAccounts = []; + + for (const key in accounts) { + if (accounts.hasOwnProperty(key)) { + const account = accounts[key]; + if (!account.encrypted) { + validAccounts.push({ id: account.hash, lnd: account.lnd }); + } + } + } + + return validAccounts; + }, + + // Try to connect to nodes + checkNodes: [ + 'getNodes', + async ({ getNodes }) => { + return map(getNodes, async ({ lnd, id }) => { + try { + const info = await getWalletInfo({ lnd }); + + const network = getNetwork(info?.chains?.[0] || ''); + const sliced = info.public_key.slice(0, 10); + const name = `${info.alias}(${sliced})[${network}]`; + + return { + id, + name, + pubkey: info.public_key, + lnd, + network, + }; + } catch (err) { + this.logger.error('Error connecting to node', { + id, + err, + }); + } + }); + }, + ], + + // Check which nodes are available and remove duplicates + checkAvailable: [ + 'checkNodes', + async ({ checkNodes }: { checkNodes: NodeType[] }) => { + const unique = checkNodes.filter(Boolean); + + if (!unique.length) { + throw new Error('No node available for balance pushes'); + } + + const names = unique.map(a => a.name); + + this.logger.silly( + `Connected to ${names.join(', ')} for balance pushes` + ); + + return unique; + }, + ], + + pingAmboss: [ + 'checkAvailable', + async ({ checkAvailable }) => { + await each(checkAvailable, async node => { + if (node.network !== 'btc') { + this.logger.silly('Balance pushes are only sent for mainnet', { + node: node.name, + }); + return; + } + + let onchain; + let message = ''; + + if (onchainPushEnabled) { + const { chain_balance } = await this.nodeService.getChainBalance( + node.id + ); + const { pending_chain_balance } = + await this.nodeService.getPendingChainBalance(node.id); + + onchain = (chain_balance + pending_chain_balance).toString(); + message += onchain; + } + + const allChannels = []; + + if (channelPushEnabled) { + const channels = await this.nodeService.getChannels(node.id, { + is_public: true, + }); + + if (!channels.channels.length) return; + + const mapped = channels.channels.map(c => ({ + chan_id: c.id, + balance: c.local_balance + '', + capacity: c.capacity + '', + })); + + allChannels.push(...mapped); + } + + if (privateChannelPushEnabled) { + const privateChannels = await this.nodeService.getChannels( + node.id, + { is_private: true } + ); + + if (!privateChannels.channels.length) return; + + const mapped = privateChannels.channels.map(c => ({ + chan_id: c.id, + balance: c.local_balance + '', + capacity: c.capacity + '', + })); + + allChannels.push(...mapped); + } + + if (allChannels.length) { + const infoString = allChannels.reduce((p, c) => { + return p + `${c.chan_id}${c.balance}${c.capacity || ''}`; + }, ''); + + message += getSHA256Hash(infoString); + } + + const timestamp = new Date().toISOString(); + const finalMessage = timestamp + message; + + const { signature } = await this.nodeService.signMessage( + node.id, + finalMessage + ); + + this.logger.info('Push Info', { + onchainBalance: !!onchain, + amountOfChannels: allChannels.length, + finalMessage, + signature, + }); + + await this.pushBalancesToAmboss( + timestamp, + signature, + onchain, + allChannels + ); + }); + }, + ], + }) + .then(result => { + const nodes = result.checkAvailable.length; + this.logger.silly( + `Finished balance pushes for ${nodes} node${ + nodes.length > 1 ? 's' : '' + }.` + ); + }) + .catch(error => { + this.logger.error(error.message); + }); + } } diff --git a/src/server/modules/api/userConfig/userConfig.resolver.ts b/src/server/modules/api/userConfig/userConfig.resolver.ts index 6b029aa2..2646b5a9 100644 --- a/src/server/modules/api/userConfig/userConfig.resolver.ts +++ b/src/server/modules/api/userConfig/userConfig.resolver.ts @@ -1,10 +1,19 @@ import { Inject } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { Mutation, Query, ResolveField, Resolver } from '@nestjs/graphql'; +import { + Args, + Mutation, + Query, + registerEnumType, + ResolveField, + Resolver, +} from '@nestjs/graphql'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { Logger } from 'winston'; import { UserConfigService } from './userConfig.service'; -import { ConfigState } from './userConfig.types'; +import { ConfigFields, ConfigState } from './userConfig.types'; + +registerEnumType(ConfigFields, { name: 'ConfigFields' }); @Resolver(ConfigState) export class UserConfigStateResolver { @@ -51,6 +60,63 @@ export class UserConfigStateResolver { return healthCheckPingEnabled; } + + @ResolveField() + onchain_push_enabled() { + const { onchainPushEnabled } = this.userConfigService.getConfig(); + + const disabled = this.configService.get('amboss.disableBalancePushes'); + + if (disabled) { + if (onchainPushEnabled) { + this.logger.warn( + 'Balance pushes are enabled in the config file but disabled in the env file.' + ); + } + + return false; + } + + return onchainPushEnabled; + } + + @ResolveField() + channels_push_enabled() { + const { channelPushEnabled } = this.userConfigService.getConfig(); + + const disabled = this.configService.get('amboss.disableBalancePushes'); + + if (disabled) { + if (channelPushEnabled) { + this.logger.warn( + 'Balance pushes are enabled in the config file but disabled in the env file.' + ); + } + + return false; + } + + return channelPushEnabled; + } + + @ResolveField() + private_channels_push_enabled() { + const { privateChannelPushEnabled } = this.userConfigService.getConfig(); + + const disabled = this.configService.get('amboss.disableBalancePushes'); + + if (disabled) { + if (privateChannelPushEnabled) { + this.logger.warn( + 'Balance pushes are enabled in the config file but disabled in the env file.' + ); + } + + return false; + } + + return privateChannelPushEnabled; + } } @Resolver() @@ -66,28 +132,57 @@ export class UserConfigResolver { } @Mutation(() => Boolean) - async toggleAutoBackups() { - const disabled = this.configService.get('subscriptions.disableBackups'); + async toggleConfig( + @Args('field', { type: () => ConfigFields }) field: ConfigFields + ) { + switch (field) { + case ConfigFields.BACKUPS: { + const disabled = this.configService.get('subscriptions.disableBackups'); - if (disabled) { - throw new Error('Auto backups is disabled in the server.'); + if (disabled) { + throw new Error('Auto backups are disabled in the server.'); + } + + this.userConfigService.toggleAutoBackups(); + break; + } + case ConfigFields.HEALTHCHECKS: { + const disabled = this.configService.get( + 'amboss.disableHealthCheckPings' + ); + + if (disabled) { + throw new Error('Healthcheck pings are disabled in the server.'); + } + + this.userConfigService.toggleHealthCheckPing(); + break; + } + case ConfigFields.ONCHAIN_PUSH: + case ConfigFields.CHANNELS_PUSH: + case ConfigFields.PRIVATE_CHANNELS_PUSH: { + const disabled = this.configService.get('amboss.disableBalancePushes'); + + if (disabled) { + throw new Error('Balance pushes are disabled in the server.'); + } + + switch (field) { + case ConfigFields.ONCHAIN_PUSH: + this.userConfigService.toggleOnChainPush(); + break; + case ConfigFields.CHANNELS_PUSH: + this.userConfigService.toggleChannelPush(); + break; + case ConfigFields.PRIVATE_CHANNELS_PUSH: + this.userConfigService.togglePrivateChannelPush(); + break; + } + + break; + } } - this.userConfigService.toggleAutoBackups(); - - return true; - } - - @Mutation(() => Boolean) - async toggleHealthPings() { - const disabled = this.configService.get('amboss.disableHealthCheckPings'); - - if (disabled) { - throw new Error('Healthcheck pings is disabled in the server.'); - } - - this.userConfigService.toggleHealthCheckPing(); - return true; } } diff --git a/src/server/modules/api/userConfig/userConfig.service.ts b/src/server/modules/api/userConfig/userConfig.service.ts index f0c90ab5..ed086518 100644 --- a/src/server/modules/api/userConfig/userConfig.service.ts +++ b/src/server/modules/api/userConfig/userConfig.service.ts @@ -27,59 +27,66 @@ export class UserConfigService implements OnModuleInit { this.logger.info( `No account config file found at path ${accountConfigPath}` ); - return { backupsEnabled: false, healthCheckPingEnabled: false }; + return { + backupsEnabled: false, + healthCheckPingEnabled: false, + onchainPushEnabled: false, + channelPushEnabled: false, + privateChannelPushEnabled: false, + }; } return { backupsEnabled: !!yaml.backupsEnabled, healthCheckPingEnabled: !!yaml.healthCheckPingEnabled, + onchainPushEnabled: !!yaml.onchainPushEnabled, + channelPushEnabled: !!yaml.channelPushEnabled, + privateChannelPushEnabled: !!yaml.privateChannelPushEnabled, }; } + toggleValue(field: string): void { + const accountConfigPath = this.configService.get('accountConfigPath'); + + if (!accountConfigPath) { + this.logger.verbose('No config file path provided'); + throw new Error('Error enabling auto backups'); + } + + const accountConfig = this.filesService.parseYaml(accountConfigPath); + + if (!accountConfig) { + this.logger.info(`No config file found at path ${accountConfigPath}`); + throw new Error('Error enabling auto backups'); + } + + const currentStatus = accountConfig[field]; + const configCopy = { + ...accountConfig, + [field]: !currentStatus, + }; + + this.config = configCopy; + this.filesService.saveHashedYaml(configCopy, accountConfigPath); + } + + togglePrivateChannelPush(): void { + this.toggleValue('privateChannelPushEnabled'); + } + + toggleChannelPush(): void { + this.toggleValue('channelPushEnabled'); + } + + toggleOnChainPush(): void { + this.toggleValue('onchainPushEnabled'); + } + toggleHealthCheckPing(): void { - const accountConfigPath = this.configService.get('accountConfigPath'); - - if (!accountConfigPath) { - this.logger.verbose('No config file path provided'); - throw new Error('Error enabling auto backups'); - } - - const accountConfig = this.filesService.parseYaml(accountConfigPath); - - if (!accountConfig) { - this.logger.info(`No config file found at path ${accountConfigPath}`); - throw new Error('Error enabling auto backups'); - } - - const currentStatus = accountConfig.healthCheckPingEnabled; - const configCopy = { - ...accountConfig, - healthCheckPingEnabled: !currentStatus, - }; - - this.config = configCopy; - this.filesService.saveHashedYaml(configCopy, accountConfigPath); + this.toggleValue('healthCheckPingEnabled'); } toggleAutoBackups(): void { - const accountConfigPath = this.configService.get('accountConfigPath'); - - if (!accountConfigPath) { - this.logger.verbose('No config file path provided'); - throw new Error('Error enabling auto backups'); - } - - const accountConfig = this.filesService.parseYaml(accountConfigPath); - - if (!accountConfig) { - this.logger.info(`No config file found at path ${accountConfigPath}`); - throw new Error('Error enabling auto backups'); - } - - const currentStatus = accountConfig.backupsEnabled; - const configCopy = { ...accountConfig, backupsEnabled: !currentStatus }; - - this.config = configCopy; - this.filesService.saveHashedYaml(configCopy, accountConfigPath); + this.toggleValue('backupsEnabled'); } } diff --git a/src/server/modules/api/userConfig/userConfig.types.ts b/src/server/modules/api/userConfig/userConfig.types.ts index edcc8019..b7882849 100644 --- a/src/server/modules/api/userConfig/userConfig.types.ts +++ b/src/server/modules/api/userConfig/userConfig.types.ts @@ -1,9 +1,23 @@ import { Field, ObjectType } from '@nestjs/graphql'; +export enum ConfigFields { + BACKUPS = 'BACKUPS', + HEALTHCHECKS = 'HEALTHCHECKS', + ONCHAIN_PUSH = 'ONCHAIN_PUSH', + CHANNELS_PUSH = 'CHANNELS_PUSH', + PRIVATE_CHANNELS_PUSH = 'PRIVATE_CHANNELS_PUSH', +} + @ObjectType() export class ConfigState { @Field() backup_state: boolean; @Field() healthcheck_ping_state: boolean; + @Field() + onchain_push_enabled: boolean; + @Field() + channels_push_enabled: boolean; + @Field() + private_channels_push_enabled: boolean; } diff --git a/src/server/modules/fetch/fetch.service.ts b/src/server/modules/fetch/fetch.service.ts index 3acb4d39..aea88809 100644 --- a/src/server/modules/fetch/fetch.service.ts +++ b/src/server/modules/fetch/fetch.service.ts @@ -6,6 +6,9 @@ import { Agent } from 'https'; import { SocksProxyAgent } from 'socks-proxy-agent'; import { DocumentNode, GraphQLError, print } from 'graphql'; +type Variables = { + [key: string]: string | number | string[] | boolean | any[] | Variables; +}; @Injectable() export class FetchService { agent: Agent | null = null; @@ -31,7 +34,7 @@ export class FetchService { async graphqlFetchWithProxy( url: string, query: DocumentNode, - variables?: { [key: string]: string | number | string[] | boolean }, + variables?: Variables, headers?: { [key: string]: string | number | string[] | boolean } ): Promise<{ data: any; diff --git a/src/server/modules/files/files.types.ts b/src/server/modules/files/files.types.ts index e3d4f616..60d88b59 100644 --- a/src/server/modules/files/files.types.ts +++ b/src/server/modules/files/files.types.ts @@ -47,10 +47,16 @@ export type AccountConfigType = { defaultNetwork: string | null; backupsEnabled: boolean | null; healthCheckPingEnabled: boolean | null; + onchainPushEnabled: boolean | null; + channelPushEnabled: boolean | null; + privateChannelPushEnabled: boolean | null; accounts: AccountType[]; }; export type ConfigType = { backupsEnabled: boolean; healthCheckPingEnabled: boolean; + onchainPushEnabled: boolean; + channelPushEnabled: boolean; + privateChannelPushEnabled: boolean; };