Skip to content
This repository was archived by the owner on Feb 23, 2021. It is now read-only.
Merged
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
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"electron-log": "^2.2.14",
"electron-updater": "^2.21.10",
"grpc": "^1.10.0",
"locale-currency": "0.0.2",
"mobx": "^3.6.2",
"mobx-react": "^4.4.3",
"qr-image": "^3.2.0",
Expand Down
3 changes: 3 additions & 0 deletions public/electron.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ log.transports.console.level = 'info';
log.transports.file.level = 'info';
ipcMain.on('log', (event, arg) => log.info(...arg));
ipcMain.on('log-error', (event, arg) => log.error(...arg));
ipcMain.on('locale-get', event =>
event.sender.send('locale', { response: app.getLocale() })
);

let logQueue = [];
let logsReady = false;
Expand Down
4 changes: 3 additions & 1 deletion public/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
const _ipcRenderer = require('electron').ipcRenderer;

const filter = event => {
if (!/^(lnd)|(unlock)|(log)|(open-url)[a-zA-Z_-]{0,20}$/.test(event)) {
if (
!/^(lnd)|(unlock)|(log)|(locale)|(open-url)[a-zA-Z_-]{0,20}$/.test(event)
Copy link
Contributor

Choose a reason for hiding this comment

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

is this a security measure?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. See #75

) {
throw new Error(`Invalid IPC: ${event}`);
}
return event;
Expand Down
25 changes: 8 additions & 17 deletions src/action/grpc.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
/**
* @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
* over an IPC 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';

class GrpcAction {
constructor(store, ipcRenderer) {
constructor(store, ipc) {
this._store = store;
this._ipcRenderer = ipcRenderer;
this._ipc = ipc;
}

//
Expand Down Expand Up @@ -96,14 +96,14 @@ class GrpcAction {
const stream = new Duplex({
write(data) {
data = JSON.parse(data.toString('utf8'));
self._ipcRenderer.send('lndStreamWrite', { method, data });
self._ipc.send('lndStreamWrite', null, { method, data });
},
read() {},
});
this._ipcRenderer.on(`lndStreamEvent_${method}`, (e, arg) => {
this._ipc.listen(`lndStreamEvent_${method}`, (e, arg) => {
stream.emit(arg.event, arg.data || arg.err);
});
this._ipcRenderer.send('lndStreamRequest', { method, body });
this._ipc.send('lndStreamRequest', null, { method, body });
return stream;
}

Expand All @@ -112,17 +112,8 @@ class GrpcAction {
//

_sendIpc(event, listen, method, body) {
return new Promise((resolve, reject) => {
listen = method ? `${listen}_${method}` : listen;
this._ipcRenderer.once(listen, (e, arg) => {
if (arg.err) {
reject(arg.err);
} else {
resolve(arg.response);
}
});
this._ipcRenderer.send(event, { method, body });
});
listen = method ? `${listen}_${method}` : listen;
return this._ipc.send(event, listen, { method, body });
}
}

Expand Down
20 changes: 14 additions & 6 deletions src/action/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AsyncStorage, Clipboard } from 'react-native';
import { nap } from '../helper';
import store from '../store';
import AppStorage from './app-storage';
import IpcAction from './ipc';
import GrpcAction from './grpc';
import NavAction from './nav';
import WalletAction from './wallet';
Expand All @@ -21,18 +22,17 @@ import PaymentAction from './payment';
import InvoiceAction from './invoice';
import SettingAction from './setting';

const ipcRenderer = window.ipcRenderer; // exposed to sandbox via preload.js

//
// Inject dependencies
//

store.init(); // initialize computed values

export const ipc = new IpcAction(window.ipcRenderer);
export const db = new AppStorage(store, AsyncStorage);
export const log = new LogAction(store, ipcRenderer);
export const log = new LogAction(store, ipc);
export const nav = new NavAction(store);
export const grpc = new GrpcAction(store, ipcRenderer);
export const grpc = new GrpcAction(store, ipc);
export const notify = new NotificationAction(store, nav);
export const wallet = new WalletAction(store, grpc, db, nav, notify);
export const info = new InfoAction(store, grpc, nav, notify);
Expand All @@ -47,9 +47,9 @@ export const invoice = new InvoiceAction(
Clipboard
);
export const payment = new PaymentAction(store, grpc, transaction, nav, notify);
export const setting = new SettingAction(store, wallet, db);
export const setting = new SettingAction(store, wallet, db, ipc);

payment.listenForUrl(ipcRenderer); // enable incoming url handler
payment.listenForUrl(ipc); // enable incoming url handler

//
// Init actions
Expand All @@ -71,6 +71,14 @@ observe(store, 'unlockerReady', async () => {
await wallet.init();
});

/**
* Triggered the first time the app was started e.g. to set the
* local fiat currency only once.
*/
observe(store, 'firstStart', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

seems like we could get rid of this logic by following the pattern of WalletAction's init function instead (maybe trying to restore from AppStorage instead).

or alternately, simplify WalletAction's init function by putting wallet initialization in this trigger..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought about that too...

seems like we could get rid of this logic by following the pattern of WalletAction's init function instead (maybe trying to restore from AppStorage instead).

Problem is we can't use the settings action inside wallet init, because setting already depends on wallet i.e. it's injected to fetch the exchange rate.

or alternately, simplify WalletAction's init function by putting wallet initialization in this trigger..

Not super happy about that either because we can't really unit test this routine. Trying to keep all testable logic in actions.

await setting.detectLocalCurrency();
});

/**
* Triggered after the user's password has unlocked the wallet.
*/
Expand Down
44 changes: 44 additions & 0 deletions src/action/ipc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* @fileOverview a low level action to wrap electron's IPC renderer api.
*/

class IpcAction {
Copy link
Contributor

Choose a reason for hiding this comment

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

this is nice 👌

learned a lot about electron reading this code!

constructor(ipcRenderer) {
this._ipcRenderer = ipcRenderer;
}

/**
* A wrapper around electron's ipcRenderer send api that can be
* reused wherever IPC to the main process is necessary.
* @param {string} event The event name the main process listens to
* @param {string} listen (optional) The response event name this process listens to
* @param {*} payload The data sent over IPC
* @return {Promise<Object>}
*/
send(event, listen, payload) {
return new Promise((resolve, reject) => {
this._ipcRenderer.send(event, payload);
if (!listen) return resolve();
this._ipcRenderer.once(listen, (e, arg) => {
if (arg.err) {
reject(arg.err);
} else {
resolve(arg.response);
}
});
});
}

/**
* A wrapper around electron's ipcRenderer listen api that can be
* reused wherever listening to IPC from the main process is necessary.
* @param {string} event The event name this process listens to
* @param {Function} callback The event handler for incoming data
* @return {undefined}
*/
listen(event, callback) {
this._ipcRenderer.on(event, callback);
}
}

export default IpcAction;
14 changes: 7 additions & 7 deletions src/action/log.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { MAX_LOG_LENGTH } from '../config';

let _store;
let _ipcRenderer;
let _ipc;

/**
* Log an info event e.g. when something relevant but non-critical happens.
Expand All @@ -19,7 +19,7 @@ let _ipcRenderer;
*/
export function info(...args) {
console.log(...args);
_ipcRenderer && _ipcRenderer.send('log', args);
_ipc && _ipc.send('log', null, args);
}

/**
Expand All @@ -39,7 +39,7 @@ export function error(...args) {
pushLogs(JSON.stringify(args[i], null, ' '));
}
pushLogs(''); // newline
_ipcRenderer && _ipcRenderer.send('log-error', args);
_ipc && _ipc.send('log-error', null, args);
}

function pushLogs(message) {
Expand All @@ -52,11 +52,11 @@ function pushLogs(message) {
}

class LogAction {
constructor(store, ipcRenderer) {
constructor(store, ipc) {
_store = store;
_ipcRenderer = ipcRenderer;
_ipcRenderer.on('logs', (event, message) => pushLogs(message));
_ipcRenderer.send('logs-ready', true);
_ipc = ipc;
_ipc.listen('logs', (event, message) => pushLogs(message));
_ipc.send('logs-ready', null, true);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/action/payment.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class PaymentAction {
* @param {Object} ipcRenderer Electron's IPC api for the rendering process
* @return {undefined}
*/
listenForUrl(ipcRenderer) {
ipcRenderer.on('open-url', async (event, url) => {
listenForUrl(ipc) {
ipc.listen('open-url', async (event, url) => {
log.info('open-url', url);
if (!isLnUri(url)) {
return;
Expand Down
20 changes: 19 additions & 1 deletion src/action/setting.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
*/

import { UNITS, FIATS } from '../config';
import localeCurrency from 'locale-currency';
import * as log from './log';

class SettingAction {
constructor(store, wallet, db) {
constructor(store, wallet, db, ipc) {
this._store = store;
this._wallet = wallet;
this._db = db;
this._ipc = ipc;
}

/**
Expand Down Expand Up @@ -38,6 +41,21 @@ class SettingAction {
this._wallet.getExchangeRate();
this._db.save();
}

/**
* Detect the user's local fiat currency based on their OS locale.
* If the currency is not supported use the default currency `usd`.
* @return {Promise<undefined>}
*/
async detectLocalCurrency() {
try {
let locale = await this._ipc.send('locale-get', 'locale');
const fiat = localeCurrency.getCurrency(locale).toLowerCase();
this.setFiatCurrency({ fiat });
} catch (err) {
log.error('Detecting local currency failed', err);
}
}
}

export default SettingAction;
1 change: 1 addition & 0 deletions src/action/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ class WalletAction {
async init() {
try {
await this.generateSeed();
this._store.firstStart = true;
this._nav.goLoader();
await nap(NOTIFICATION_DELAY);
this._nav.goSeed();
Expand Down
1 change: 1 addition & 0 deletions src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class Store {
extendObservable(this, {
loaded: false, // Is persistent data loaded
unlockerReady: false, // Is wallet unlocker running
firstStart: false, // Is the first time the app was started
walletUnlocked: false, // Is the wallet unlocked
lndReady: false, // Is lnd process running
syncedToChain: false, // Is lnd synced to blockchain
Expand Down
4 changes: 3 additions & 1 deletion stories/screen.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { action } from '@storybook/addon-actions';
import sinon from 'sinon';
import { Store } from '../src/store';
import NavAction from '../src/action/nav';
import IpcAction from '../src/action/ipc';
import GrpcAction from '../src/action/grpc';
import InfoAction from '../src/action/info';
import AppStorage from '../src/action/app-storage';
Expand Down Expand Up @@ -53,11 +54,12 @@ const store = new Store();
store.init();
const nav = sinon.createStubInstance(NavAction);
const db = sinon.createStubInstance(AppStorage);
const ipc = sinon.createStubInstance(IpcAction);
const grpc = sinon.createStubInstance(GrpcAction);
const info = sinon.createStubInstance(InfoAction);
const notify = sinon.createStubInstance(NotificationAction);
const wallet = new WalletAction(store, grpc, db, nav, notify);
const setting = new SettingAction(store, wallet, db);
const setting = new SettingAction(store, wallet, db, ipc);
sinon.stub(wallet, 'update');
sinon.stub(wallet, 'checkSeed');
sinon.stub(wallet, 'checkNewPassword');
Expand Down
16 changes: 12 additions & 4 deletions test/integration/action/action-integration.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { rmdir, poll, isPortOpen } from './test-util';
import { Store } from '../../../src/store';
import IpcAction from '../../../src/action/ipc';
import GrpcAction from '../../../src/action/grpc';
import AppStorage from '../../../src/action/app-storage';
import NavAction from '../../../src/action/nav';
Expand Down Expand Up @@ -40,8 +41,11 @@ const MACAROONS_ENABLED = false;
const NAP_TIME = process.env.NAP_TIME || 5000;
const walletPassword = 'bitconeeeeeect';

const wireUpIpc = (s1, s2) =>
(s1.send = (msg, ...args) => s2.emit(msg, { sender: s2 }, ...args));
const wireUpIpc = (s1, s2) => {
s1.send = (msg, ...args) => {
setTimeout(() => s2.emit(msg, { sender: s2 }, ...args), 1);
};
};

const ipcMainStub1 = new EventEmitter();
const ipcRendererStub1 = new EventEmitter();
Expand All @@ -66,6 +70,7 @@ describe('Action Integration Tests', function() {
let btcdProcess;
let nav1;
let notify1;
let ipc1;
let grpc1;
let info1;
let wallet1;
Expand All @@ -75,6 +80,7 @@ describe('Action Integration Tests', function() {
let invoice1;
let nav2;
let notify2;
let ipc2;
let grpc2;
let info2;
let wallet2;
Expand Down Expand Up @@ -141,7 +147,8 @@ describe('Action Integration Tests', function() {
db1 = sinon.createStubInstance(AppStorage);
nav1 = sinon.createStubInstance(NavAction);
notify1 = sinon.createStubInstance(NotificationAction, nav1);
grpc1 = new GrpcAction(store1, ipcRendererStub1);
ipc1 = new IpcAction(ipcRendererStub1);
grpc1 = new GrpcAction(store1, ipc1);
info1 = new InfoAction(store1, grpc1, nav1, notify1);
wallet1 = new WalletAction(store1, grpc1, db1, nav1, notify1);
channels1 = new ChannelAction(store1, grpc1, nav1, notify1);
Expand All @@ -152,7 +159,8 @@ describe('Action Integration Tests', function() {
db2 = sinon.createStubInstance(AppStorage);
nav2 = sinon.createStubInstance(NavAction);
notify2 = sinon.createStubInstance(NotificationAction, nav2);
grpc2 = new GrpcAction(store2, ipcRendererStub2);
ipc2 = new IpcAction(ipcRendererStub2);
grpc2 = new GrpcAction(store2, ipc2);
info2 = new InfoAction(store2, grpc2, nav2, notify2);
wallet2 = new WalletAction(store2, grpc2, db2, nav2, notify2);
channels2 = new ChannelAction(store2, grpc2, nav2, notify2);
Expand Down
Loading