Merge pull request #1292 from Talej/forceclose
Channels: force close channel timer display
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -474,6 +474,7 @@ export default class Eclair {
|
||||
supportsOnchainReceiving = () => true;
|
||||
supportsKeysend = () => false;
|
||||
supportsChannelManagement = () => true;
|
||||
supportsPendingChannels = () => false;
|
||||
supportsMPP = () => false;
|
||||
supportsAMP = () => false;
|
||||
supportsCoinControl = () => false;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -89,6 +89,7 @@ export default class LndHub extends LND {
|
||||
);
|
||||
supportsKeysend = () => false;
|
||||
supportsChannelManagement = () => false;
|
||||
supportsPendingChannels = () => false;
|
||||
supportsMPP = () => false;
|
||||
supportsAMP = () => false;
|
||||
supportsCoinControl = () => false;
|
||||
|
||||
@@ -316,6 +316,7 @@ export default class Spark {
|
||||
supportsOnchainReceiving = () => true;
|
||||
supportsKeysend = () => false;
|
||||
supportsChannelManagement = () => true;
|
||||
supportsPendingChannels = () => false;
|
||||
supportsMPP = () => false;
|
||||
supportsAMP = () => false;
|
||||
supportsCoinControl = () => false;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
}
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
@@ -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()}
|
||||
|
||||
Reference in New Issue
Block a user