Skip to content

Commit

Permalink
feat: add button to auto-balance LN channels
Browse files Browse the repository at this point in the history
Solves #831.

NOTE: work in progress, implementation not ready yet.
  • Loading branch information
uwla committed Mar 15, 2024
1 parent 9ae4cd9 commit f473d7a
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 1 deletion.
25 changes: 25 additions & 0 deletions src/components/designer/AutoBalanceButton.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ network }) => {
const { autoBalanceChannels } = useStoreActions(s => s.network);

const handleClick = async () => autoBalanceChannels({ id: network.id });

return <Styled.Button onClick={handleClick}>Auto Balance channels</Styled.Button>;
};

export default AutoBalanceButton;
2 changes: 2 additions & 0 deletions src/components/network/NetworkActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down Expand Up @@ -130,6 +131,7 @@ const NetworkActions: React.FC<Props> = ({
</Button>
<AutoMineButton network={network} />
<SyncButton network={network} />
<AutoBalanceButton network={network} />
<Divider type="vertical" />
</>
)}
Expand Down
104 changes: 103 additions & 1 deletion src/store/models/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -178,6 +188,9 @@ export interface NetworkModel {
setAutoMineMode: Action<NetworkModel, { id: number; mode: AutoMineMode }>;
setMiningState: Action<NetworkModel, { id: number; mining: boolean }>;
mineBlock: Thunk<NetworkModel, { id: number }, StoreInjections, RootModel>;

/* */
autoBalanceChannels: Thunk<NetworkModel, { id: number }, StoreInjections, RootModel>;
}

const networkModel: NetworkModel = {
Expand Down Expand Up @@ -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<string, LightningNode>;

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;

0 comments on commit f473d7a

Please sign in to comment.