diff --git a/src/action/channel.js b/src/action/channel.js index 76a964cd0..e0da758ad 100644 --- a/src/action/channel.js +++ b/src/action/channel.js @@ -3,7 +3,7 @@ * call the corresponding GRPC apis for channel management. */ -import { toSatoshis, poll } from '../helper'; +import { toSatoshis, poll, getTimeTilAvailable } from '../helper'; import * as log from './log'; class ChannelAction { @@ -160,6 +160,7 @@ class ChannelAction { limboBalance: pfcc.limboBalance, maturityHeight: pfcc.maturityHeight, blocksTilMaturity: pfcc.blocksTilMaturity, + timeTilAvailable: getTimeTilAvailable(pfcc.blocksTilMaturity), status: 'pending-force-closing', })); const wccs = response.waitingCloseChannels.map(wcc => ({ diff --git a/src/helper.js b/src/helper.js index 67f56b905..30a0f1962 100644 --- a/src/helper.js +++ b/src/helper.js @@ -137,6 +137,23 @@ export const toLabel = (amount, settings) => { return toAmountLabel(satoshis, settings); }; +/** + * Convert a number of blocks to an amount of time in the format "X days and Y + * hours" assuming 10 minutes per block. + * @param {number} numBlocks The number of blocks to convert. + * @return {string} The amount of time the blocks is equivalent to. + */ +export const getTimeTilAvailable = numBlocks => { + if (!Number.isInteger(numBlocks)) { + throw new Error('Invalid input!'); + } + const days = Math.floor(numBlocks / (24 * 6)); + const hours = Math.floor((numBlocks % (24 * 6)) / 6); + const daysString = days === 1 ? 'day' : 'days'; + const hoursString = hours === 1 ? 'hour' : 'hours'; + return `${days} ${daysString} and ${hours} ${hoursString}`; +}; + /** * Split '_' separated words and convert to uppercase * @param {string} value The input string diff --git a/src/view/channel-detail.js b/src/view/channel-detail.js index c852a1bc1..7d92d3d60 100644 --- a/src/view/channel-detail.js +++ b/src/view/channel-detail.js @@ -48,6 +48,11 @@ const ChannelDetailView = ({ store, nav }) => ( {store.selectedChannel.statusLabel} + {store.selectedChannel.timeTilAvailable ? ( + + {store.selectedChannel.timeTilAvailable} + + ) : null} {store.selectedChannel.capacityLabel} {store.unitLabel} diff --git a/test/unit/action/channel.spec.js b/test/unit/action/channel.spec.js index 3f04c809f..3a8822b61 100644 --- a/test/unit/action/channel.spec.js +++ b/test/unit/action/channel.spec.js @@ -146,7 +146,12 @@ describe('Action Channels Unit Tests', () => { grpc.sendCommand.withArgs('pendingChannels').resolves({ pendingOpenChannels: [{ channel: { ...pendingChannel } }], pendingClosingChannels: [{ channel: { ...pendingChannel } }], - pendingForceClosingChannels: [{ channel: { ...pendingChannel } }], + pendingForceClosingChannels: [ + { + channel: { ...pendingChannel }, + blocksTilMaturity: 463, + }, + ], waitingCloseChannels: [{ channel: { ...pendingChannel } }], totalLimboBalance: 1, }); @@ -156,6 +161,9 @@ describe('Action Channels Unit Tests', () => { remotePubkey: 'some-key', fundingTxId: 'FFFF', }); + expect(store.pendingChannels[2], 'to satisfy', { + timeTilAvailable: '3 days and 5 hours', + }); }); it('should log error on failure', async () => { diff --git a/test/unit/helper.spec.js b/test/unit/helper.spec.js index 1eba11788..a81f35f2e 100644 --- a/test/unit/helper.spec.js +++ b/test/unit/helper.spec.js @@ -817,4 +817,32 @@ describe('Helpers Unit Tests', () => { ); }); }); + + describe('getTimeTilAvailable()', () => { + it('should format blocks to human-readable time', () => { + const numBlocks = 463; + const time = helpers.getTimeTilAvailable(numBlocks); + expect(time, 'to equal', '3 days and 5 hours'); + }); + + it('should error on undefined', () => { + expect( + helpers.getTimeTilAvailable.bind(undefined), + 'to throw', + /Invalid/ + ); + }); + + it('should work for 1 day and 1 hour', () => { + const numBlocks = 150; + const time = helpers.getTimeTilAvailable(numBlocks); + expect(time, 'to equal', '1 day and 1 hour'); + }); + + it('should work for 0', () => { + const numBlocks = 0; + const time = helpers.getTimeTilAvailable(numBlocks); + expect(time, 'to equal', '0 days and 0 hours'); + }); + }); });