Skip to content

Commit

Permalink
feat: add balance channels modal
Browse files Browse the repository at this point in the history
The modal has slicers to manually balance LN channels.
It also has a button to evenly balance the channels.
  • Loading branch information
uwla committed Jun 23, 2024
1 parent 1e31b94 commit 4884497
Show file tree
Hide file tree
Showing 11 changed files with 328 additions and 7 deletions.
37 changes: 37 additions & 0 deletions src/components/common/BalanceChannelsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import { SwapOutlined } from '@ant-design/icons';
import styled from '@emotion/styled';
import { Button, Tooltip } from 'antd';
import { usePrefixedTranslation } from 'hooks';
import { useStoreActions } from 'store';
import { Network } from 'types';

const Styled = {
Button: styled(Button)`
margin-left: 8px;
`,
};

interface Props {
network: Network;
}

const BalanceChannelsButton: React.FC<Props> = ({ network }) => {
const { l } = usePrefixedTranslation('cmps.common.BalanceChannelsButton');
const { showBalanceChannels } = useStoreActions(s => s.modals);
const { resetChannelsInfo } = useStoreActions(s => s.lightning);
const showModal = async () => {
await showBalanceChannels();
await resetChannelsInfo(network);

Check warning on line 25 in src/components/common/BalanceChannelsButton.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsButton.tsx#L24-L25

Added lines #L24 - L25 were not covered by tests
};

return (
<Tooltip title={l('btn')}>
<Styled.Button onClick={showModal}>
<SwapOutlined />
</Styled.Button>
</Tooltip>
);
};

export default BalanceChannelsButton;
86 changes: 86 additions & 0 deletions src/components/common/BalanceChannelsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';
import { PercentageOutlined, ReloadOutlined } from '@ant-design/icons';
import { Button, Col, Modal, Row, Slider } from 'antd';
import { usePrefixedTranslation } from 'hooks';
import { useStoreActions, useStoreState } from 'store';
import { ChannelInfo, Network } from 'types';
import { format } from 'utils/units';
import styled from '@emotion/styled';

interface Props {
network: Network;
}

const Styled = {
Button: styled(Button)`
width: 100%;
`,
};

const BalanceChannelsModal: React.FC<Props> = ({ network }) => {
const { l } = usePrefixedTranslation('cmps.common.BalanceChannelsModal');
const { channelsInfo } = useStoreState(s => s.lightning);
const { visible } = useStoreState(s => s.modals.balanceChannels);
const { hideBalanceChannels } = useStoreActions(s => s.modals);

Check warning on line 24 in src/components/common/BalanceChannelsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L21-L24

Added lines #L21 - L24 were not covered by tests
const {
resetChannelsInfo,
manualBalanceChannelsInfo,
autoBalanceChannelsInfo,
updateBalanceOfChannels,
} = useStoreActions(s => s.lightning);

Check warning on line 30 in src/components/common/BalanceChannelsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L30

Added line #L30 was not covered by tests

return (

Check warning on line 32 in src/components/common/BalanceChannelsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L32

Added line #L32 was not covered by tests
<Modal
title="Balance Channels"
open={visible}
okText={l('update')}
cancelText={l('close')}
onOk={() => updateBalanceOfChannels(network)}
onCancel={() => hideBalanceChannels()}

Check warning on line 39 in src/components/common/BalanceChannelsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L38-L39

Added lines #L38 - L39 were not covered by tests
>
{/* sliders */}
{(channelsInfo || []).map((channel: ChannelInfo, index: number) => {
const { to, from, id, remoteBalance, localBalance, nextLocalBalance } = channel;
const total = Number(remoteBalance) + Number(localBalance);
return (

Check warning on line 45 in src/components/common/BalanceChannelsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L43-L45

Added lines #L43 - L45 were not covered by tests
<div key={id}>
<Row>
<Col span={12}>
{from}
<br />
{format(nextLocalBalance)}
</Col>
<Col span={12} style={{ textAlign: 'right' }}>
{to}
<br />
{format(total - nextLocalBalance)}
</Col>
</Row>
<Slider
value={nextLocalBalance}
onChange={value => manualBalanceChannelsInfo({ value, index })}

Check warning on line 61 in src/components/common/BalanceChannelsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L61

Added line #L61 was not covered by tests
min={0}
max={total}
/>
</div>
);
})}
{/* end sliders */}
<br />
<Row gutter={10}>
<Col span={12}>
<Styled.Button onClick={() => resetChannelsInfo(network)}>

Check warning on line 72 in src/components/common/BalanceChannelsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L72

Added line #L72 was not covered by tests
<ReloadOutlined /> <span>{l('reset')}</span>
</Styled.Button>
</Col>
<Col span={12}>
<Styled.Button onClick={() => autoBalanceChannelsInfo()}>

Check warning on line 77 in src/components/common/BalanceChannelsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L77

Added line #L77 was not covered by tests
<PercentageOutlined /> <span>{l('autoBalance')}</span>
</Styled.Button>
</Col>
</Row>
</Modal>
);
};

export default BalanceChannelsModal;
4 changes: 2 additions & 2 deletions src/components/designer/AutoMineButton.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { FieldTimeOutlined } from '@ant-design/icons';
import styled from '@emotion/styled';
import { Button, Dropdown, Tooltip, MenuProps } from 'antd';
import { Button, Dropdown, MenuProps, Tooltip } from 'antd';
import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { usePrefixedTranslation } from 'hooks';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useStoreActions, useStoreState } from 'store';
import { AutoMineMode, Network } from 'types';

Expand Down
3 changes: 3 additions & 0 deletions src/components/designer/NetworkDesigner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useStoreActions, useStoreState } from 'store';
import { Network } from 'types';
import { Loader } from 'components/common';
import AdvancedOptionsModal from 'components/common/AdvancedOptionsModal';
import BalanceChannelsModal from 'components/common/BalanceChannelsModal';
import SendOnChainModal from './bitcoind/actions/SendOnChainModal';
import { CanvasOuterDark, Link, NodeInner, Port, Ports } from './custom';
import {
Expand Down Expand Up @@ -60,6 +61,7 @@ const NetworkDesigner: React.FC<Props> = ({ network, updateStateDelay = 3000 })
changeBackend,
sendOnChain,
advancedOptions,
balanceChannels,
changeTapBackend,
} = useStoreState(s => s.modals);

Expand Down Expand Up @@ -104,6 +106,7 @@ const NetworkDesigner: React.FC<Props> = ({ network, updateStateDelay = 3000 })
{changeBackend.visible && <ChangeBackendModal network={network} />}
{sendOnChain.visible && <SendOnChainModal network={network} />}
{advancedOptions.visible && <AdvancedOptionsModal network={network} />}
{balanceChannels.visible && <BalanceChannelsModal network={network} />}
{mintAsset.visible && <MintAssetModal network={network} />}
{newAddress.visible && <NewAddressModal network={network} />}
{changeTapBackend.visible && <ChangeTapBackendModal network={network} />}
Expand Down
10 changes: 6 additions & 4 deletions src/components/network/NetworkActions.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React, { ReactNode, useCallback } from 'react';
import {
CloseOutlined,
ExportOutlined,
Expand All @@ -11,15 +12,15 @@ import {
import styled from '@emotion/styled';
import { Button, Divider, Dropdown, MenuProps, Tag } from 'antd';
import { ButtonType } from 'antd/lib/button';
import AutoMineButton from 'components/designer/AutoMineButton';
import { useMiningAsync } from 'hooks/useMiningAsync';
import SyncButton from 'components/designer/SyncButton';
import { usePrefixedTranslation } from 'hooks';
import React, { ReactNode, useCallback } from 'react';
import { useMiningAsync } from 'hooks/useMiningAsync';
import { Status } from 'shared/types';
import { useStoreState } from 'store';
import { Network } from 'types';
import { getNetworkBackendId } from 'utils/network';
import BalanceChannelsButton from 'components/common/BalanceChannelsButton';
import AutoMineButton from 'components/designer/AutoMineButton';
import SyncButton from 'components/designer/SyncButton';

const Styled = {
Button: styled(Button)`
Expand Down Expand Up @@ -129,6 +130,7 @@ const NetworkActions: React.FC<Props> = ({
{l('mineBtn')}
</Button>
<AutoMineButton network={network} />
<BalanceChannelsButton network={network} />
<SyncButton network={network} />
<Divider type="vertical" />
</>
Expand Down
6 changes: 6 additions & 0 deletions src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
"cmps.common.AdvancedOptionsModal.cancelBtn": "Cancel",
"cmps.common.AdvancedOptionsModal.success": "Updated advanced options for {{name}}",
"cmps.common.AdvancedOptionsModal.error": "Failed to update options",
"cmps.common.BalanceChannelsButton.btn": "Balance Channels",
"cmps.common.BalanceChannelsModal.title": "Balance Channels",
"cmps.common.BalanceChannelsModal.autoBalance": "Auto Balance",
"cmps.common.BalanceChannelsModal.update": "Update Channels",
"cmps.common.BalanceChannelsModal.reset": "Reset",
"cmps.common.BalanceChannelsModal.close": "Close",
"cmps.common.CopyIcon.message": "Copied {{label}} to clipboard",
"cmps.common.NavMenu.createNetwork": "Create Network",
"cmps.common.NavMenu.manageNodes": "Manage Images",
Expand Down
137 changes: 136 additions & 1 deletion src/store/models/lightning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { Action, action, Thunk, thunk, ThunkOn, thunkOn } from 'easy-peasy';
import { throttle } from 'lodash';
import { LightningNode, Status } from 'shared/types';
import * as PLN from 'lib/lightning/types';
import { Network, StoreInjections } from 'types';
import { ChannelInfo, Network, PreInvoice, StoreInjections } from 'types';
import { delay } from 'utils/async';
import { BLOCKS_TIL_CONFIRMED } from 'utils/constants';
import { getInvoicePayload } from 'utils/network';
import { fromSatsNumeric } from 'utils/units';
import { RootModel } from './';
import { LightningNodeChannel } from 'lib/lightning/types';

export interface LightningNodeMapping {
[key: string]: LightningNodeModel;
Expand Down Expand Up @@ -45,6 +47,7 @@ export interface PayInvoicePayload {

export interface LightningModel {
nodes: LightningNodeMapping;
channelsInfo: ChannelInfo[];
removeNode: Action<LightningModel, string>;
clearNodes: Action<LightningModel, void>;
setInfo: Action<LightningModel, { node: LightningNode; info: PLN.LightningNodeInfo }>;
Expand Down Expand Up @@ -88,11 +91,23 @@ export interface LightningModel {
addListeners: Thunk<LightningModel, Network, StoreInjections, RootModel>;
removeListeners: Thunk<LightningModel, Network, StoreInjections, RootModel>;
addChannelListeners: Thunk<LightningModel, Network, StoreInjections, RootModel>;
setChannelsInfo: Action<LightningModel, ChannelInfo[]>;
resetChannelsInfo: Thunk<LightningModel, Network, StoreInjections, RootModel>;
manualBalanceChannelsInfo: Action<LightningModel, { value: number; index: number }>;
autoBalanceChannelsInfo: Action<LightningModel>;
updateBalanceOfChannels: Thunk<LightningModel, Network, StoreInjections, RootModel>;
balanceChannels: Thunk<
LightningModel,
{ id: number; toPay: PreInvoice[] },
StoreInjections,
RootModel
>;
}

const lightningModel: LightningModel = {
// state properties
nodes: {},
channelsInfo: [],
// reducer actions (mutations allowed thx to immer)
removeNode: action((state, name) => {
if (state.nodes[name]) {
Expand Down Expand Up @@ -129,6 +144,7 @@ const lightningModel: LightningModel = {
const api = injections.lightningFactory.getService(node);
const channels = await api.getChannels(node);
actions.setChannels({ node, channels });
return channels;
}),
getAllInfo: thunk(async (actions, node) => {
await actions.getInfo(node);
Expand Down Expand Up @@ -334,6 +350,125 @@ const lightningModel: LightningModel = {
}
},
),
setChannelsInfo: action((state, payload) => {
state.channelsInfo = payload;
}),
resetChannelsInfo: thunk(async (actions, network, { getStoreState }) => {
const channels = [] as LightningNodeChannel[];
const { getChannels } = actions;
const { links } = getStoreState().designer.activeChart;

Check warning on line 359 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L358-L359

Added lines #L358 - L359 were not covered by tests

const id2Node = {} as Record<string, LightningNode>;
const channelsInfo = [] as ChannelInfo[];

await Promise.all(

Check warning on line 364 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L361-L364

Added lines #L361 - L364 were not covered by tests
network.nodes.lightning.map(async node => {
const nodeChannels = await getChannels(node);
channels.push(...nodeChannels);

Check warning on line 367 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L366-L367

Added lines #L366 - L367 were not covered by tests
id2Node[node.name] = node;
}),
);

for (const channel of channels) {
const { uniqueId: id, localBalance, remoteBalance } = channel;

Check warning on line 373 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L369-L373

Added lines #L369 - L373 were not covered by tests
if (!links[id]) continue;
const from = links[id].from.nodeId;
const to = links[id].to.nodeId;
if (!to) continue;
const nextLocalBalance = Number(localBalance);

Check warning on line 378 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L377-L378

Added lines #L377 - L378 were not covered by tests
channelsInfo.push({
id,
to,

Check warning on line 381 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L380-L381

Added lines #L380 - L381 were not covered by tests
from,
localBalance,
remoteBalance,

Check warning on line 384 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L383-L384

Added lines #L383 - L384 were not covered by tests
nextLocalBalance,
});
}

actions.setChannelsInfo(channelsInfo);
}),
manualBalanceChannelsInfo: action((state, { value, index }) => {
const { channelsInfo: info } = state;
if (info && info[index]) {
info[index].nextLocalBalance = value;

Check warning on line 394 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L394

Added line #L394 was not covered by tests
state.channelsInfo = info;
}
}),

Check warning on line 397 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L396-L397

Added lines #L396 - L397 were not covered by tests
autoBalanceChannelsInfo: action(state => {
const { channelsInfo } = state;
if (!channelsInfo) {

Check warning on line 400 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L399-L400

Added lines #L399 - L400 were not covered by tests
return;
}
for (let index = 0; index < channelsInfo.length; index += 1) {
const { localBalance, remoteBalance } = channelsInfo[index];

Check warning on line 404 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L403-L404

Added lines #L403 - L404 were not covered by tests
const halfAmount = Math.floor((Number(localBalance) + Number(remoteBalance)) / 2);
channelsInfo[index].nextLocalBalance = halfAmount;

Check warning on line 406 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L406

Added line #L406 was not covered by tests
}
state.channelsInfo = channelsInfo;
}),
updateBalanceOfChannels: thunk(
async (actions, network, { getStoreActions, getState }) => {

Check warning on line 411 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L408-L411

Added lines #L408 - L411 were not covered by tests
const { notify } = getStoreActions().app;
const { hideBalanceChannels } = getStoreActions().modals;

Check warning on line 413 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L413

Added line #L413 was not covered by tests
const { channelsInfo } = getState();

if (!channelsInfo) return;

const toPay: PreInvoice[] = channelsInfo
.filter(c => Number(c.localBalance) !== c.nextLocalBalance)

Check warning on line 419 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L416-L419

Added lines #L416 - L419 were not covered by tests
.map(c => ({ channelId: c.id, nextLocalBalance: c.nextLocalBalance }));

await actions.balanceChannels({ id: network.id, toPay });
await hideBalanceChannels();
notify({ message: 'Channels balanced!' });
},

Check warning on line 425 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L423-L425

Added lines #L423 - L425 were not covered by tests
),
balanceChannels: thunk(async (actions, { id, toPay }, { getStoreState }) => {
const { networks } = getStoreState().network;
const network = networks.find(n => n.id === id);

Check warning on line 429 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L427-L429

Added lines #L427 - L429 were not covered by tests
if (!network) throw new Error('networkByIdErr');
const { createInvoice, payInvoice, getChannels } = actions;
const lnNodes = network.nodes.lightning;
const channels = [] as LightningNodeChannel[];
const id2Node = {} as Record<string, LightningNode>;

Check warning on line 434 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L432-L434

Added lines #L432 - L434 were not covered by tests
const id2channel = {} as Record<string, LightningNodeChannel>;

await Promise.all(
lnNodes.map(async node => {
id2Node[node.name] = node;
const nodeChannels = await getChannels(node);

Check warning on line 440 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L436-L440

Added lines #L436 - L440 were not covered by tests
channels.push(...nodeChannels);
}),
);

channels.forEach(channel => (id2channel[channel.uniqueId] = channel));
const minimumSatsDifference = 50;

Check warning on line 446 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L442-L446

Added lines #L442 - L446 were not covered by tests
const links = getStoreState().designer.activeChart.links;

await Promise.all(
toPay.map(async ({ channelId, nextLocalBalance }) => {
const channel = id2channel[channelId];
const { to, from } = links[channelId];

Check warning on line 452 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L450-L452

Added lines #L450 - L452 were not covered by tests
if (!to.nodeId) return;
const fromNode = id2Node[from.nodeId];
const toNode = id2Node[to.nodeId];
const payload = getInvoicePayload(channel, fromNode, toNode, nextLocalBalance);

Check warning on line 457 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L454-L457

Added lines #L454 - L457 were not covered by tests
if (payload.amount < minimumSatsDifference) return;

const invoice = await createInvoice({
node: payload.target,

Check warning on line 461 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L459-L461

Added lines #L459 - L461 were not covered by tests
amount: payload.amount,
});

await payInvoice({

Check warning on line 465 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L465

Added line #L465 was not covered by tests
invoice,
node: payload.source,
});
}),
);

Check warning on line 470 in src/store/models/lightning.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/lightning.ts#L470

Added line #L470 was not covered by tests
}),
};

export default lightningModel;
Loading

0 comments on commit 4884497

Please sign in to comment.