From 096068683b7b14275e835d88296b8c8da69c493d Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 1 Aug 2018 18:00:40 +0200 Subject: [PATCH 01/17] Add docs to helper.js --- src/helper.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/helper.js b/src/helper.js index d27c374b7..3904e466e 100644 --- a/src/helper.js +++ b/src/helper.js @@ -1,3 +1,7 @@ +/** + * @fileOverview helper and utility functions that can be reused go here. + */ + import { UNITS, LND_INIT_DELAY } from './config'; /** From 458ae442d49a6c3ebc53e9aa5aa48990ec9c243f Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 1 Aug 2018 18:00:52 +0200 Subject: [PATCH 02/17] Add docs to app-storage.js --- src/action/app-storage.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/action/app-storage.js b/src/action/app-storage.js index 18d2b23fb..f1deabd9f 100644 --- a/src/action/app-storage.js +++ b/src/action/app-storage.js @@ -1,3 +1,8 @@ +/** + * @fileOverview repesents the local storage database on a user's device + * which can be used to persist user settings on disk. + */ + import * as log from './log'; class AppStorage { @@ -6,6 +11,12 @@ class AppStorage { this._AsyncStorage = AsyncStorage; } + /** + * Read the user settings from disk and set them accordingly in the + * application state. After the state has bee read to the global + * `store` instance `store.loaded` is set to true. + * @return {Promise} + */ async restore() { try { const stateString = await this._AsyncStorage.getItem('settings'); @@ -24,6 +35,11 @@ class AppStorage { } } + /** + * Persist the user settings to disk so that they may be read the + * next time the application is opened by the user. + * @return {Promise} + */ async save() { try { const state = JSON.stringify(this._store.settings); @@ -34,6 +50,11 @@ class AppStorage { } } + /** + * Delete all of the data in local storage completely. Should be used + * carefully e.g. when the user wants to wipe the data on disk. + * @return {Promise} + */ async clear() { try { await this._AsyncStorage.clear(); From 15f7d72de5a8e1bb589ff5ac5fc227a1b615f843 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Wed, 1 Aug 2018 18:01:13 +0200 Subject: [PATCH 03/17] [WIP] Add docs to channel action --- src/action/channel.js | 62 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/action/channel.js b/src/action/channel.js index 9e9f8112b..b63ab84e6 100644 --- a/src/action/channel.js +++ b/src/action/channel.js @@ -1,3 +1,8 @@ +/** + * @fileOverview actions to set channel state within the app and to + * call the corresponding GRPC apis for channel management. + */ + import { toSatoshis, parseSat } from '../helper'; import * as log from './log'; @@ -13,16 +18,31 @@ class ChannelAction { // Create channel actions // + /** + * Initiate the create channel view by resetting input values + * and then navigating to the view. + * @return {undefined} + */ initCreate() { this._store.channel.pubkeyAtHost = ''; this._store.channel.amount = ''; this._nav.goChannelCreate(); } + /** + * Set the amount input for the create channel view. This amount + * is either in btc or fiat depending on user settings. + * @param {string} options.amount The string formatted number + */ setAmount({ amount }) { this._store.channel.amount = amount; } + /** + * Set the channel public key and hostname in a single variable + * which can be parsed before calling the create channel grpc api. + * @param {string} options.pubkeyAtHost The combined public key and host + */ setPubkeyAtHost({ pubkeyAtHost }) { this._store.channel.pubkeyAtHost = pubkeyAtHost; } @@ -31,17 +51,33 @@ class ChannelAction { // Channel list actions // + /** + * Initiate the channel list view by navigating to the view and updating + * the app's channel state by calling all necessary grpc apis. + * @return {undefined} + */ init() { this._nav.goChannels(); this.update(); } + /** + * Select a channel item from the channel list view and then navigate + * to the detail view to list channel parameters. + * @param {Object} options.item The selected channel object + * @return {undefined} + */ select({ item }) { this._store.selectedChannel = item; this._nav.goChannelDetail(); this.update(); } + /** + * Update the peers, channels, and pending channels in the app state + * by querying all required grpc apis. + * @return {Promise} + */ async update() { await Promise.all([ this.getPeers(), @@ -50,6 +86,15 @@ class ChannelAction { ]); } + // + // Generic channel actions + // + + /** + * List the open channels by calling the respective grpc api and updating + * the channels array in the global store. + * @return {Promise} + */ async getChannels() { try { const { channels } = await this._grpc.sendCommand('listChannels'); @@ -69,6 +114,11 @@ class ChannelAction { } } + /** + * List the pending channels by calling the respective grpc api and updating + * the pendingChannels array in the global store. + * @return {Promise} + */ async getPendingChannels() { try { const response = await this._grpc.sendCommand('pendingChannels'); @@ -112,6 +162,11 @@ class ChannelAction { } } + /** + * List the peers by calling the respective grpc api and updating + * the peers array in the global store. + * @return {Promise} + */ async getPeers() { try { const { peers } = await this._grpc.sendCommand('listPeers'); @@ -131,6 +186,13 @@ class ChannelAction { } } + /** + * Attempty to connect to a peer and open a channel in a single call. + * If a connection already exists, just a channel will be opened. + * This action can be called from a view event handler as does all + * the necessary error handling and notification display. + * @return {Promise} + */ async connectAndOpen() { try { const { channel, settings } = this._store; From a8cdc89d633f21375595b8627269c4cc5b67e68b Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 2 Aug 2018 09:12:44 +0200 Subject: [PATCH 04/17] Add return doc to helper.js --- src/helper.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/helper.js b/src/helper.js index 3904e466e..1f831837c 100644 --- a/src/helper.js +++ b/src/helper.js @@ -247,6 +247,7 @@ export const checkHttpStatus = response => { /** * Take a nice little nap :) * @param {number} ms The amount of milliseconds to sleep + * @return {Promise} */ export const nap = (ms = LND_INIT_DELAY) => { return new Promise(resolve => setTimeout(resolve, ms)); From 53d0094f816f7d07d462f467b1383f417325b3ee Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 2 Aug 2018 09:13:07 +0200 Subject: [PATCH 05/17] Fix comment indentation --- src/action/app-storage.js | 10 +++++----- src/action/channel.js | 28 ++++++++++++++-------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/action/app-storage.js b/src/action/app-storage.js index f1deabd9f..0d71bb43b 100644 --- a/src/action/app-storage.js +++ b/src/action/app-storage.js @@ -1,6 +1,6 @@ /** * @fileOverview repesents the local storage database on a user's device - * which can be used to persist user settings on disk. + * which can be used to persist user settings on disk. */ import * as log from './log'; @@ -13,8 +13,8 @@ class AppStorage { /** * Read the user settings from disk and set them accordingly in the - * application state. After the state has bee read to the global - * `store` instance `store.loaded` is set to true. + * application state. After the state has bee read to the global + * `store` instance `store.loaded` is set to true. * @return {Promise} */ async restore() { @@ -37,7 +37,7 @@ class AppStorage { /** * Persist the user settings to disk so that they may be read the - * next time the application is opened by the user. + * next time the application is opened by the user. * @return {Promise} */ async save() { @@ -52,7 +52,7 @@ class AppStorage { /** * Delete all of the data in local storage completely. Should be used - * carefully e.g. when the user wants to wipe the data on disk. + * carefully e.g. when the user wants to wipe the data on disk. * @return {Promise} */ async clear() { diff --git a/src/action/channel.js b/src/action/channel.js index b63ab84e6..72b4ca9a6 100644 --- a/src/action/channel.js +++ b/src/action/channel.js @@ -1,6 +1,6 @@ /** * @fileOverview actions to set channel state within the app and to - * call the corresponding GRPC apis for channel management. + * call the corresponding GRPC apis for channel management. */ import { toSatoshis, parseSat } from '../helper'; @@ -20,7 +20,7 @@ class ChannelAction { /** * Initiate the create channel view by resetting input values - * and then navigating to the view. + * and then navigating to the view. * @return {undefined} */ initCreate() { @@ -31,7 +31,7 @@ class ChannelAction { /** * Set the amount input for the create channel view. This amount - * is either in btc or fiat depending on user settings. + * is either in btc or fiat depending on user settings. * @param {string} options.amount The string formatted number */ setAmount({ amount }) { @@ -40,7 +40,7 @@ class ChannelAction { /** * Set the channel public key and hostname in a single variable - * which can be parsed before calling the create channel grpc api. + * which can be parsed before calling the create channel grpc api. * @param {string} options.pubkeyAtHost The combined public key and host */ setPubkeyAtHost({ pubkeyAtHost }) { @@ -53,7 +53,7 @@ class ChannelAction { /** * Initiate the channel list view by navigating to the view and updating - * the app's channel state by calling all necessary grpc apis. + * the app's channel state by calling all necessary grpc apis. * @return {undefined} */ init() { @@ -63,7 +63,7 @@ class ChannelAction { /** * Select a channel item from the channel list view and then navigate - * to the detail view to list channel parameters. + * to the detail view to list channel parameters. * @param {Object} options.item The selected channel object * @return {undefined} */ @@ -75,7 +75,7 @@ class ChannelAction { /** * Update the peers, channels, and pending channels in the app state - * by querying all required grpc apis. + * by querying all required grpc apis. * @return {Promise} */ async update() { @@ -92,7 +92,7 @@ class ChannelAction { /** * List the open channels by calling the respective grpc api and updating - * the channels array in the global store. + * the channels array in the global store. * @return {Promise} */ async getChannels() { @@ -116,7 +116,7 @@ class ChannelAction { /** * List the pending channels by calling the respective grpc api and updating - * the pendingChannels array in the global store. + * the pendingChannels array in the global store. * @return {Promise} */ async getPendingChannels() { @@ -164,7 +164,7 @@ class ChannelAction { /** * List the peers by calling the respective grpc api and updating - * the peers array in the global store. + * the peers array in the global store. * @return {Promise} */ async getPeers() { @@ -187,10 +187,10 @@ class ChannelAction { } /** - * Attempty to connect to a peer and open a channel in a single call. - * If a connection already exists, just a channel will be opened. - * This action can be called from a view event handler as does all - * the necessary error handling and notification display. + * Attempt to connect to a peer and open a channel in a single call. + * If a connection already exists, just a channel will be opened. + * This action can be called from a view event handler as does all + * the necessary error handling and notification display. * @return {Promise} */ async connectAndOpen() { From 5cd38119d3969a10dd6630664c87498b50b1a83e Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 2 Aug 2018 12:04:12 +0200 Subject: [PATCH 06/17] Add missing channel action docs --- src/action/channel.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/action/channel.js b/src/action/channel.js index 72b4ca9a6..330e074f5 100644 --- a/src/action/channel.js +++ b/src/action/channel.js @@ -211,6 +211,13 @@ class ChannelAction { } } + /** + * Connect to peer and fail gracefully by catching exceptions and + * logging their output. + * @param {string} options.host The hostname of the peer + * @param {string} options.pubkey The public key of the peer + * @return {Promise} + */ async connectToPeer({ host, pubkey }) { try { await this._grpc.sendCommand('connectPeer', { @@ -221,6 +228,13 @@ class ChannelAction { } } + /** + * Open a channel to a peer without advertising it and update channel + * state on data event from the streaming grpc api. + * @param {string} options.pubkey The public key of the peer + * @param {number} options.amount The amount in satoshis to fund the channel + * @return {Promise} + */ async openChannel({ pubkey, amount }) { const stream = this._grpc.sendStreamCommand('openChannel', { node_pubkey: new Buffer(pubkey, 'hex'), @@ -235,6 +249,12 @@ class ChannelAction { }); } + /** + * Close the selected channel by attempting a cooperative close. + * This action can be called from a view event handler as does all + * the necessary error handling and notification display. + * @return {Promise} + */ async closeSelectedChannel() { try { const { selectedChannel } = this._store; @@ -245,6 +265,14 @@ class ChannelAction { } } + /** + * Close a channel using the grpc streaming api and update the state + * on data events. Once the channel close is complete the channel will + * be removed from the channels array in the store. + * @param {string} options.channelPoint The channel identifier + * @param {Boolean} options.force Force or cooperative close + * @return {Promise} + */ async closeChannel({ channelPoint, force = false }) { const stream = this._grpc.sendStreamCommand('closeChannel', { channel_point: this._parseChannelPoint(channelPoint), From 5cf63a8e159732ea2ef7efd0eb147439f921bee2 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 2 Aug 2018 15:04:47 +0200 Subject: [PATCH 07/17] Add grpc action docs --- src/action/grpc.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/action/grpc.js b/src/action/grpc.js index 35d2f8c60..36aba87c9 100644 --- a/src/action/grpc.js +++ b/src/action/grpc.js @@ -1,3 +1,9 @@ +/** + * @fileOverview a low level action to proxy GRPC api calls to and from lnd + * over electron's IPC renderer api. This module should not be invokes directly + * from the UI but rather used within other higher level actions. + */ + import { Duplex } from 'stream'; import * as log from './log'; @@ -11,17 +17,34 @@ class GrpcAction { // WalletUnlocker grpc client // + /** + * The first GRPC api that is called to initialize the wallet unlocker. + * Once `unlockerReady` is set to true on the store GRPC calls can be + * made to the client. + * @return {Promise} + */ async initUnlocker() { await this._sendIpc('unlockInit', 'unlockReady'); log.info('GRPC unlockerReady'); this._store.unlockerReady = true; } + /** + * This GRPC api is called after the wallet is unlocked to close the grpc + * client to lnd before the main lnd client is re-opened + * @return {Promise} + */ async closeUnlocker() { await this._sendIpc('unlockClose', 'unlockClosed'); log.info('GRPC unlockerClosed'); } + /** + * Wrapper function to execute calls to the wallet unlocker. + * @param {string} method The unlocker GRPC api to call + * @param {Object} body The payload passed to the api + * @return {Promise} + */ async sendUnlockerCommand(method, body) { return this._sendIpc('unlockRequest', 'unlockResponse', method, body); } @@ -30,21 +53,44 @@ class GrpcAction { // Lightning (lnd) grpc client // + /** + * This is called to initialize the main GRPC client to lnd. Once `lndReady` + * is set to true on the store GRPC calls can be made to the client. + * @return {Promise} + */ async initLnd() { await this._sendIpc('lndInit', 'lndReady'); log.info('GRPC lndReady'); this._store.lndReady = true; } + /** + * Closes the main GRPC client to lnd. This should only be called upon exiting + * the application as api calls need to be throughout the lifetime of the app. + * @return {Promise} + */ async closeLnd() { await this._sendIpc('lndClose', 'lndClosed'); log.info('GRPC lndClosed'); } + /** + * Wrapper function to execute calls to the lnd grpc client. + * @param {string} method The lnd GRPC api to call + * @param {Object} body The payload passed to the api + * @return {Promise} + */ sendCommand(method, body) { return this._sendIpc('lndRequest', 'lndResponse', method, body); } + /** + * Wrapper function to execute GRPC streaming api calls to lnd. This function + * proxies data to and from lnd using a duplex stream which is returned. + * @param {string} method The lnd GRPC api to call + * @param {Object} body The payload passed to the api + * @return {Duplex} The duplex stream object instance + */ sendStreamCommand(method, body) { const self = this; const stream = new Duplex({ From 1b9b11a32fcdac797b3352d7b159e79b55d2dd8d Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 2 Aug 2018 15:32:51 +0200 Subject: [PATCH 08/17] Add docs to main action module --- src/action/index.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/action/index.js b/src/action/index.js index 453e502a9..17a97fea0 100644 --- a/src/action/index.js +++ b/src/action/index.js @@ -1,3 +1,9 @@ +/** + * @fileOverview this is the main action module where all actions are initilized + * by injecting dependencies and then triggering startup actions when certain + * flags on the global store are set to true. + */ + import { observe } from 'mobx'; import { AsyncStorage, Clipboard } from 'react-native'; import { nap } from '../helper'; @@ -21,7 +27,7 @@ const ipcRenderer = window.ipcRenderer; // exposed to sandbox via preload.js // Inject dependencies // -store.init(); +store.init(); // initialize computed values export const db = new AppStorage(store, AsyncStorage); export const log = new LogAction(store, ipcRenderer); @@ -43,28 +49,43 @@ export const invoice = new InvoiceAction( export const payment = new PaymentAction(store, grpc, transaction, nav, notify); export const setting = new SettingAction(store, wallet, db); -payment.listenForUrl(ipcRenderer); +payment.listenForUrl(ipcRenderer); // enable incoming url handler // // Init actions // -db.restore(); +db.restore(); // read user settings from disk +/** + * Triggered after user settings are restored from disk. + */ observe(store, 'loaded', async () => { await grpc.initUnlocker(); }); +/** + * Triggered after the wallet unlocker grpc client is initialized. + */ observe(store, 'unlockerReady', async () => { await wallet.init(); }); +/** + * Triggered after the user's password has unlocked the wallet. + */ observe(store, 'walletUnlocked', async () => { await nap(); await grpc.closeUnlocker(); await grpc.initLnd(); }); +/** + * Triggered once the main lnd grpc client is inialized. This is when + * the user can really begin to interact with the application and calls + * to and from lnd can be done. The display the current state of the + * lnd node all balances, channels and transactions are fetched. + */ observe(store, 'lndReady', () => { info.getInfo(); wallet.update(); From 0cd8a601b9bf83796ab91c5d2f055230e91666dc Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 2 Aug 2018 15:45:44 +0200 Subject: [PATCH 09/17] Add info action docs --- src/action/info.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/action/info.js b/src/action/info.js index d42c58e5b..6dc8c69d3 100644 --- a/src/action/info.js +++ b/src/action/info.js @@ -1,3 +1,9 @@ +/** + * @fileOverview the info actions are used to fetch general details about the + * state of the lnd such as the public key as well as synchronization state and + * the current block height. + */ + import { RETRY_DELAY } from '../config'; import { observe } from 'mobx'; import * as log from './log'; @@ -10,6 +16,12 @@ class InfoAction { this._notification = notification; } + /** + * Fetches the current details of the lnd node and sets the corresponding + * store parameters. This api is polled at the beginning of app initialization + * until lnd has finished syncing the chain to the connected bitcoin full node. + * @return {Promise} + */ async getInfo() { try { const response = await this._grpc.sendCommand('getInfo'); @@ -31,6 +43,13 @@ class InfoAction { } } + /** + * A navigation helper called during the app onboarding process. The loader + * screen indicating the syncing progress in displayed until syncing has + * completed `syncedToChain` is set to true. After that the user is taken + * to the home screen. + * @return {undefined} + */ initLoaderSyncing() { if (this._store.syncedToChain) { this._nav.goHome(); @@ -40,6 +59,12 @@ class InfoAction { } } + /** + * An internal helper function to approximate the current progress while + * syncing Neutrino to the full node. + * @param {Object} response The getInfo's grpc api response + * @return {number} The percrentage a number between 0 and 1 + */ calcPercentSynced(response) { const bestHeaderTimestamp = response.best_header_timestamp; const currTimestamp = new Date().getTime() / 1000; From a0a633861a06816ac0225528b8a01566e73a1ac9 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 2 Aug 2018 16:17:14 +0200 Subject: [PATCH 10/17] Add invoice action docs --- src/action/invoice.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/action/invoice.js b/src/action/invoice.js index a63934e64..3a4cf0396 100644 --- a/src/action/invoice.js +++ b/src/action/invoice.js @@ -1,3 +1,8 @@ +/** + * @fileOverview actions required to generate a lightning payment request + * a.k.a invoice that can be sent to another user. + */ + import { PREFIX_URI } from '../config'; import { toSatoshis } from '../helper'; @@ -11,6 +16,11 @@ class InvoiceAction { this._clipboard = clipboard; } + /** + * Initialize the invoice view by resetting input values + * and then navigating to the view. + * @return {undefined} + */ init() { this._store.invoice.amount = ''; this._store.invoice.note = ''; @@ -19,14 +29,34 @@ class InvoiceAction { this._nav.goInvoice(); } + /** + * Set the amount input for the invoice view. This amount + * is either in btc or fiat depending on user settings. + * @param {string} options.amount The string formatted number + */ setAmount({ amount }) { this._store.invoice.amount = amount; } + /** + * Set the node input for the invoice view. This is used as + * the description in the invoice later viewed by the payer. + * @param {string} options.note The invoice description + */ setNote({ note }) { this._store.invoice.note = note; } + /** + * Read the input values amount and note and generates an encoded + * payment request via the gprc api. The invoice uri is also set + * which can be rendered in a QR code for scanning. After the values + * are set on the store the user is navigated to the invoice QR view + * which displays the QR for consumption by the payer. + * This action can be called from a view event handler as does all + * the necessary error handling and notification display. + * @return {Promise} + */ async generateUri() { try { const { invoice, settings } = this._store; @@ -43,6 +73,13 @@ class InvoiceAction { await this._transaction.update(); } + /** + * A simple wrapper around the react native clipboard api. This can + * be called when a string like a payment request or address should be + * copied and pasted from the application UI. + * @param {string} options.text The payload to be copied to the clipboard + * @return {undefined} + */ toClipboard({ text }) { this._clipboard.setString(text); this._store.displayCopied = true; From 696b8a0dbaa67f750448d740b785edf10fedb14f Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 2 Aug 2018 16:35:25 +0200 Subject: [PATCH 11/17] Add log action docs --- src/action/log.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/action/log.js b/src/action/log.js index 4b458ff4e..bdb0e32c2 100644 --- a/src/action/log.js +++ b/src/action/log.js @@ -1,13 +1,36 @@ +/** + * @fileOverview actions for logging to the cli. This module can be regarded as a + * global singleton and can be imported directly as an ES6 module at the top of + * other actions for easier use as it stores instances of dependencies in closure + * variables. + */ + import { MAX_LOG_LENGTH } from '../config'; let _store; let _ipcRenderer; +/** + * Log an info event e.g. when something relevant but non-critical happens. + * The data is also sent to the electron main process via IPC to be logged + * to standard output. + * @param {...string|Object} args An info message or object to be logged + * @return {undefined} + */ export function info(...args) { console.log(...args); _ipcRenderer && _ipcRenderer.send('log', args); } +/** + * Log an error event e.g. when something does not work as planned. Apart + * from logging the error on the console this also appends the error to the + * logs which are displayed to the user in the Logs/CLI view. + * The data is also sent to the electron main process via IPC to be logged + * to standard output. + * @param {...string|Object} args An error message of Error object + * @return {undefined} + */ export function error(...args) { console.error(...args); pushLogs(''); // newline From 730a4922b4c24b0fdb5f01dd20727bd226d00968 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 2 Aug 2018 17:04:12 +0200 Subject: [PATCH 12/17] Add nav docs --- src/action/nav.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/action/nav.js b/src/action/nav.js index 45dfee642..3f9a53d2f 100644 --- a/src/action/nav.js +++ b/src/action/nav.js @@ -1,3 +1,9 @@ +/** + * @fileOverview actions for wrap navigation between views behing a platform + * independant api. These action should be pretty dumb and only change the + * route to be rendered in the user interface. + */ + class NavAction { constructor(store) { this._store = store; From c5630eb24ab77f9ebac4d0821a0f8d01957d7bf3 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 2 Aug 2018 17:04:24 +0200 Subject: [PATCH 13/17] Add notification action docs --- src/action/notification.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/action/notification.js b/src/action/notification.js index 0b128e16a..0ca710aa3 100644 --- a/src/action/notification.js +++ b/src/action/notification.js @@ -1,3 +1,10 @@ +/** + * @fileOverview actions to display notifications to the user in case something + * relevant happens or an error occurs. Notifications are display in the notification + * bar at the top of the screen for a brief time and can be listed in the notification + * view later. + */ + import * as log from './log'; import { NOTIFICATION_DELAY } from '../config'; @@ -7,6 +14,21 @@ class NotificationAction { this._nav = nav; } + /** + * The main api used to display notifications thorughout the application. Several + * types of notifications can be displayed including `info` `error` or `success`. + * If the wait flag is set the notification bar will display a spinner e.g. when + * something is loading. If an error is provided that will be logged to the cli. + * Also an action handler can be passed which will render a button e.g. for error + * resolution. A notification is displayed for a few seconds. + * @param {string} options.type Either `info` `error` or `success` + * @param {string} options.msg The notification message + * @param {boolean} options.wait If a spinner should be displayed + * @param {Error} options.err The error object to be logged + * @param {Function} options.handler Called when the button is pressed + * @param {string} options.handlerLbl The action handler button text + * @return {undefined} + */ display({ type, msg, wait, err, handler, handlerLbl }) { if (err) log.error(msg, err); this._store.notifications.push({ @@ -22,6 +44,11 @@ class NotificationAction { this.tdisplay = setTimeout(() => this.close(), NOTIFICATION_DELAY); } + /** + * Called after the notification bar display time has run out to stop rendering + * the notification in the notification bar. + * @return {undefined} + */ close() { this._store.notifications.forEach(n => { n.display = false; From bd005c0e48c48d88e1d4ab4d02091e568ed06fd9 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 3 Aug 2018 14:44:07 +0200 Subject: [PATCH 14/17] Add payment action docs --- src/action/payment.js | 62 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/action/payment.js b/src/action/payment.js index 451430b7e..4ecfa18ad 100644 --- a/src/action/payment.js +++ b/src/action/payment.js @@ -1,3 +1,8 @@ +/** + * @fileOverview actions to set payment state within the app and to + * call the corresponding GRPC apis for payment management. + */ + import { PREFIX_URI } from '../config'; import { toSatoshis, @@ -18,6 +23,12 @@ class PaymentAction { this._notification = notification; } + /** + * Set the listener for IPC from the main electron process to + * handle incoming URIs containing lightning invoices. + * @param {Object} ipcRenderer Electron's IPC api for the rendering process + * @return {undefined} + */ listenForUrl(ipcRenderer) { ipcRenderer.on('open-url', async (event, url) => { log.info('open-url', url); @@ -33,6 +44,11 @@ class PaymentAction { }); } + /** + * Initialize the payment view by resetting input values + * and then navigating to the view. + * @return {undefined} + */ init() { this._store.payment.address = ''; this._store.payment.amount = ''; @@ -41,14 +57,32 @@ class PaymentAction { this._nav.goPay(); } + /** + * Set the address input for the payment view. This can either be + * an on-chain bitcoin addres or an encoded lightning invoice. + * @param {string} options.address The payment address + */ setAddress({ address }) { this._store.payment.address = address; } + /** + * Set the amount input for the payment view. This amount + * is either in btc or fiat depending on user settings. + * @param {string} options.amount The string formatted number + */ setAmount({ amount }) { this._store.payment.amount = amount; } + /** + * Check if the address input provided by the user is either an on-chain + * bitcoin address or a lightning invoice. Depending on which type it is + * the app will navigate to the corresponding payment view. + * This action can be called from a view event handler as does all + * the necessary error handling and notification display. + * @return {Promise} + */ async checkType() { if (!this._store.payment.address) { return this._notification.display({ msg: 'Enter an invoice or address' }); @@ -62,6 +96,13 @@ class PaymentAction { } } + /** + * Attempt to decode a lightning invoice using the lnd grpc api. If it is + * an invoice the amount and note store values will be set and the lightning + * transaction fee will also be estimated. + * @param {string} options.invoice The input to be validated + * @return {Promise} If the input is a valid invoice + */ async decodeInvoice({ invoice }) { try { const { payment, settings } = this._store; @@ -81,6 +122,13 @@ class PaymentAction { } } + /** + * Estimate the lightning transaction fee using the queryRoutes grpc api + * after which the fee is set in the store. + * @param {string} options.destination The lnd node that is to be payed + * @param {number} options.satAmt The amount to be payed in satoshis + * @return {Promise} + */ async estimateLightningFee({ destination, satAmt }) { try { const { payment, settings } = this._store; @@ -95,6 +143,13 @@ class PaymentAction { } } + /** + * Send the specified amount as an on-chain transaction to the provided + * bitcoin address and display a payment confirmation screen. + * This action can be called from a view event handler as does all + * the necessary error handling and notification display. + * @return {Promise} + */ async payBitcoin() { try { const { payment, settings } = this._store; @@ -109,6 +164,13 @@ class PaymentAction { await this._transaction.update(); } + /** + * Send the amount specified in the invoice as a lightning transaction and + * display the wait screen while the payment confirms. + * This action can be called from a view event handler as does all + * the necessary error handling and notification display. + * @return {Promise} + */ async payLightning() { try { this._nav.goWait(); From c40a02ba7af9430314f988750a80007a63767627 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 3 Aug 2018 15:53:32 +0200 Subject: [PATCH 15/17] Add settings action docs --- src/action/setting.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/action/setting.js b/src/action/setting.js index c4fa1fb9c..9e79638df 100644 --- a/src/action/setting.js +++ b/src/action/setting.js @@ -1,3 +1,8 @@ +/** + * @fileOverview actions to handle settings state and save persist + * them to disk when they are updated by the user. + */ + import { UNITS, FIATS } from '../config'; class SettingAction { @@ -7,6 +12,11 @@ class SettingAction { this._db = db; } + /** + * Set the bitcoin unit that is to be displayed in the UI and + * perist the updated settings to disk. + * @param {string} options.unit The bitcoin unit e.g. `btc` + */ setBitcoinUnit({ unit }) { if (!UNITS[unit]) { throw new Error(`Invalid bitcoin unit: ${unit}`); @@ -15,6 +25,11 @@ class SettingAction { this._db.save(); } + /** + * Set the fiat currency that is to be displayed in the UI and + * perist the updated settings to disk. + * @param {string} options.fiat The fiat currency e.g. `usd` + */ setFiatCurrency({ fiat }) { if (!FIATS[fiat]) { throw new Error(`Invalid fiat currency: ${fiat}`); From fe0f0b60661bebb336e83ebecaf4b489abb683e3 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 3 Aug 2018 16:39:39 +0200 Subject: [PATCH 16/17] Add transaction action docs --- src/action/transaction.js | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/action/transaction.js b/src/action/transaction.js index bfa147537..213ad6e21 100644 --- a/src/action/transaction.js +++ b/src/action/transaction.js @@ -1,3 +1,8 @@ +/** + * @fileOverview actions to set transactions state within the app and to + * call the corresponding GRPC apis for listing transactions. + */ + import * as log from './log'; import { parseDate, parseSat, toHex, toHash } from '../helper'; @@ -9,17 +14,33 @@ class TransactionAction { this._nav = nav; } + /** + * Initiate the transaction list view by navigating to the view and updating + * the app's transaction state by calling all necessary grpc apis. + * @return {undefined} + */ init() { this._nav.goTransactions(); this.update(); } + /** + * Select a transaction item from the transaction list view and then navigate + * to the detail view to list transaction parameters. + * @param {Object} options.item The selected transaction object + * @return {undefined} + */ select({ item }) { this._store.selectedTransaction = item; this._nav.goTransactionDetail(); this.update(); } + /** + * Update the on-chain transactions, invoice, lighting payments, and wallet + * balances in the app state by querying all required grpc apis. + * @return {Promise} + */ async update() { await Promise.all([ this.getTransactions(), @@ -30,6 +51,11 @@ class TransactionAction { ]); } + /** + * List the on-chain transactions by calling the respective grpc api and updating + * the transactions array in the global store. + * @return {Promise} + */ async getTransactions() { try { const { transactions } = await this._grpc.sendCommand('getTransactions'); @@ -48,6 +74,11 @@ class TransactionAction { } } + /** + * List the lightning invoices by calling the respective grpc api and updating + * the invoices array in the global store. + * @return {Promise} + */ async getInvoices() { try { const { invoices } = await this._grpc.sendCommand('listInvoices'); @@ -65,6 +96,11 @@ class TransactionAction { } } + /** + * List the lightning payments by calling the respective grpc api and updating + * the payments array in the global store. + * @return {Promise} + */ async getPayments() { try { const { payments } = await this._grpc.sendCommand('listPayments'); @@ -82,6 +118,10 @@ class TransactionAction { } } + /** + * Subscribe to incoming on-chain transactions using the grpc streaming api. + * @return {Promise} + */ async subscribeTransactions() { const stream = this._grpc.sendStreamCommand('subscribeTransactions'); await new Promise((resolve, reject) => { @@ -92,6 +132,10 @@ class TransactionAction { }); } + /** + * Subscribe to incoming invoice payments using the grpc streaming api. + * @return {Promise} + */ async subscribeInvoices() { const stream = this._grpc.sendStreamCommand('subscribeInvoices'); await new Promise((resolve, reject) => { From 64e804bbd78453f7d86bb48b03023dec2cbb1453 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Fri, 3 Aug 2018 20:23:38 +0200 Subject: [PATCH 17/17] Add wallet action docs --- src/action/wallet.js | 105 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/src/action/wallet.js b/src/action/wallet.js index 0a03f763c..11f108a4c 100644 --- a/src/action/wallet.js +++ b/src/action/wallet.js @@ -1,3 +1,8 @@ +/** + * @fileOverview actions to set wallet state within the app and to + * call the corresponding GRPC apis for updating wallet balances. + */ + import { observe } from 'mobx'; import { toBuffer, parseSat, checkHttpStatus, nap } from '../helper'; import { MIN_PASSWORD_LENGTH, NOTIFICATION_DELAY } from '../config'; @@ -16,11 +21,22 @@ class WalletAction { // Verify Seed actions // + /** + * Initialize the seed verify view by resetting input values + * and then navigating to the view. + * @return {undefined} + */ initSeedVerify() { this._store.wallet.seedVerify = ['', '', '']; this._nav.goSeedVerify(); } + /** + * Set the verify seed input by validation the seed word and + * seed index. + * @param {string} options.word The seed word + * @param {number} options.index The seed index + */ setSeedVerify({ word, index }) { this._store.wallet.seedVerify[index] = word; } @@ -29,21 +45,39 @@ class WalletAction { // Wallet Password actions // + /** + * Initialize the set password view by resetting input values + * and then navigating to the view. + * @return {undefined} + */ initSetPassword() { this._store.wallet.password = ''; this._store.wallet.passwordVerify = ''; this._nav.goSetPassword(); } + /** + * Initialize the password view by resetting input values + * and then navigating to the view. + * @return {undefined} + */ initPassword() { this._store.wallet.password = ''; this._nav.goPassword(); } + /** + * Set the password input for the password view. + * @param {string} options.password The wallet password + */ setPassword({ password }) { this._store.wallet.password = password; } + /** + * Set the verify password input for the password view. + * @param {string} options.password The wallet password a second time + */ setPasswordVerify({ password }) { this._store.wallet.passwordVerify = password; } @@ -52,6 +86,12 @@ class WalletAction { // Wallet actions // + /** + * Initialize the wallet by trying to generate a new seed. If seed + * generation in lnd fails, the app assumes a wallet already exists + * and wallet unlock via password input will be initiated. + * @return {Promise} + */ async init() { try { await this.generateSeed(); @@ -63,6 +103,11 @@ class WalletAction { } } + /** + * Update the wallet on-chain balance, channel balance, wallet address + * and fiat/btc exchange rate. + * @return {Promise} + */ async update() { await Promise.all([ this.getBalance(), @@ -72,11 +117,22 @@ class WalletAction { ]); } + /** + * Generate a new wallet seed. This needs to be done the first time the + * app is started. + * @return {Promise} + */ async generateSeed() { const response = await this._grpc.sendUnlockerCommand('GenSeed'); this._store.seedMnemonic = response.cipher_seed_mnemonic; } + /** + * Verify that the user has written down the generated seed correctly by + * checking three random seed words. If the match continue to setting the + * wallet password. + * @return {undefined} + */ async checkSeed() { const { wallet: { seedVerify }, @@ -93,6 +149,12 @@ class WalletAction { this.initSetPassword(); } + /** + * Check the wallet password that was chosen by the user has the correct + * length and that it was also entered correctly twice to make sure that + * there was no typo. + * @return {Promise} + */ async checkNewPassword() { const { password, passwordVerify } = this._store.wallet; if (!password || password.length < MIN_PASSWORD_LENGTH) { @@ -109,6 +171,14 @@ class WalletAction { }); } + /** + * Initiate the lnd wallet using the generated seed and password. If this + * is success set `walletUnlocked` to true and navigate to the seed success + * screen. + * @param {string} options.walletPassword The user chosen password + * @param {Array} options.seedMnemonic The seed words to generate the wallet + * @return {Promise} + */ async initWallet({ walletPassword, seedMnemonic }) { try { await this._grpc.sendUnlockerCommand('InitWallet', { @@ -122,11 +192,20 @@ class WalletAction { } } + /** + * Check the password input by the user by attempting to unlock the wallet. + * @return {Promise} + */ async checkPassword() { const { password } = this._store.wallet; await this.unlockWallet({ walletPassword: password }); } + /** + * Unlock the wallet by calling the grpc api with the user chosen password. + * @param {string} options.walletPassword The password used to encrypt the wallet + * @return {Promise} + */ async unlockWallet({ walletPassword }) { try { await this._grpc.sendUnlockerCommand('UnlockWallet', { @@ -140,11 +219,21 @@ class WalletAction { } } + /** + * Toggle if fiat or btc should be use as the primary amount display in the + * application. Aftwards save the user's current preference on disk. + * @return {undefined} + */ toggleDisplayFiat() { this._store.settings.displayFiat = !this._store.settings.displayFiat; this._db.save(); } + /** + * Fetch the on-chain wallet balances using the lnd grpc api and set the + * corresponding values on the global store. + * @return {Promise} + */ async getBalance() { try { const r = await this._grpc.sendCommand('WalletBalance'); @@ -156,6 +245,11 @@ class WalletAction { } } + /** + * Fetch the lightning channel balances using the lnd grpc api and set the + * corresponding values on the global store. + * @return {Promise} + */ async getChannelBalance() { try { const r = await this._grpc.sendCommand('ChannelBalance'); @@ -166,6 +260,11 @@ class WalletAction { } } + /** + * Fetch a new on-chain bitcoin address which can be used to fund the wallet + * or receive an on-chain transaction from another user. + * @return {Promise} + */ async getNewAddress() { // - `p2wkh`: Pay to witness key hash (`WITNESS_PUBKEY_HASH` = 0) // - `np2wkh`: Pay to nested witness key hash (`NESTED_PUBKEY_HASH` = 1) @@ -180,6 +279,12 @@ class WalletAction { } } + /** + * Fetch a current btc/fiat exchange rate based on the currently selected + * fiat currency and persist the value on disk for the next time the app + * starts up. + * @return {Promise} + */ async getExchangeRate() { try { const fiat = this._store.settings.fiat;