Skip to content

Commit

Permalink
feat: balance channels modal
Browse files Browse the repository at this point in the history
Adds modal that allows users to manually balance channels using a
slicers, or to evenly balance the channels automatically via button.
  • Loading branch information
uwla committed Jun 16, 2024
1 parent 1e31b94 commit a5eb13e
Show file tree
Hide file tree
Showing 13 changed files with 371 additions and 11 deletions.
32 changes: 32 additions & 0 deletions src/components/common/BalanceChannelsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { SwapOutlined } from '@ant-design/icons';
import styled from '@emotion/styled';
import { Button } 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, resetBalanceChannels } = useStoreActions(s => s.modals);

return (
<Styled.Button
onClick={() => resetBalanceChannels(network).then(showBalanceChannels)}

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#L25

Added line #L25 was not covered by tests
>
<SwapOutlined /> {l('btn')}
</Styled.Button>
);
};

export default BalanceChannelsButton;
87 changes: 87 additions & 0 deletions src/components/common/BalanceChannelsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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';

interface Props {
network: Network;
}

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

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

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L14-L15

Added lines #L14 - L15 were not covered by tests
const {
resetBalanceChannels,
hideBalanceChannels,
autoBalanceChannels,
manualBalanceChannels,
updateBalanceOfChannels,
} = useStoreActions(s => s.modals);

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

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L22

Added line #L22 was not covered by tests

const CustomModalFooter = (
<Row gutter={10} justify="end">

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

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L25

Added line #L25 was not covered by tests
<Col>
<Button onClick={() => hideBalanceChannels()}>{l('close')}</Button>

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

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L27

Added line #L27 was not covered by tests
</Col>
<Col>
<Button onClick={() => updateBalanceOfChannels(network)}>{l('update')}</Button>

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
</Col>
</Row>
);

return (

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

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L35

Added line #L35 was not covered by tests
<Modal
title="Balance Channels"
open={visible}
footer={CustomModalFooter}
onCancel={() => hideBalanceChannels()}

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

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L40

Added line #L40 was 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 46 in src/components/common/BalanceChannelsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L44-L46

Added lines #L44 - L46 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 => manualBalanceChannels({ value, index })}

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

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L62

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

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

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L73

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

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

View check run for this annotation

Codecov / codecov/patch

src/components/common/BalanceChannelsModal.tsx#L78

Added line #L78 was not covered by tests
<PercentageOutlined /> <span>{l('autoBalance')}</span>
</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
9 changes: 6 additions & 3 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 @@ -17,9 +17,10 @@ const Styled = {

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 +43,9 @@ const SyncButton: React.FC<Props> = ({ network }) => {
icon={<ReloadOutlined />}
onClick={syncChartAsync.execute}
loading={syncChartAsync.loading}
/>
>
{children}
</Styled.Button>
</Tooltip>
);
};
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
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
116 changes: 115 additions & 1 deletion src/store/models/modals.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Action, action, Thunk, thunk } from 'easy-peasy';
import { StoreInjections } from 'types';
import { LightningNode } from 'shared/types';
import { LightningNodeChannel } from 'lib/lightning/types';
import { ChannelInfo, Network, PreInvoice, StoreInjections } from 'types';
import { RootModel } from './';

interface OpenChannelModel {
Expand Down Expand Up @@ -35,6 +37,11 @@ interface AdvancedOptionsModel {
defaultCommand?: string;
}

interface BalanceChannelsModel {
visible: boolean;
channelsInfo?: ChannelInfo[];
}

interface ImageUpdatesModel {
visible: boolean;
}
Expand Down Expand Up @@ -80,6 +87,7 @@ export interface ModalsModel {
createInvoice: CreateInvoiceModel;
payInvoice: PayInvoiceModel;
advancedOptions: AdvancedOptionsModel;
balanceChannels: BalanceChannelsModel;
imageUpdates: ImageUpdatesModel;
sendOnChain: SendOnChainModel;
assetInfo: AssetInfoModel;
Expand All @@ -102,6 +110,15 @@ export interface ModalsModel {
setAdvancedOptions: Action<ModalsModel, AdvancedOptionsModel>;
showAdvancedOptions: Thunk<ModalsModel, Partial<AdvancedOptionsModel>, StoreInjections>;
hideAdvancedOptions: Thunk<ModalsModel, void, StoreInjections, RootModel>;

setBalanceChannels: Action<ModalsModel, BalanceChannelsModel>;
showBalanceChannels: Thunk<ModalsModel, void, StoreInjections>;
hideBalanceChannels: Thunk<ModalsModel, void, StoreInjections, RootModel>;
resetBalanceChannels: Thunk<ModalsModel, Network, StoreInjections, RootModel>;
manualBalanceChannels: Action<ModalsModel, { value: number; index: number }>;
autoBalanceChannels: Action<ModalsModel>;
updateBalanceOfChannels: Thunk<ModalsModel, Network, StoreInjections, RootModel>;

setImageUpdates: Action<ModalsModel, ImageUpdatesModel>;
showImageUpdates: Thunk<ModalsModel, void, StoreInjections>;
hideImageUpdates: Thunk<ModalsModel, void, StoreInjections, RootModel>;
Expand Down Expand Up @@ -138,6 +155,7 @@ const modalsModel: ModalsModel = {
createInvoice: { visible: false },
payInvoice: { visible: false },
advancedOptions: { visible: false },
balanceChannels: { visible: false },
imageUpdates: { visible: false },
sendOnChain: { visible: false },
assetInfo: { visible: false },
Expand Down Expand Up @@ -235,6 +253,102 @@ const modalsModel: ModalsModel = {
defaultCommand: undefined,
});
}),
setBalanceChannels: action((state, payload) => {
state.balanceChannels = {

Check warning on line 257 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L256-L257

Added lines #L256 - L257 were not covered by tests
...state.balanceChannels,
...payload,
};
}),
showBalanceChannels: thunk(actions => {
actions.setBalanceChannels({ visible: true });

Check warning on line 263 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L262-L263

Added lines #L262 - L263 were not covered by tests
}),
hideBalanceChannels: thunk(actions => {
actions.setBalanceChannels({ visible: false });

Check warning on line 266 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L265-L266

Added lines #L265 - L266 were not covered by tests
}),
resetBalanceChannels: thunk(
async (actions, network, { getStoreActions, getStoreState }) => {
const channels = [] as LightningNodeChannel[];
const { getChannels } = getStoreActions().lightning;
const { links } = getStoreState().designer.activeChart;

Check warning on line 272 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L269-L272

Added lines #L269 - L272 were not covered by tests

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

Check warning on line 276 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L274-L276

Added lines #L274 - L276 were not covered by tests

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

Check warning on line 282 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L278-L282

Added lines #L278 - L282 were not covered by tests
}),
);
}
await Promise.all(promisesToAwait);

Check warning on line 286 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L286

Added line #L286 was not covered by tests

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

Check warning on line 289 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L288-L289

Added lines #L288 - L289 were not covered by tests
if (!links[id]) continue;
const from = links[id].from.nodeId;
const to = links[id].to.nodeId as string;
const nextLocalBalance = Number(localBalance);
channelsInfo.push({

Check warning on line 294 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L291-L294

Added lines #L291 - L294 were not covered by tests
id,
to,
from,
localBalance,
remoteBalance,
nextLocalBalance,
});
}

actions.setBalanceChannels({ channelsInfo } as BalanceChannelsModel);

Check warning on line 304 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L304

Added line #L304 was not covered by tests
},
),
manualBalanceChannels: action((state, { value, index }) => {
const { channelsInfo: info } = state.balanceChannels;

Check warning on line 308 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L307-L308

Added lines #L307 - L308 were not covered by tests
if (info && info[index]) {
info[index].nextLocalBalance = value;
state.balanceChannels.channelsInfo = info;

Check warning on line 311 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L310-L311

Added lines #L310 - L311 were not covered by tests
}
}),
autoBalanceChannels: action(state => {
const { channelsInfo } = state.balanceChannels;

Check warning on line 315 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L314-L315

Added lines #L314 - L315 were not covered by tests
if (!channelsInfo) {
return;

Check warning on line 317 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L317

Added line #L317 was not covered by tests
}
for (let index = 0; index < channelsInfo.length; index += 1) {
const { localBalance, remoteBalance } = channelsInfo[index];
const halfAmount = Math.floor((Number(localBalance) + Number(remoteBalance)) / 2);
channelsInfo[index].nextLocalBalance = halfAmount;

Check warning on line 322 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L319-L322

Added lines #L319 - L322 were not covered by tests
}
state.balanceChannels = {

Check warning on line 324 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L324

Added line #L324 was not covered by tests
...state.balanceChannels,
...channelsInfo,
};
}),
updateBalanceOfChannels: thunk(
(actions, network, { getStoreActions, getStoreState }) => {
const { notify } = getStoreActions().app;
const { balanceChannels } = getStoreActions().network;
const { channelsInfo } = getStoreState().modals.balanceChannels;

Check warning on line 333 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L330-L333

Added lines #L330 - L333 were not covered by tests

if (!channelsInfo) {
return;

Check warning on line 336 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L336

Added line #L336 was not covered by tests
}

const toPay = [] as PreInvoice[];
for (const { id, localBalance, nextLocalBalance } of channelsInfo) {

Check warning on line 340 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L339-L340

Added lines #L339 - L340 were not covered by tests
if (Number(localBalance) !== nextLocalBalance) {
toPay.push({ channelId: id, nextLocalBalance });

Check warning on line 342 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L342

Added line #L342 was not covered by tests
}
}

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

Check warning on line 348 in src/store/models/modals.ts

View check run for this annotation

Codecov / codecov/patch

src/store/models/modals.ts#L346-L348

Added lines #L346 - L348 were not covered by tests
},
),

setImageUpdates: action((state, payload) => {
state.imageUpdates = {
...state.imageUpdates,
Expand Down
Loading

0 comments on commit a5eb13e

Please sign in to comment.