Skip to content
This repository was archived by the owner on Feb 23, 2021. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/action/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,13 +259,24 @@ class ChannelAction {
* the necessary error handling and notification display.
* @return {Promise<undefined>}
*/
async closeSelectedChannel() {
async closeSelectedChannel(force = false) {
try {
const { selectedChannel } = this._store;
this._nav.goChannels();
await this.closeChannel({ channelPoint: selectedChannel.channelPoint });
await this.closeChannel({
channelPoint: selectedChannel.channelPoint,
force,
});
} catch (err) {
this._notification.display({ msg: 'Closing channel failed!', err });
if (
err &&
err.details &&
err.details.includes('try force closing it instead')
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're checking the string contents of the error message here because LND returns a generic/unknown error code.

https://github.com/lightningnetwork/lnd/blob/master/rpcserver.go#L1138
https://github.com/grpc/grpc-go/blob/master/codes/codes.go#L43

) {
this._nav.goChannelForceDelete();
} else {
this._notification.display({ msg: 'Closing channel failed!', err });
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/action/nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ class NavAction {
this._store.route = 'ChannelDelete';
}

goChannelForceDelete() {
this._store.route = 'ChannelForceDelete';
}

goChannelCreate() {
this._store.route = 'ChannelCreate';
}
Expand Down
1 change: 1 addition & 0 deletions src/computed/channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const ComputedChannel = store => {
);
all.sort((a, b) => (a.active === b.active ? 0 : a.active ? -1 : 1));
all.forEach(c => {
c.isClosing = !c.status.includes('open');
c.statusLabel = toCaps(c.status);
c.capacityLabel = toAmountLabel(c.capacity, settings);
c.localBalanceLabel = toAmountLabel(c.localBalance, settings);
Expand Down
6 changes: 5 additions & 1 deletion src/view/channel-detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ const ChannelDetailView = ({ store, nav }) => (
<DetailField name="Balance">
{store.selectedChannel.localBalanceLabel} {store.unitLabel}
</DetailField>
<Button style={styles.deleteBtn} onPress={() => nav.goChannelDelete()}>
<Button
style={styles.deleteBtn}
disabled={store.selectedChannel.isClosing}
onPress={() => nav.goChannelDelete()}
>
<ButtonText style={styles.deleteBtnText}>CLOSE CHANNEL</ButtonText>
</Button>
</Modal>
Expand Down
55 changes: 55 additions & 0 deletions src/view/channel-force-delete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import { observer } from 'mobx-react';
import PropTypes from 'prop-types';
import Background from '../component/background';
import MainContent from '../component/main-content';
import { H1Text, CopyText } from '../component/text';
import { FormStretcher } from '../component/form';
import { Button, ButtonText, PillButton } from '../component/button';
import { color } from '../component/style';

const styles = StyleSheet.create({
copyTxt: {
marginTop: 10,
},
deleteBtn: {
alignSelf: 'center',
backgroundColor: color.glas,
width: 400,
},
cancelBtn: {
marginTop: 5,
marginBottom: 25,
},
});

const ChannelForceDeleteView = ({ nav, channel }) => (
<Background color={color.blackDark}>
<MainContent>
<FormStretcher>
<H1Text>Force close channel?</H1Text>
<CopyText style={styles.copyTxt}>
Mutual close of the channel failed. Force closing the channel will
result in a delay to access your funds.
</CopyText>
</FormStretcher>
<PillButton
style={styles.deleteBtn}
onPress={() => channel.closeSelectedChannel(true)}
>
Force close this channel
</PillButton>
<Button style={styles.cancelBtn} onPress={() => nav.goChannelDetail()}>
<ButtonText>Cancel</ButtonText>
</Button>
</MainContent>
</Background>
);

ChannelForceDeleteView.propTypes = {
nav: PropTypes.object.isRequired,
channel: PropTypes.object.isRequired,
};

export default observer(ChannelForceDeleteView);
4 changes: 4 additions & 0 deletions src/view/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import Deposit from './deposit';
import Channel from './channel';
import ChannelDetail from './channel-detail';
import ChannelDelete from './channel-delete';
import ChannelForceDelete from './channel-force-delete';
import ChannelCreate from './channel-create';
import Transaction from './transaction';
import Setting from './setting';
Expand Down Expand Up @@ -142,6 +143,9 @@ class MainView extends Component {
{route === 'ChannelDelete' && (
<ChannelDelete store={store} channel={channel} nav={nav} />
)}
{route === 'ChannelForceDelete' && (
<ChannelForceDelete store={store} channel={channel} nav={nav} />
)}
{route === 'ChannelCreate' && (
<ChannelCreate store={store} channel={channel} nav={nav} />
)}
Expand Down
15 changes: 15 additions & 0 deletions stories/screen-story.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import TransactionDetail from '../src/view/transaction-detail';
import Channel from '../src/view/channel';
import ChannelDetail from '../src/view/channel-detail';
import ChannelDelete from '../src/view/channel-delete';
import ChannelForceDelete from '../src/view/channel-force-delete';
import ChannelCreate from '../src/view/channel-create';
import Home from '../src/view/home';
import Deposit from '../src/view/deposit';
Expand Down Expand Up @@ -135,9 +136,23 @@ storiesOf('Screens', module)
<Channel store={{ computedChannels: [] }} channel={channel} nav={nav} />
))
.add('Channel Details', () => <ChannelDetail store={store} nav={nav} />)
.add('Channel Details (Closing)', () => (
<ChannelDetail
store={{
...store,
selectedChannel: store.pendingChannels.find(
c => c.status === 'pending-closing'
),
}}
nav={nav}
/>
))
.add('Channel Delete', () => (
<ChannelDelete store={store} channel={channel} nav={nav} />
))
.add('Channel Force Delete', () => (
<ChannelForceDelete store={store} channel={channel} nav={nav} />
))
.add('Channel Create', () => (
<ChannelCreate store={store} channel={channel} nav={nav} />
))
Expand Down
21 changes: 21 additions & 0 deletions test/unit/action/channel.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,27 @@ describe('Action Channels Unit Tests', () => {
});
});

it('should force close open channel and navigate to channels view', async () => {
await channel.closeSelectedChannel(true);
expect(nav.goChannels, 'was called once');
expect(channel.closeChannel, 'was called with', {
channelPoint: 'some-channel-point',
force: true,
});
});

it('should navigate to force delete channel view in case of channel link error', async () => {
channel.closeChannel.rejects({
code: 2,
details:
'unable to gracefully close channel while peer is offline (try force closing it instead): channel link not found',
});
await channel.closeSelectedChannel();
expect(nav.goChannels, 'was called once');
expect(notification.display, 'was not called');
expect(nav.goChannelForceDelete, 'was called once');
});

it('should display notification in case of error event', async () => {
channel.closeChannel.rejects(new Error('Boom!'));
await channel.closeSelectedChannel();
Expand Down
7 changes: 7 additions & 0 deletions test/unit/action/nav.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ describe('Action Nav Unit Tests', () => {
});
});

describe('goChannelForceDelete()', () => {
it('should set correct route', () => {
nav.goChannelForceDelete();
expect(store.route, 'to equal', 'ChannelForceDelete');
});
});

describe('goTransactions()', () => {
it('should set correct route', () => {
nav.goTransactions();
Expand Down
3 changes: 3 additions & 0 deletions test/unit/computed/channel.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ describe('Computed Channels Unit Tests', () => {
expect(store.computedChannels.length, 'to equal', 3);
const ch = store.computedChannels.find(t => t.id === '0');
expect(ch.statusLabel, 'to equal', 'Open');
expect(ch.isClosing, 'to equal', false);
expect(ch.capacityLabel, 'to match', /0[,.]02005/);
expect(ch.localBalanceLabel, 'to match', /0[,.]0199/);
expect(ch.remoteBalanceLabel, 'to match', /0[,.]0001/);
expect(store.channelBalanceOpenLabel, 'to match', /0[,.]0199/);
expect(store.channelBalancePendingLabel, 'to match', /0[,.]006/);
expect(store.channelBalanceClosingLabel, 'to match', /0[,.]005/);
expect(store.showChannelAlert, 'to equal', false);
const ch2 = store.computedChannels.find(t => t.id === '2');
expect(ch2.isClosing, 'to equal', true);
});

it('should channel values in usd', () => {
Expand Down