mirror of
https://github.com/apotdevin/thunderhub.git
synced 2025-02-23 14:40:27 +01:00
feat: balances (#476)
This commit is contained in:
parent
8d45e297e4
commit
29fb50ab7e
19 changed files with 601 additions and 268 deletions
14
schema.gql
14
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!
|
||||
|
|
|
@ -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<ToggleAutoBackupsMutation>;
|
||||
export type ToggleAutoBackupsMutationOptions = Apollo.BaseMutationOptions<
|
||||
ToggleAutoBackupsMutation,
|
||||
ToggleAutoBackupsMutationVariables
|
||||
>;
|
62
src/client/src/graphql/mutations/__generated__/toggleConfig.generated.tsx
generated
Normal file
62
src/client/src/graphql/mutations/__generated__/toggleConfig.generated.tsx
generated
Normal file
|
@ -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<ToggleConfigMutation>;
|
||||
export type ToggleConfigMutationOptions = Apollo.BaseMutationOptions<
|
||||
ToggleConfigMutation,
|
||||
ToggleConfigMutationVariables
|
||||
>;
|
|
@ -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<ToggleHealthPingsMutation>;
|
||||
export type ToggleHealthPingsMutationOptions = Apollo.BaseMutationOptions<
|
||||
ToggleHealthPingsMutation,
|
||||
ToggleHealthPingsMutationVariables
|
||||
>;
|
|
@ -1,7 +0,0 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const TOGGLE_AUTO_BACKUPS = gql`
|
||||
mutation ToggleAutoBackups {
|
||||
toggleAutoBackups
|
||||
}
|
||||
`;
|
7
src/client/src/graphql/mutations/toggleConfig.ts
Normal file
7
src/client/src/graphql/mutations/toggleConfig.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const TOGGLE_CONFIG = gql`
|
||||
mutation ToggleConfig($field: ConfigFields!) {
|
||||
toggleConfig(field: $field)
|
||||
}
|
||||
`;
|
|
@ -1,7 +0,0 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const TOGGLE_HEALTH_PINGS = gql`
|
||||
mutation ToggleHealthPings {
|
||||
toggleHealthPings
|
||||
}
|
||||
`;
|
|
@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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<Scalars['Float']>;
|
||||
};
|
||||
|
||||
export type MutationToggleConfigArgs = {
|
||||
field: ConfigFields;
|
||||
};
|
||||
|
||||
export type MutationUpdateFeesArgs = {
|
||||
base_fee_tokens?: InputMaybe<Scalars['Float']>;
|
||||
cltv_delta?: InputMaybe<Scalars['Float']>;
|
||||
|
|
|
@ -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 (
|
||||
<SingleLine>
|
||||
<InputTitle>Auto Backups</InputTitle>
|
||||
<MultiButton loading={loading || toggleLoading} width="103px">
|
||||
<InputTitle>{title}</InputTitle>
|
||||
<MultiButton loading={loading} width="103px">
|
||||
<SingleButton
|
||||
disabled={loading || toggleLoading}
|
||||
disabled={loading}
|
||||
selected={enabled}
|
||||
onClick={toggle}
|
||||
onClick={() => toggle({ variables: { field } })}
|
||||
>
|
||||
Yes
|
||||
</SingleButton>
|
||||
<SingleButton
|
||||
disabled={loading || toggleLoading}
|
||||
disabled={loading}
|
||||
selected={!enabled}
|
||||
onClick={toggle}
|
||||
>
|
||||
No
|
||||
</SingleButton>
|
||||
</MultiButton>
|
||||
</SingleLine>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<SingleLine>
|
||||
<InputTitle>Healthcheck Pings</InputTitle>
|
||||
<MultiButton loading={loading || toggleLoading} width="103px">
|
||||
<SingleButton
|
||||
disabled={loading || toggleLoading}
|
||||
selected={enabled}
|
||||
onClick={toggle}
|
||||
>
|
||||
Yes
|
||||
</SingleButton>
|
||||
<SingleButton
|
||||
disabled={loading || toggleLoading}
|
||||
selected={!enabled}
|
||||
onClick={toggle}
|
||||
onClick={() => toggle({ variables: { field } })}
|
||||
>
|
||||
No
|
||||
</SingleButton>
|
||||
|
@ -93,12 +58,55 @@ const HealthPings = () => {
|
|||
};
|
||||
|
||||
export const AmbossSettings = () => {
|
||||
const { data, loading } = useGetConfigStateQuery({
|
||||
onError: err => toast.error(getErrorContent(err)),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <LoadingCard title="Amboss" />;
|
||||
}
|
||||
|
||||
if (!data?.getConfigState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
backup_state,
|
||||
channels_push_enabled,
|
||||
healthcheck_ping_state,
|
||||
onchain_push_enabled,
|
||||
private_channels_push_enabled,
|
||||
} = data.getConfigState;
|
||||
|
||||
return (
|
||||
<CardWithTitle>
|
||||
<SubTitle>Amboss</SubTitle>
|
||||
<Card>
|
||||
<AutoBackups />
|
||||
<HealthPings />
|
||||
<ConfigFieldToggle
|
||||
field={ConfigFields.Backups}
|
||||
enabled={backup_state}
|
||||
title={'Auto Backups'}
|
||||
/>
|
||||
<ConfigFieldToggle
|
||||
field={ConfigFields.Healthchecks}
|
||||
enabled={healthcheck_ping_state}
|
||||
title={'Healthcheck Pings'}
|
||||
/>
|
||||
<ConfigFieldToggle
|
||||
field={ConfigFields.OnchainPush}
|
||||
enabled={onchain_push_enabled}
|
||||
title={'Onchain Push'}
|
||||
/>
|
||||
<ConfigFieldToggle
|
||||
field={ConfigFields.ChannelsPush}
|
||||
enabled={channels_push_enabled}
|
||||
title={'Channels Push'}
|
||||
/>
|
||||
<ConfigFieldToggle
|
||||
field={ConfigFields.PrivateChannelsPush}
|
||||
enabled={private_channels_push_enabled}
|
||||
title={'Private Channel Push'}
|
||||
/>
|
||||
</Card>
|
||||
</CardWithTitle>
|
||||
);
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -90,3 +90,9 @@ export const pingHealthCheckMutation = gql`
|
|||
healthCheck(signature: $signature, timestamp: $timestamp)
|
||||
}
|
||||
`;
|
||||
|
||||
export const pushBalancesMutation = gql`
|
||||
mutation PushBalances($input: BalancePushInput!) {
|
||||
pushBalances(input: $input)
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,27 +132,56 @@ export class UserConfigResolver {
|
|||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async toggleAutoBackups() {
|
||||
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.');
|
||||
throw new Error('Auto backups are disabled in the server.');
|
||||
}
|
||||
|
||||
this.userConfigService.toggleAutoBackups();
|
||||
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async toggleHealthPings() {
|
||||
const disabled = this.configService.get('amboss.disableHealthCheckPings');
|
||||
case ConfigFields.HEALTHCHECKS: {
|
||||
const disabled = this.configService.get(
|
||||
'amboss.disableHealthCheckPings'
|
||||
);
|
||||
|
||||
if (disabled) {
|
||||
throw new Error('Healthcheck pings is disabled in the server.');
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue