Merge pull request #1292 from Talej/forceclose

Channels: force close channel timer display
This commit is contained in:
Evan Kaloudis
2023-02-11 19:09:54 -05:00
committed by GitHub
12 changed files with 255 additions and 26 deletions
+1
View File
@@ -201,6 +201,7 @@ export default class CLightningREST extends LND {
supportsOnchainReceiving = () => true;
supportsKeysend = () => true;
supportsChannelManagement = () => true;
supportsPendingChannels = () => false;
supportsMPP = () => false;
supportsAMP = () => false;
supportsCoinControl = () => this.supports('v0.8.2', undefined, 'v0.4.0');
+1
View File
@@ -474,6 +474,7 @@ export default class Eclair {
supportsOnchainReceiving = () => true;
supportsKeysend = () => false;
supportsChannelManagement = () => true;
supportsPendingChannels = () => false;
supportsMPP = () => false;
supportsAMP = () => false;
supportsCoinControl = () => false;
+3
View File
@@ -204,6 +204,8 @@ export default class LND {
transactions: data.transactions
}));
getChannels = () => this.getRequest('/v1/channels');
getPendingChannels = () => this.getRequest('/v1/channels/pending');
getClosedChannels = () => this.getRequest('/v1/channels/closed');
getChannelInfo = (chanId: string) =>
this.getRequest(`/v1/graph/edge/${chanId}`);
getBlockchainBalance = () => this.getRequest('/v1/balance/blockchain');
@@ -355,6 +357,7 @@ export default class LND {
supportsOnchainReceiving = () => true;
supportsKeysend = () => true;
supportsChannelManagement = () => true;
supportsPendingChannels = () => true;
supportsMPP = () => this.supports('v0.10.0');
supportsAMP = () => this.supports('v0.13.0');
supportsCoinControl = () => this.supports('v0.12.0');
+9
View File
@@ -53,6 +53,14 @@ export default class LightningNodeConnect {
await this.lnc.lnd.lightning
.listChannels({})
.then((data: lnrpc.ListChannelsResponse) => snakeize(data));
getPendingChannels = async () =>
await this.lnc.lnd.lightning
.pendingChannels({})
.then((data: lnrpc.PendingChannelsResponse) => snakeize(data));
getClosedChannels = async () =>
await this.lnc.lnd.lightning
.closedChannels({})
.then((data: lnrpc.ClosedChannelsResponse) => snakeize(data));
getChannelInfo = async (chanId: string) => {
const request: lnrpc.ChanInfoRequest = { chanId };
return await this.lnc.lnd.lightning
@@ -291,6 +299,7 @@ export default class LightningNodeConnect {
supportsOnchainReceiving = () => true;
supportsKeysend = () => true;
supportsChannelManagement = () => true;
supportsPendingChannels = () => true;
supportsMPP = () => this.supports('v0.10.0');
supportsAMP = () => this.supports('v0.13.0');
supportsCoinControl = () => this.supports('v0.12.0');
+1
View File
@@ -89,6 +89,7 @@ export default class LndHub extends LND {
);
supportsKeysend = () => false;
supportsChannelManagement = () => false;
supportsPendingChannels = () => false;
supportsMPP = () => false;
supportsAMP = () => false;
supportsCoinControl = () => false;
+1
View File
@@ -316,6 +316,7 @@ export default class Spark {
supportsOnchainReceiving = () => true;
supportsKeysend = () => false;
supportsChannelManagement = () => true;
supportsPendingChannels = () => false;
supportsMPP = () => false;
supportsAMP = () => false;
supportsCoinControl = () => false;
+32 -10
View File
@@ -13,23 +13,32 @@ import { themeColor } from './../../utils/ThemeUtils';
import Stores from '../../stores/Stores';
import ClockIcon from '../../assets/images/SVG/Clock.svg';
export function ChannelItem({
title,
inbound,
outbound,
largestTotal,
status
status,
pendingTimelock
}: {
title: string;
inbound: number;
outbound: number;
largestTotal?: number;
status: Status;
pendingTimelock?: String;
}) {
const { settings } = Stores.settingsStore;
const { privacy } = settings;
const lurkerMode = (privacy && privacy.lurkerMode) || false;
const isOffline =
status === Status.Offline ||
status === Status.Closing ||
status === Status.Opening;
const percentOfLargest = largestTotal
? (Number(inbound) + Number(outbound)) / largestTotal
: 1.0;
@@ -47,17 +56,30 @@ export function ChannelItem({
<View style={{ flex: 1, paddingRight: 10 }}>
<Body>{PrivacyUtils.sensitiveValue(title)}</Body>
</View>
{pendingTimelock ? (
<View style={{ flexDirection: 'row', marginRight: 5 }}>
<ClockIcon
color={themeColor('bitcoin')}
width={17}
height={17}
style={{ marginRight: 5 }}
/>
<Body small={true}>{pendingTimelock}</Body>
</View>
) : null}
<Tag status={status} />
</Row>
<Row>
<BalanceBar
left={lurkerMode ? 50 : Number(outbound)}
right={lurkerMode ? 50 : Number(inbound)}
offline={status === Status.Offline}
percentOfLargest={percentOfLargest}
showProportionally={lurkerMode ? false : true}
/>
</Row>
{inbound && outbound && (
<Row>
<BalanceBar
left={lurkerMode ? 50 : Number(outbound)}
right={lurkerMode ? 50 : Number(inbound)}
offline={isOffline}
percentOfLargest={percentOfLargest}
showProportionally={lurkerMode ? false : true}
/>
</Row>
)}
<Row justify="space-between">
<Amount sats={outbound} sensitive />
<Amount sats={inbound} sensitive />
+2
View File
@@ -24,6 +24,8 @@ export function Tag({ status }: { status: Status }) {
colors.background = '#E14C4C';
colors.dot = '#FF0000';
break;
case Status.Opening:
case Status.Closing:
case Status.Offline:
colors.background = '#A7A9AC';
colors.dot = '#E5E5E5';
+34
View File
@@ -0,0 +1,34 @@
import { computed } from 'mobx';
import BaseModel from './BaseModel';
import { localeString } from './../utils/LocaleUtils';
export default class ChannelClose extends BaseModel {
channel_point: string;
chain_hash: string;
chan_id: string;
closing_tx_hash: string;
remote_pubkey: string;
capacity: string;
close_height: number;
settled_balance: string;
time_lock_balance: string;
close_time: number;
open_initiator: any;
close_initiator: any;
resolutions: any;
@computed
public get channelId(): string {
return this.chan_id || localeString('models.Channel.unknownId');
}
@computed
public get localBalance(): string {
return this.settled_balance;
}
@computed
public get remoteBalance(): string {
return `${Number(this.capacity) - Number(this.settled_balance)}`;
}
}
+52
View File
@@ -1,6 +1,7 @@
import { action, observable, reaction } from 'mobx';
import { randomBytes } from 'react-native-randombytes';
import Channel from './../models/Channel';
import ChannelClose from './../models/ChannelClose';
import ChannelInfo from './../models/ChannelInfo';
import OpenChannelRequest from './../models/OpenChannelRequest';
import CloseChannelRequest from './../models/CloseChannelRequest';
@@ -22,6 +23,8 @@ export default class ChannelsStore {
@observable public nodes: any = {};
@observable public aliasesById: any = {};
@observable public channels: Array<Channel> = [];
@observable public pendingChannels: Array<Channel> = [];
@observable public closedChannels: Array<Channel> = [];
@observable public output_index: number | null;
@observable public funding_txid_str: string | null;
@observable public openingChannel = false;
@@ -137,6 +140,55 @@ export default class ChannelsStore {
});
this.channels = channels;
this.error = false;
})
.catch(() => {
this.getChannelsError();
});
BackendUtils.getPendingChannels()
.then((data: any) => {
const pendingOpenChannels = data.pending_open_channels.map(
(pending: any) => {
pending.channel.pendingOpen = true;
return new Channel(pending.channel);
}
);
const pendingCloseChannels = data.pending_closing_channels.map(
(pending: any) => {
pending.channel.pendingClose = true;
return new Channel(pending.channel);
}
);
const forceCloseChannels =
data.pending_force_closing_channels.map((pending: any) => {
pending.channel.blocks_til_maturity =
pending.blocks_til_maturity;
pending.channel.forceClose = true;
return new Channel(pending.channel);
});
const waitCloseChannels = data.waiting_close_channels.map(
(pending: any) => {
pending.channel.closing = true;
return new Channel(pending.channel);
}
);
this.pendingChannels = pendingOpenChannels
.concat(pendingCloseChannels)
.concat(forceCloseChannels)
.concat(waitCloseChannels);
this.error = false;
})
.catch(() => {
this.getChannelsError();
});
BackendUtils.getClosedChannels()
.then((data: any) => {
const closedChannels = data.channels.map(
(channel: any) => new ChannelClose(channel)
);
this.closedChannels = closedChannels;
this.error = false;
this.loading = false;
})
.catch(() => {
+5
View File
@@ -54,6 +54,10 @@ class BackendUtils {
getTransactions = (...args: any[]) => this.call('getTransactions', args);
getChannels = (...args: any[]) => this.call('getChannels', args);
getPendingChannels = (...args: any[]) =>
this.call('getPendingChannels', args);
getClosedChannels = (...args: any[]) =>
this.call('getClosedChannels', args);
getChannelInfo = (...args: any[]) => this.call('getChannelInfo', args);
getBlockchainBalance = (...args: any[]) =>
this.call('getBlockchainBalance', args);
@@ -110,6 +114,7 @@ class BackendUtils {
supportsOnchainReceiving = () => this.call('supportsOnchainReceiving');
supportsKeysend = () => this.call('supportsKeysend');
supportsChannelManagement = () => this.call('supportsChannelManagement');
supportsPendingChannels = () => this.call('supportsPendingChannels');
supportsMPP = () => this.call('supportsMPP');
supportsAMP = () => this.call('supportsAMP');
supportsCoinControl = () => this.call('supportsCoinControl');
+114 -16
View File
@@ -1,5 +1,10 @@
import * as React from 'react';
import { FlatList, View, TouchableHighlight } from 'react-native';
import {
FlatList,
View,
TouchableHighlight,
TouchableOpacity
} from 'react-native';
import { inject, observer } from 'mobx-react';
@@ -14,12 +19,17 @@ import { Spacer } from '../../components/layout/Spacer';
import ChannelsStore from '../../stores/ChannelsStore';
import SettingsStore from '../../stores/SettingsStore';
import { duration } from 'moment';
import BackendUtils from '../../utils/BackendUtils';
// TODO: does this belong in the model? Or can it be computed from the model?
export enum Status {
Good = 'Good',
Stable = 'Stable',
Unstable = 'Unstable',
Offline = 'Offline'
Offline = 'Offline',
Opening = 'Opening',
Closing = 'Closing'
}
interface ChannelsProps {
@@ -28,12 +38,20 @@ interface ChannelsProps {
SettingsStore: SettingsStore;
}
interface ChannelsState {
channelsType: number;
}
@inject('ChannelsStore', 'SettingsStore')
@observer
export default class ChannelsPane extends React.PureComponent<
ChannelsProps,
{}
ChannelsState
> {
state = {
channelsType: 0
};
renderItem = ({ item }) => {
const { ChannelsStore, navigation } = this.props;
const { nodes, largestChannelSats } = ChannelsStore;
@@ -42,6 +60,43 @@ export default class ChannelsPane extends React.PureComponent<
(nodes[item.remote_pubkey] && nodes[item.remote_pubkey].alias) ||
item.remote_pubkey ||
item.channelId;
const getStatus = () => {
if (item.isActive) {
return Status.Good;
} else if (item.pendingOpen) {
return Status.Opening;
} else if (item.pendingClose || item.forceClose) {
return Status.Closing;
} else {
return Status.Offline;
}
};
const forceCloseTimeLabel = (maturity: number) => {
return duration(maturity * 10, 'minutes').humanize();
};
if (this.state.channelsType === 0) {
return (
<TouchableHighlight
onPress={() =>
navigation.navigate('Channel', {
channel: item
})
}
>
<ChannelItem
title={displayName}
status={getStatus()}
inbound={item.remoteBalance}
outbound={item.localBalance}
largestTotal={largestChannelSats}
/>
</TouchableHighlight>
);
}
return (
<TouchableHighlight
onPress={() =>
@@ -52,37 +107,80 @@ export default class ChannelsPane extends React.PureComponent<
>
<ChannelItem
title={displayName}
status={item.isActive ? Status.Good : Status.Offline}
inbound={item.remoteBalance}
outbound={item.localBalance}
largestTotal={largestChannelSats}
status={getStatus()}
pendingTimelock={
item.forceClose
? forceCloseTimeLabel(item.blocks_til_maturity)
: null
}
/>
</TouchableHighlight>
);
};
toggleChannelsType = () => {
const { channelsType } = this.state;
if (channelsType === 2) {
this.setState({
channelsType: 0
});
} else {
this.setState({
channelsType: channelsType + 1
});
}
};
render() {
const { ChannelsStore, SettingsStore, navigation } = this.props;
const { channelsType } = this.state;
const {
loading,
getChannels,
totalInbound,
totalOutbound,
totalOffline,
channels
channels,
pendingChannels,
closedChannels
} = ChannelsStore;
const headerString = `${localeString(
'views.Wallet.Wallet.channels'
)} (${channels.length})`;
let headerString;
let channelsData;
switch (channelsType) {
case 0:
headerString = `${localeString(
'views.Wallet.Wallet.channels'
)} (${channels.length})`;
channelsData = channels;
break;
case 1:
headerString = `Pending Channels (${pendingChannels.length})`;
channelsData = pendingChannels;
break;
case 2:
headerString = `Closed Channels (${closedChannels.length})`;
channelsData = closedChannels;
break;
}
return (
<View style={{ flex: 1 }}>
<WalletHeader
navigation={navigation}
title={headerString}
SettingsStore={SettingsStore}
channels
/>
<TouchableOpacity
onPress={() => {
if (BackendUtils.supportsPendingChannels())
this.toggleChannelsType();
}}
>
<WalletHeader
navigation={navigation}
title={headerString}
SettingsStore={SettingsStore}
channels
/>
</TouchableOpacity>
<ChannelsHeader
totalInbound={totalInbound}
totalOutbound={totalOutbound}
@@ -92,7 +190,7 @@ export default class ChannelsPane extends React.PureComponent<
<LoadingIndicator />
) : (
<FlatList
data={channels}
data={channelsData}
renderItem={this.renderItem}
ListFooterComponent={<Spacer height={100} />}
onRefresh={() => getChannels()}