Skip to content

Commit

Permalink
feat: enables manual balancing of channels
Browse files Browse the repository at this point in the history
User can manually balance channels using a slicer. The channels are
shown in a modal after clicking a button.

NOTE: this is a prototype, not working yet.
  • Loading branch information
uwla committed Jun 9, 2024
1 parent 1e31b94 commit 49be98d
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 23 deletions.
6 changes: 3 additions & 3 deletions src/components/designer/AutoMineButton.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
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';

const Styled = {
Button: styled(Button)`
margin-left: 8px;
width: 100%;
`,
RemainingBar: styled.div`
transition: width 400ms ease-in-out;
Expand Down
158 changes: 158 additions & 0 deletions src/components/designer/BalanceChannelsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import React, { useState } from 'react';
import { PercentageOutlined, ReloadOutlined, SwapOutlined } from '@ant-design/icons';
import styled from '@emotion/styled';
import { Button, Col, Modal, Row, Slider } from 'antd';
import { ChannelInfo, LightningNode, PreInvoice } from 'shared/types';
import { LightningNodeChannel } from 'lib/lightning/types';
import { useStoreActions, useStoreState } from 'store';
import { Network } from 'types';

const Styled = {
Button: styled(Button)`
width: 100%;
font-variant: small-caps;
`,
};

interface Props {
network: Network;
}

const AutoBalanceButton: React.FC<Props> = ({ network }) => {
const { getChannels } = useStoreActions(s => s.lightning);
const { balanceChannels } = useStoreActions(s => s.network);
const { links } = useStoreState(s => s.designer.activeChart);
const { notify } = useStoreActions(s => s.app);
const [visible, setVisible] = useState(false);
const [info, setInfo] = useState([] as ChannelInfo[]);

const hideModal = () => setVisible(false);
const showModal = () => {
updateInfo();
setVisible(true);
};

const setNextBalance = (value: number, index: number) => {
info[index].nextLocalBalance = value;
setInfo([...info]);
};

const autoBalance = () => {
for (let index = 0; index < info.length; index += 1) {
const { localBalance, remoteBalance } = info[index];
const halfAmount = Math.floor((Number(localBalance) + Number(remoteBalance)) / 2);
info[index].nextLocalBalance = halfAmount;
}
setInfo([...info]);
};

// Store all channels in an array and build a map nodeName->node.
async function updateInfo() {
const channels = [] as LightningNodeChannel[];
const id2Node = {} as Record<string, LightningNode>;
const promisesToAwait = [] as Promise<unknown>[];

for (const node of network.nodes.lightning) {
promisesToAwait.push(
getChannels(node).then((nodeChannels: LightningNodeChannel[]) => {
channels.push(...nodeChannels);
id2Node[node.name] = node;
}),
);
}
await Promise.all(promisesToAwait);

const info = [] as ChannelInfo[];
for (const channel of channels) {
const { uniqueId: id, localBalance, remoteBalance } = channel;
if (!links[id]) continue;
const from = links[id].from.nodeId;
const to = links[id].to.nodeId as string;
const nextLocalBalance = Number(localBalance);
info.push({ id, to, from, localBalance, remoteBalance, nextLocalBalance });
}
setInfo(info);
}

async function updateChannels() {
const toPay = [] as PreInvoice[];
for (const { id, localBalance, nextLocalBalance } of info) {
if (Number(localBalance) !== nextLocalBalance) {
toPay.push({ channelId: id, nextLocalBalance });
}
}
balanceChannels({ id: network.id, toPay })
.then(() => notify({ message: 'Channels balanced!' }))
.then(hideModal);
}

const CustomModalFooter = (
<Row gutter={10} justify="end">
<Col>
<Styled.Button onClick={hideModal}>Close</Styled.Button>
</Col>
<Col>
<Styled.Button onClick={updateChannels}>Update channels</Styled.Button>
</Col>
</Row>
);

return (
<>
<Button onClick={showModal}>
<SwapOutlined /> Balance channels
</Button>
<Modal
title="Balance Channels"
open={visible}
onCancel={hideModal}
footer={CustomModalFooter}
style={{ fontVariant: 'small-caps' }}
>
{/* sliders */}
{info.map((channel: ChannelInfo, index: number) => {
const { to, from, id, remoteBalance, localBalance, nextLocalBalance } = channel;
const total = Number(remoteBalance) + Number(localBalance);
return (
<div key={id}>
<Row>
<Col span={12}>
{from}
<br />
{nextLocalBalance}
</Col>
<Col span={12} style={{ textAlign: 'right' }}>
{to}
<br />
{total - nextLocalBalance}
</Col>
</Row>
<Slider
value={nextLocalBalance}
onChange={value => setNextBalance(value, index)}
min={0}
max={total}
/>
</div>
);
})}
{/* end sliders */}
<br />
<Row gutter={10}>
<Col span={12}>
<Styled.Button onClick={updateInfo}>
<ReloadOutlined /> Refresh
</Styled.Button>
</Col>
<Col span={12}>
<Styled.Button onClick={autoBalance}>
<PercentageOutlined /> Auto Balance
</Styled.Button>
</Col>
</Row>
</Modal>
</>
);
};

export default AutoBalanceButton;
13 changes: 7 additions & 6 deletions src/components/designer/SyncButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { ReactNode } from 'react';
import { useAsyncCallback } from 'react-async-hook';
import { ReloadOutlined } from '@ant-design/icons';
import styled from '@emotion/styled';
Expand All @@ -9,17 +9,16 @@ import { Network } from 'types';

const Styled = {
Button: styled(Button)`
margin-left: 8px;
font-size: 18px;
padding: 2px 0 !important;
width: 100%;
`,
};

interface Props {
network: Network;
children?: ReactNode;
}

const SyncButton: React.FC<Props> = ({ network }) => {
const SyncButton: React.FC<Props> = ({ children, network }) => {
const { l } = usePrefixedTranslation('cmps.designer.SyncButton');
const { notify } = useStoreActions(s => s.app);
const { syncChart } = useStoreActions(s => s.designer);
Expand All @@ -42,7 +41,9 @@ const SyncButton: React.FC<Props> = ({ network }) => {
icon={<ReloadOutlined />}
onClick={syncChartAsync.execute}
loading={syncChartAsync.loading}
/>
>
{children}
</Styled.Button>
</Tooltip>
);
};
Expand Down
58 changes: 44 additions & 14 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,20 +12,23 @@ 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 AutoMineButton from 'components/designer/AutoMineButton';
import BalanceChannelsButton from 'components/designer/BalanceChannelsButton';
import SyncButton from 'components/designer/SyncButton';

const Styled = {
Button: styled(Button)`
margin-left: 0;
`,
ButtonBlock: styled(Button)`
width: 100%;
`,
Dropdown: styled(Dropdown)`
margin-left: 12px;
`,
Expand Down Expand Up @@ -110,26 +114,52 @@ const NetworkActions: React.FC<Props> = ({
}
}, []);

const items: MenuProps['items'] = [
const networkActions: MenuProps['items'] = [
{ key: 'rename', label: l('menuRename'), icon: <FormOutlined /> },
{ key: 'export', label: l('menuExport'), icon: <ExportOutlined /> },
{ key: 'delete', label: l('menuDelete'), icon: <CloseOutlined /> },
];

const QuickMineButton = () => (
<Styled.ButtonBlock
onClick={mineAsync.execute}
loading={mineAsync.loading}
icon={<ToolOutlined />}
>
{l('mineBtn')}
</Styled.ButtonBlock>
);

const networkQuickActions: MenuProps['items'] = [
{
key: 'sync',
label: <SyncButton network={network}>Synchronize</SyncButton>,
},
{
key: 'quickMine',
label: <QuickMineButton />,
},
{
key: 'autoMine',
label: <AutoMineButton network={network} />,
},
{
key: 'balance',
label: <BalanceChannelsButton network={network} />,
},
];

return (
<>
{bitcoinNode.status === Status.Started && nodeState?.chainInfo && (
<>
<Tag>height: {nodeState.chainInfo.blocks}</Tag>
<Button
onClick={mineAsync.execute}
loading={mineAsync.loading}
icon={<ToolOutlined />}
<Styled.Dropdown
key="options"
menu={{ theme: 'dark', items: networkQuickActions }}
>
{l('mineBtn')}
</Button>
<AutoMineButton network={network} />
<SyncButton network={network} />
<Button icon={<MoreOutlined />}>Quick Actions</Button>
</Styled.Dropdown>
<Divider type="vertical" />
</>
)}
Expand All @@ -146,7 +176,7 @@ const NetworkActions: React.FC<Props> = ({
</Styled.Button>
<Styled.Dropdown
key="options"
menu={{ theme: 'dark', items, onClick: handleClick }}
menu={{ theme: 'dark', items: networkActions, onClick: handleClick }}
>
<Button icon={<MoreOutlined />} />
</Styled.Dropdown>
Expand Down
14 changes: 14 additions & 0 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,20 @@ export interface TapdNode extends TapNode {
};
}

export interface ChannelInfo {
id: string;
to: string;
from: string;
localBalance: string;
remoteBalance: string;
nextLocalBalance: number;
}

export interface PreInvoice {
channelId: string;
nextLocalBalance: number;
}

export type NodeImplementation =
| BitcoinNode['implementation']
| LightningNode['implementation']
Expand Down
1 change: 1 addition & 0 deletions src/store/models/lightning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,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
Loading

0 comments on commit 49be98d

Please sign in to comment.