diff --git a/src/components/designer/AutoBalanceButton.tsx b/src/components/designer/AutoBalanceButton.tsx new file mode 100644 index 0000000000..1b67c631d1 --- /dev/null +++ b/src/components/designer/AutoBalanceButton.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import styled from '@emotion/styled'; +import { Button } from 'antd'; +import { useStoreActions } from 'store'; +import { Network } from 'types'; + +const Styled = { + Button: styled(Button)` + margin-left: 8px; + `, +}; + +interface Props { + network: Network; +} + +const AutoBalanceButton: React.FC = ({ network }) => { + const { autoBalanceChannels } = useStoreActions(s => s.network); + + const handleClick = async () => autoBalanceChannels({ id: network.id }); + + return Auto Balance channels; +}; + +export default AutoBalanceButton; diff --git a/src/components/network/NetworkActions.tsx b/src/components/network/NetworkActions.tsx index ad64a5eb48..9793bcb827 100644 --- a/src/components/network/NetworkActions.tsx +++ b/src/components/network/NetworkActions.tsx @@ -20,6 +20,7 @@ import { Status } from 'shared/types'; import { useStoreState } from 'store'; import { Network } from 'types'; import { getNetworkBackendId } from 'utils/network'; +import AutoBalanceButton from 'components/designer/AutoBalanceButton'; const Styled = { Button: styled(Button)` @@ -130,6 +131,7 @@ const NetworkActions: React.FC = ({ + )} diff --git a/src/store/models/network.ts b/src/store/models/network.ts index 4762b01e07..97d5b0826e 100644 --- a/src/store/models/network.ts +++ b/src/store/models/network.ts @@ -13,7 +13,17 @@ import { TapdNode, TapNode, } from 'shared/types'; -import { AutoMineMode, CustomImage, Network, StoreInjections } from 'types'; +import { clightningService } from 'lib/lightning/clightning'; +import { eclairService } from 'lib/lightning/eclair'; +import { lndService } from 'lib/lightning/lnd'; +import { LightningNodeChannel } from 'lib/lightning/types'; +import { + AutoMineMode, + CustomImage, + LightningService, + Network, + StoreInjections, +} from 'types'; import { delay } from 'utils/async'; import { initChartFromNetwork } from 'utils/chart'; import { APP_VERSION, DOCKER_REPO } from 'utils/constants'; @@ -178,6 +188,9 @@ export interface NetworkModel { setAutoMineMode: Action; setMiningState: Action; mineBlock: Thunk; + + /* */ + autoBalanceChannels: Thunk; } const networkModel: NetworkModel = { @@ -922,6 +935,95 @@ const networkModel: NetworkModel = { actions.setAutoMineMode({ id, mode }); }), + autoBalanceChannels: thunk(async (actions, { id }, { getState, getStoreState }) => { + const { networks } = getState(); + const network = networks.find(n => n.id === id); + if (!network) throw new Error(l('networkByIdErr', { id })); + + const getNodeLightningService = (node: LightningNode): LightningService => { + switch (node.implementation) { + case 'LND': + return lndService; + case 'c-lightning': + return clightningService; + case 'eclair': + return eclairService; + default: + throw new Error('unknown implementation'); + } + }; + + const balanceChannel = async ( + channel: LightningNodeChannel, + localNode: LightningNode, + remoteNode: LightningNode, + satsTolerance = 150, + ) => { + if (channel.status !== 'Open') { + // TODO: warn about channel not opened. + return; + } + + if (remoteNode === undefined || remoteNode === null) { + // TODO: warn about remote node being null. + return; + } + + const localBalance = Number(channel.localBalance); + const remoteBalance = Number(channel.remoteBalance); + const toPay = Math.floor(Math.abs(localBalance - remoteBalance) / 2); + + // If the balance difference in satoshis is too small, we ignore it. + if (toPay < satsTolerance) { + return; + } + + // The source node pays an invoice to the target node, in order to balance the channel. + const src = localBalance > remoteBalance ? localNode : remoteNode; + const target = localBalance > remoteBalance ? remoteNode : localNode; + + console.log( + '[AUTO BALANCE]: ', + 'paying from ' + src.name + ' to ' + target.name + ' the amount ' + toPay, + ); + + const invoice = await getNodeLightningService(target).createInvoice(target, toPay); + + await getNodeLightningService(src).payInvoice(src, invoice); + }; + + interface ChannelInfo { + channel: LightningNodeChannel; + fromNode: LightningNode; + toNode: LightningNode; + } + + const lnNodes = network.nodes.lightning; + const channels = [] as LightningNodeChannel[]; + const id2Node = {} as Record; + + for (const node of lnNodes) { + const lightningService = getNodeLightningService(node); + const nodeChannels = await lightningService.getChannels(node); + channels.push(...nodeChannels); + id2Node[node.name] = node; + } + + const links = getStoreState().designer.activeChart.links; + const channelsInfo = [] as ChannelInfo[]; + + for (const channel of channels) { + const id = channel.uniqueId; + const { to, from } = links[id]; + const fromNode = id2Node[from.nodeId as string]; + const toNode = id2Node[to.nodeId as string]; + channelsInfo.push({ channel, fromNode, toNode }); + } + + for (const { channel, fromNode, toNode } of channelsInfo) { + await balanceChannel(channel, fromNode, toNode); + } + }), }; export default networkModel;