Skip to content
This repository was archived by the owner on Feb 23, 2021. It is now read-only.
21 changes: 21 additions & 0 deletions src/action/app-storage.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<undefined>}
*/
async restore() {
try {
const stateString = await this._AsyncStorage.getItem('settings');
Expand All @@ -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<undefined>}
*/
async save() {
try {
const state = JSON.stringify(this._store.settings);
Expand All @@ -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<undefined>}
*/
async clear() {
try {
await this._AsyncStorage.clear();
Expand Down
90 changes: 90 additions & 0 deletions src/action/channel.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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;
}
Expand All @@ -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<undefined>}
*/
async update() {
await Promise.all([
this.getPeers(),
Expand All @@ -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<undefined>}
*/
async getChannels() {
try {
const { channels } = await this._grpc.sendCommand('listChannels');
Expand All @@ -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<undefined>}
*/
async getPendingChannels() {
try {
const response = await this._grpc.sendCommand('pendingChannels');
Expand Down Expand Up @@ -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<undefined>}
*/
async getPeers() {
try {
const { peers } = await this._grpc.sendCommand('listPeers');
Expand All @@ -131,6 +186,13 @@ class ChannelAction {
}
}

/**
* 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<undefined>}
*/
async connectAndOpen() {
try {
const { channel, settings } = this._store;
Expand All @@ -149,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<undefined>}
*/
async connectToPeer({ host, pubkey }) {
try {
await this._grpc.sendCommand('connectPeer', {
Expand All @@ -159,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<undefined>}
*/
async openChannel({ pubkey, amount }) {
const stream = this._grpc.sendStreamCommand('openChannel', {
node_pubkey: new Buffer(pubkey, 'hex'),
Expand All @@ -173,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<undefined>}
*/
async closeSelectedChannel() {
try {
const { selectedChannel } = this._store;
Expand All @@ -183,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<undefined>}
*/
async closeChannel({ channelPoint, force = false }) {
const stream = this._grpc.sendStreamCommand('closeChannel', {
channel_point: this._parseChannelPoint(channelPoint),
Expand Down
46 changes: 46 additions & 0 deletions src/action/grpc.js
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<undefined>}
*/
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<undefined>}
*/
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<Object>}
*/
async sendUnlockerCommand(method, body) {
return this._sendIpc('unlockRequest', 'unlockResponse', method, body);
}
Expand All @@ -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<undefined>}
*/
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<undefined>}
*/
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<Object>}
*/
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({
Expand Down
27 changes: 24 additions & 3 deletions src/action/index.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand All @@ -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();
Expand Down
Loading