Browse Source

Merge branch 'master' into fix/onboarding-layout-ui

renovate/lint-staged-8.x
Kristiyan Lukanov 7 years ago
committed by GitHub
parent
commit
e7d6bec662
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .babelrc
  2. 3
      .commitlintrc
  3. 38
      .eslintrc
  4. 4
      .gitignore
  5. 6
      .huskyrc
  6. 6
      .lintstagedrc
  7. 22
      .prettierignore
  8. 15
      .prettierrc
  9. 2
      .travis.yml
  10. 22
      ADVANCED.md
  11. 92
      CONTRIBUTING.md
  12. 5
      LICENSE
  13. 87
      README.md
  14. 5
      app/api/index.js
  15. 2
      app/app.global.scss
  16. 8
      app/app.html
  17. 16
      app/components/Activity/ActivityModal.js
  18. 35
      app/components/Activity/Countdown.js
  19. 38
      app/components/Activity/InvoiceModal.js
  20. 25
      app/components/Activity/PaymentModal.js
  21. 44
      app/components/Activity/TransactionModal.js
  22. 44
      app/components/Contacts/AddChannel.js
  23. 5
      app/components/Contacts/ChannelForm.js
  24. 13
      app/components/Contacts/ConnectManually.js
  25. 36
      app/components/Contacts/ContactModal.js
  26. 75
      app/components/Contacts/ContactsForm.js
  27. 152
      app/components/Contacts/Network.js
  28. 46
      app/components/Contacts/SubmitChannelForm.js
  29. 20
      app/components/Contacts/SubmitChannelForm.scss
  30. 19
      app/components/Contacts/SuggestedNodes.js
  31. 7
      app/components/CurrencyIcon/CurrencyIcon.js
  32. 5
      app/components/Form/Form.js
  33. 106
      app/components/Form/Pay.js
  34. 48
      app/components/Form/Request.js
  35. 4
      app/components/Onboarding/Alias.js
  36. 28
      app/components/Onboarding/ConnectionDetails.js
  37. 9
      app/components/Onboarding/InitWallet.js
  38. 23
      app/components/Onboarding/Login.js
  39. 8
      app/components/Onboarding/NewAezeedPassword.js
  40. 8
      app/components/Onboarding/NewWalletPassword.js
  41. 6
      app/components/Onboarding/NewWalletSeed.js
  42. 54
      app/components/Onboarding/Onboarding.js
  43. 11
      app/components/Onboarding/ReEnterSeed.js
  44. 13
      app/components/Onboarding/RecoverForm.js
  45. 22
      app/components/Onboarding/Signup.js
  46. 6
      app/components/Onboarding/Syncing.js
  47. 13
      app/components/Value/Value.js
  48. 40
      app/components/Wallet/ReceiveModal.js
  49. 50
      app/components/Wallet/Wallet.js
  50. 6
      app/containers/Root.js
  51. 12
      app/lnd/index.js
  52. 9
      app/lnd/lib/lightning.js
  53. 9
      app/lnd/lib/walletUnlocker.js
  54. 16
      app/lnd/methods/channelController.js
  55. 91
      app/lnd/methods/index.js
  56. 18
      app/lnd/methods/invoicesController.js
  57. 24
      app/lnd/methods/networkController.js
  58. 27
      app/lnd/methods/paymentsController.js
  59. 14
      app/lnd/methods/peersController.js
  60. 48
      app/lnd/methods/walletController.js
  61. 6
      app/lnd/subscribe/index.js
  62. 8
      app/lnd/subscribe/invoices.js
  63. 12
      app/lnd/subscribe/transactions.js
  64. 15
      app/lnd/walletUnlockerMethods/index.js
  65. 97
      app/main.dev.js
  66. 136
      app/menu.js
  67. 5
      app/package.json
  68. 65
      app/reducers/activity.js
  69. 2
      app/reducers/address.js
  70. 15
      app/reducers/balance.js
  71. 133
      app/reducers/channels.js
  72. 36
      app/reducers/contactsform.js
  73. 2
      app/reducers/error.js
  74. 43
      app/reducers/info.js
  75. 43
      app/reducers/invoice.js
  76. 10
      app/reducers/ipc.js
  77. 21
      app/reducers/lnd.js
  78. 62
      app/reducers/network.js
  79. 18
      app/reducers/onboarding.js
  80. 55
      app/reducers/payform.js
  81. 14
      app/reducers/payment.js
  82. 41
      app/reducers/peers.js
  83. 6
      app/reducers/requestform.js
  84. 42
      app/reducers/ticker.js
  85. 27
      app/reducers/transaction.js
  86. 2
      app/routes.js
  87. 70
      app/routes/activity/components/Activity.js
  88. 28
      app/routes/activity/components/components/Invoice/Invoice.js
  89. 26
      app/routes/activity/components/components/Payment/Payment.js
  90. 20
      app/routes/activity/components/components/Transaction/Transaction.js
  91. 38
      app/routes/activity/containers/ActivityContainer.js
  92. 25
      app/routes/app/components/App.js
  93. 53
      app/routes/app/containers/AppContainer.js
  94. 5
      app/store/configureStore.dev.js
  95. 17
      app/utils/blockExplorer.js
  96. 80
      app/utils/log.js
  97. 4
      app/utils/usd.js
  98. 6
      app/yarn.lock
  99. 4
      appveyor.yml
  100. 3
      internals/scripts/CheckNodeEnv.js

2
.babelrc

@ -3,7 +3,7 @@
["env", { ["env", {
"targets": { "targets": {
"node": 8, "node": 8,
"browsers": "electron 1.7" "browsers": "electron 2.0"
}, },
"useBuiltIns": true "useBuiltIns": true
}], }],

3
.commitlintrc

@ -0,0 +1,3 @@
{
"extends": ["@commitlint/config-conventional"]
}

38
.eslintrc

@ -6,12 +6,17 @@
}, },
"extends": [ "extends": [
"airbnb", "airbnb",
"eslint:recommended",
"plugin:react/recommended", "plugin:react/recommended",
"plugin:flowtype/recommended", "plugin:flowtype/recommended",
"plugin:import/errors", "plugin:import/errors",
"plugin:import/warnings", "plugin:import/warnings",
"plugin:jsx-a11y/strict", "plugin:jsx-a11y/strict",
"plugin:promise/recommended" "plugin:promise/recommended",
"prettier",
"prettier/flowtype",
"prettier/react",
"plugin:prettier/recommended"
], ],
"env": { "env": {
"browser": true, "browser": true,
@ -20,42 +25,45 @@
"rules": { "rules": {
"comma-dangle": ["error", "never"], "comma-dangle": ["error", "never"],
"semi": ["error", "never"], "semi": ["error", "never"],
"indent": 2, "prettier/prettier": "error",
"jsx-quotes": ["error", "prefer-single"],
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"react/no-did-mount-set-state": 0, "react/no-did-mount-set-state": 0,
"jsx-a11y/no-static-element-interactions": 0, "jsx-a11y/no-static-element-interactions": 0,
"jsx-a11y/no-noninteractive-element-interactions": 0, "jsx-a11y/no-noninteractive-element-interactions": 0,
"jsx-a11y/click-events-have-key-events": 0, "jsx-a11y/click-events-have-key-events": 0,
"jsx-a11y/label-has-for": [ 2, { "jsx-a11y/label-has-for": [
2,
{
"components": ["Label"], "components": ["Label"],
"required": { "required": {
"every": ["id"] "every": ["id"]
}, },
"allowChildren": false "allowChildren": false
}], }
],
"react/no-array-index-key": 0, "react/no-array-index-key": 0,
"react/forbid-prop-types": 0, "react/forbid-prop-types": 0,
"camelcase": 0, "camelcase": 0,
"curly": ["error", "all"],
"react/require-default-props": 0, "react/require-default-props": 0,
"max-len": ["error", 150], "max-len": ["error", { "code": 150, "ignoreUrls": true }],
"import/no-extraneous-dependencies": 0, "import/no-extraneous-dependencies": 0,
"no-confusing-arrow": "error",
"no-mixed-operators": "error",
"no-new": 0, "no-new": 0,
"no-tabs": "error",
"compat/compat": "error", "compat/compat": "error",
"prefer-destructuring": ["error", { "prefer-destructuring": [
"error",
{
"array": false, "array": false,
"object": true "object": true
}], }
],
"prefer-promise-reject-errors": 0, "prefer-promise-reject-errors": 0,
"no-param-reassign": [2, { "props": false }] "no-param-reassign": [2, { "props": false }]
}, },
"plugins": [ "plugins": ["flowtype", "import", "json", "markdown", "prettier", "promise", "compat", "react"],
"flowtype",
"import",
"promise",
"compat",
"react"
],
"settings": { "settings": {
"import/resolver": { "import/resolver": {
"node": { "node": {

4
.gitignore

@ -53,3 +53,7 @@ npm-debug.log.*
# lnd binary # lnd binary
resources/bin/ resources/bin/
# Opt in files
.opt-in
.opt-out

6
.huskyrc

@ -0,0 +1,6 @@
{
"hooks": {
"commit-msg": "opt --in commit-msg --exec 'commitlint -E HUSKY_GIT_PARAMS'",
"pre-commit": "opt --in pre-commit --exec 'lint-staged'"
}
}

6
.lintstagedrc

@ -0,0 +1,6 @@
{
"linters": {
"*.{js,json,md}": ["npm run lint-fix-base", "git add"],
"*.scss": ["npm run lint-styles-fix-base", "git add"]
}
}

22
.prettierignore

@ -0,0 +1,22 @@
# App source
package.json
package-lock.json
.git
.babelrc
node_modules
coverage
# App packaged
release
app/main.prod.js
app/main.prod.js.map
app/renderer.prod.js
app/renderer.prod.js.map
app/style.css
app/style.css.map
app/utils/bech32.js
dist
dll
main.js
main.js.map

15
.prettierrc

@ -0,0 +1,15 @@
{
"bracketSpacing": true,
"printWidth": 150,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"overrides": [
{
"files": [".babelrc", ".commitlintrc", ".eslintrc", ".huskyrc", ".lintstagedrc", ".prettierrc", ".stylelintrc"],
"options": {
"parser": "json"
}
}
]
}

2
.travis.yml

@ -5,6 +5,7 @@ language: node_js
env: env:
global: global:
- DEBUG=electron-builder - DEBUG=electron-builder
- ELECTRON_DISABLE_SECURITY_WARNINGS=true
matrix: matrix:
- TEST=lint-ci - TEST=lint-ci
- TEST=test-ci - TEST=test-ci
@ -31,7 +32,6 @@ addons:
install: install:
- yarn - yarn
- cd app && yarn && cd ..
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16" - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16"
before_script: before_script:

22
ADVANCED.md

@ -1,8 +1,14 @@
<h1 align='center'>Advanced Usage</h1> # Advanced Usage
## Table of Contents
- [Compiling Zap From Source](#Compiling-Zap-From-Source)
- [Lightning Network Daemon (lnd)](<#Lightning-Network-Daemon-(lnd)>)
- [Running Zap](#Running-Zap)
## Compiling Zap From Source ## Compiling Zap From Source
***Note:*** *If you have installation or compilation issues, please file a [Github issue](https://github.com/LN-Zap/zap-desktop/issues) or ping us in [Slack](https://join.slack.com/t/zaphq/shared_invite/enQtMzMxMzIzNDU0NTY3LTgyM2QwYzAyZTA5OTAyMjEwMTQxZmZmZmZkNWUzMTU2MmMyNmMxNjY4Y2VjY2FiYTRkMTkwMTRlMTE4YjM2MWY).* **_Note:_** _If you have installation or compilation issues, please file a [Github issue][issues] or ping us in [Slack][slack]._
### Prerequisites ### Prerequisites
@ -21,12 +27,11 @@ git clone https://github.com/LN-Zap/zap-desktop.git
### Installing Dependencies ### Installing Dependencies
Install all the dependencies with yarn + install grpc: Install all the dependencies with yarn:
```bash ```bash
cd zap-desktop cd zap-desktop
yarn yarn
npm run install-grpc
``` ```
## Lightning Network Daemon (lnd) ## Lightning Network Daemon (lnd)
@ -46,7 +51,7 @@ This is the default configuration for the Zap wallet. To use the light client yo
#### Lightning Labs Binary #### Lightning Labs Binary
***Note:*** *The Lightning Labs `lightning-app` project is different then [lnd](https://github.com/lightningnetwork/lnd)* **_Note:_** _The Lightning Labs `lightning-app` project is different then [lnd](https://github.com/lightningnetwork/lnd)_
Download the [lnd binary](https://github.com/lightningnetwork/lnd/releases) for your appropriate OS and copy it to the [appropriate location](#lnd-location) for your OS. Download the [lnd binary](https://github.com/lightningnetwork/lnd/releases) for your appropriate OS and copy it to the [appropriate location](#lnd-location) for your OS.
@ -56,14 +61,12 @@ You can [compile](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTA
For Zap to run properly without any custom `lnd` setup, copy the `lnd` binary to the [appropriate location](#lnd-location) for your OS. For Zap to run properly without any custom `lnd` setup, copy the `lnd` binary to the [appropriate location](#lnd-location) for your OS.
The `lnd` binary can be found at `$GOPATH/bin`. The `lnd` binary can be found at `$GOPATH/bin`.
### Full Bitcoin Node ### Full Bitcoin Node
Follow the instructions on the [lnd installation](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md) page. Follow the instructions on the [lnd installation](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md) page.
### lnd Location ### lnd Location
Zap expects `lnd` to be in one of these directories depending on your OS: Zap expects `lnd` to be in one of these directories depending on your OS:
@ -81,6 +84,7 @@ chmod +x lnd
## Running Zap ## Running Zap
### Testing ### Testing
To test that everything has been installed correctly: To test that everything has been installed correctly:
```bash ```bash
@ -96,8 +100,12 @@ npm run dev
``` ```
### Linting ### Linting
To check linting: To check linting:
```bash ```bash
npm run lint npm run lint
``` ```
[issues]: https://github.com/LN-Zap/zap-desktop/issues
[slack]: https://join.slack.com/t/zaphq/shared_invite/enQtMzMxMzIzNDU0NTY3LTgyM2QwYzAyZTA5OTAyMjEwMTQxZmZmZmZkNWUzMTU2MmMyNmMxNjY4Y2VjY2FiYTRkMTkwMTRlMTE4YjM2MWY

92
CONTRIBUTING.md

@ -1,34 +1,96 @@
<h1 align='center'>Contributing</h1> # Contributing
(Even after a recent refactor the code is still a bit sloppy, in a bit of a segwit rush, apologize in advance for any "wtf is this?")
## Overview Thanks for being willing to contribute!
Please join us on [slack](https://join.slack.com/t/zaphq/shared_invite/enQtMzMxMzIzNDU0NTY3LTgyM2QwYzAyZTA5OTAyMjEwMTQxZmZmZmZkNWUzMTU2MmMyNmMxNjY4Y2VjY2FiYTRkMTkwMTRlMTE4YjM2MWY) and check [open issues](https://github.com/LN-Zap/zap-desktop/issues) to see what contributions are needed before tackling a task to avoid duplicate work.
## Pull Requests ## Table of Contents
The `master` branch will be used for all pull requests for the time being. This may change as the repo and contributors grow.
### Branch Names - [How to Contribute](#How-to-Contribute)
- [Contribution-Guidelines](#Contribution-Guidelines)
## How to Contribute
#### **Did you find a bug?**
- **Do not open up a GitHub issue if the bug is a security vulnerability in Zap**, and instead to refer to our [Security Policy](README#security).
- **Ensure the bug was not already reported** by searching on GitHub under [Issues][issues].
- If you're unable to find an open issue addressing the problem, [open a new one][issues]. Be sure to include a **title and clear description**, and as much relevant information as possible.
#### **Did you write a patch that fixes a bug?**
- Open a new GitHub pull request with the patch.
- Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable.
- Before submitting, please read the [Coding Guidelines](#coding-guidelines) to know more about our coding conventions and practices.
#### **Do you intend to add a new feature or change an existing one?**
- Please join us on [slack][slack] and check [open issues][issues] to see what contributions are needed before tackling a task to avoid duplicate work.
#### **Do you have questions about the source code?**
- Ask any question about the Zap source code in [slack][slack].
## Contribution Guidelines
Branch names should start with `feature` or `fix` followed by `/description_of_branch`. ### Committing and Pushing changes
We follow the [conventional changelog standard][convention] for commit messages. You don't have to follow this convention if you don't like to. Just know that when we merge your commit, we'll probably use "Squash and Merge" so we can change the commit message :)
Valid conventional commit types are:
- `build`: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
- `chore`: Other changes that don't modify src or test files
- `ci`: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
- `docs`: Documentation only changes
- `feat`: A new feature
- `fix`: A bug fix
- `perf`: A code change that improves performance
- `refactor`: A code change that neither fixes a bug nor adds a feature
- `revert`: Reverts a previous commit
- `style`: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- `test`: Adding missing tests or correcting existing tests
#### Example #### Example
```bash ```bash
git branch feature/list-onchain-txs git commit -m "feat(close-channel): wire up close channel to UI"
```
Please make sure to run the tests before you commit your changes. You can run `npm test` which will run the test suite.
### Opt into git hooks
There are git hooks set up with this project that are automatically installed when you install dependencies. They're really handy, but are turned off by default (so as to not hinder new contributors). You can opt into these by creating a file called `.opt-in` at the root of the project and putting this inside:
```
commit-msg
pre-commit
``` ```
### Commit Messages ### Branch Names
Commit messages should start with `feature`, `fix`, or `test` followed by `(subject_of_commit)` and ending with `: description_of_commit`. Branch names should start with a valid conventional commit type followed by `/description_of_branch`.
#### Example #### Example
```bash ```bash
git commit -m "feature(list-onchain-txs): create hard code mock of onchain-txs list" git branch feat/close-channel-ui
``` ```
## eslint ### Pull Requests
This project has eslint rules and pull requests should pass `npm run lint` before being merged. The eslint rules are not final by any means and can be changed if necessary
The `master` branch will be used for all pull requests for the time being. This may change as the repo and contributors grow.
### Style Guide
This project has eslint rules and pull requests should pass `npm run lint` before being merged. The eslint rules are not final by any means and can be changed if necessary.
### Tests
## Tests
Tests should try to be written for every feature/fix and pass `npm run test` before being merged. With the demand for the Lightning Network and Zap rising, rapid development will naturally leave some code untested but we should all try our best. Tests should try to be written for every feature/fix and pass `npm run test` before being merged. With the demand for the Lightning Network and Zap rising, rapid development will naturally leave some code untested but we should all try our best.
[issues]: https://github.com/LN-Zap/zap-desktop/issues
[slack]: https://join.slack.com/t/zaphq/shared_invite/enQtMzgyNDA2NDI2Nzg0LTQwZWQ2ZWEzOWFhMjRiNWZkZWMwYTA4MzA5NzhjMDNhNTM5YzliNDA4MmZkZWZkZTFmODM4ODJkYzU3YmI3ZmI

5
LICENSE

@ -1,6 +1,6 @@
The MIT License (MIT) MIT License
Copyright (c) 2015-present C. T. Lin Copyright (c) 2017-present Jack Mallers
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.

87
README.md

@ -1,27 +1,46 @@
<h1 align='center'> # Zap
<p align='center'>
<a href='https://zap.jackmallers.com'>
<img src='https://imgur.com/svn8Jrw.jpg' alt='screenshot' /> <img src='https://imgur.com/svn8Jrw.jpg' alt='screenshot' />
<br /> </a>
<center> </p>
<a href='https://zap.jackmallers.com'>Zap</a>
</center> > Lightning wallet focused on user experience and ease of use ⚡️
</h1>
[![dependencies Status](https://david-dm.org/LN-Zap/zap-desktop/status.svg)](https://david-dm.org/LN-Zap/zap-desktop)
[![Build Status](https://travis-ci.org/LN-Zap/zap-desktop.svg?branch=master)](https://travis-ci.org/LN-Zap/zap-desktop)
[![GitHub license](https://img.shields.io/github/license/LN-Zap/zap-desktop.svg)](LICENSE)
Zap is a free Lightning Network wallet focused on user experience and ease of use, with the overall goal of helping the cryptocurrency community scale Bitcoin and other cryptocurrencies. Zap is a free Lightning Network wallet focused on user experience and ease of use, with the overall goal of helping the cryptocurrency community scale Bitcoin and other cryptocurrencies.
The UI for Zap is created using The UI for Zap is created using
[Electron](https://electron.atom.io/) + [React](https://facebook.github.io/react/) + [Redux](https://github.com/reactjs/redux/tree/master/docs). [Electron](https://electron.atom.io/) + [React](https://facebook.github.io/react/) + [Redux](https://github.com/reactjs/redux/tree/master/docs).
We have an active [slack](https://join.slack.com/t/zaphq/shared_invite/enQtMzMxMzIzNDU0NTY3LTgyM2QwYzAyZTA5OTAyMjEwMTQxZmZmZmZkNWUzMTU2MmMyNmMxNjY4Y2VjY2FiYTRkMTkwMTRlMTE4YjM2MWY) channel where you can join the discussion on development, design and product. We have an active [slack][slack] channel where you can join the discussion on development, design and product.
## Table of Contents
## Installing - [Security](#security)
- [Install](#install)
- [Usage](#usage)
- [Advanced Usage](#advanced-usage)
- [Get Help](#get-help)
- [Maintainers](#maintainers)
- [Contribute](#contribute)
- [License](#license)
***Note:*** *If you would like to use a full bitcoin node, please see the [advanced usage](https://github.com/LN-Zap/zap-desktop/blob/master/ADVANCED.md) page.* ## Security
Download the [latest release](https://github.com/LN-Zap/zap-desktop/releases) for your appropriate OS and follow the instructions below. If you discover or learn about a potential error, weakness, or threat that can compromise the security of Zap, we ask you to keep it confidential and [submit your concern directly to the Zap security team](mailto:jimmymowschess@gmail.com?subject=[GitHub]%20Zap%20Security).
## Install
Download the [latest release][releases] for your appropriate OS and follow the instructions below.
### macOS ### macOS
Once you have the .zip file downloaded, simply **double click** on the file to unzip. Once you have the .tar.gz file downloaded, simply **double click** on the file to unzip.
Navigate to the newly extracted folder, then drag-and-drop the `Zap.app` file to the `Applications` folder. Navigate to the newly extracted folder, then drag-and-drop the `Zap.app` file to the `Applications` folder.
@ -72,22 +91,48 @@ Once you have the .AppImage file extracted, you can either **double click** the
``` ```
## Advanced Usage ## Advanced Usage
If you would like to install from source or run a full bitcoin node, please see the [advanced usage](https://github.com/LN-Zap/zap-desktop/blob/master/ADVANCED.md) page.
### Contributing If you would like to install from source, run a full bitcoin node, or connect to a custom lnd instance please see the [advanced usage](ADVANCED.md) page.
If you would like to help contribute to the project, please see the [contributing guide](https://github.com/LN-Zap/zap-desktop/blob/master/CONTRIBUTING.md).
## Q & A (Quality and Assurance) ## Get Help
***Note:*** *If you are having problems with Zap, please report the issue in [GitHub](https://github.com/LN-Zap/zap-desktop/issues) or on [slack](https://join.slack.com/t/zaphq/shared_invite/enQtMzMxMzIzNDU0NTY3LTgyM2QwYzAyZTA5OTAyMjEwMTQxZmZmZmZkNWUzMTU2MmMyNmMxNjY4Y2VjY2FiYTRkMTkwMTRlMTE4YjM2MWY) with screenshots and/or how to reproduce the bug/error.* If you are having problems with Zap, please report the issue in [GitHub][issues] or on [slack][slack] with screenshots and/or how to reproduce the bug/error.
A good product not only has good software tests but also checks the quality of the UX/UI. Putting ourselves in the shoes of a user is a very important design principle of Zap. A good product not only has good software tests but also checks the quality of the UX/UI. Putting ourselves in the shoes of a user is a very important design principle of Zap.
### Example User Stories ## Maintainers
`User wants to connect to a peer`
- [Jack Mallers (@JimmyMow)](https://github.com/JimmyMow)
- [Ben Woosley (@Empact)](https://github.com/Empact)
## Contribute
Hey! Do you like Zap? Awesome! We could actually really use your help!
Open source isn't just writing code. Zap could use your help with any of the following:
- Finding (and reporting!) bugs
- New feature suggestions
- Answering questions on issues
- Documentation improvements
- Reviewing pull requests
- Helping to manage issue priorities
- Fixing bugs/new features
If any of that sounds cool to you, feel free to dive in! [Open an issue][issues] or submit a pull request.
If you would like to help contribute to the project, please see the [Contributing Guide](CONTRIBUTING.md)
This project exists thanks to all the people who contribute.
[<img alt="JimmyMow" src="https://avatars2.githubusercontent.com/u/4040039?v=4&s=64" width="64">](https://github.com/JimmyMow)[<img alt="Empact" src="https://avatars2.githubusercontent.com/u/5470?v=4&s=64" width="64">](https://github.com/Empact)[<img alt="jackmallers" src="https://avatars3.githubusercontent.com/u/30220954?v=4&s=64" width="64">](https://github.com/jackmallers)[<img alt="mrfelton" src="https://avatars0.githubusercontent.com/u/200251?v=4&s=64" width="64">](https://github.com/mrfelton)[<img alt="VonIobro" src="https://avatars2.githubusercontent.com/u/61939?v=4&s=64" width="64">](https://github.com/VonIobro)[<img alt="joaodealmeida" src="https://avatars3.githubusercontent.com/u/5623455?v=4&s=64" width="64">](https://github.com/joaodealmeida)[<img alt="helgabutters" src="https://avatars2.githubusercontent.com/u/8001978?v=4&s=64" width="64">](https://github.com/helgabutters)[<img alt="odb366" src="https://avatars3.githubusercontent.com/u/14116101?v=4&s=64" width="64">](https://github.com/odb366)[<img alt="pajasevi" src="https://avatars3.githubusercontent.com/u/2407408?v=4&s=64" width="64">](https://github.com/pajasevi)[<img alt="jimpo" src="https://avatars3.githubusercontent.com/u/881253?v=4&s=64" width="64">](https://github.com/jimpo)[<img alt="NahomAgidew" src="https://avatars2.githubusercontent.com/u/11695305?v=4&s=64" width="64">](https://github.com/NahomAgidew)[<img alt="DataCourier" src="https://avatars1.githubusercontent.com/u/35670446?v=4&s=64" width="64">](https://github.com/DataCourier)[<img alt="tbloncar" src="https://avatars1.githubusercontent.com/u/2092395?v=4&s=64" width="64">](https://github.com/tbloncar)[<img alt="waseem999" src="https://avatars3.githubusercontent.com/u/17360809?v=4&s=64" width="64">](https://github.com/waseem999)[<img alt="dfattlar" src="https://avatars3.githubusercontent.com/u/4843270?v=4&s=64" width="64">](https://github.com/dfattlar)[<img alt="jtarre" src="https://avatars1.githubusercontent.com/u/1143894?v=4&s=64" width="64">](https://github.com/jtarre)[<img alt="dimitris-t" src="https://avatars1.githubusercontent.com/u/8949706?v=4&s=64" width="64">](https://github.com/dimitris-t)[<img alt="thinkjanis" src="https://avatars1.githubusercontent.com/u/31632325?v=4&s=64" width="64">](https://github.com/thinkjanis)[<img alt="fresheneesz" src="https://avatars3.githubusercontent.com/u/149531?v=4&s=64" width="64">](https://github.com/fresheneesz)[<img alt="funyug" src="https://avatars2.githubusercontent.com/u/8094201?v=4&s=64" width="64">](https://github.com/funyug)
## License
`User wants to open a channel` This project is open source under the MIT license, which means you have full access to the source code and can modify it to fit your own needs. See [LICENCE](LICENCE) for more information.
`User wants to create a payment request` [MIT](LICENSE) © Jack Mallers
`User wants to make a payment` [issues]: https://github.com/LN-Zap/zap-desktop/issues
[releases]: https://github.com/LN-Zap/zap-desktop/releases
[slack]: https://join.slack.com/t/zaphq/shared_invite/enQtMzgyNDA2NDI2Nzg0LTQwZWQ2ZWEzOWFhMjRiNWZkZWMwYTA4MzA5NzhjMDNhNTM5YzliNDA4MmZkZWZkZTFmODM4ODJkYzU3YmI3ZmI

5
app/api/index.js

@ -11,7 +11,8 @@ export function requestTicker(id) {
} }
export function requestTickers(ids) { export function requestTickers(ids) {
return axios.all(ids.map(id => requestTicker(id))) return axios
.all(ids.map(id => requestTicker(id)))
.then(axios.spread((btcTicker, ltcTicker) => ({ btcTicker: btcTicker[0], ltcTicker: ltcTicker[0] }))) .then(axios.spread((btcTicker, ltcTicker) => ({ btcTicker: btcTicker[0], ltcTicker: ltcTicker[0] })))
} }
@ -26,7 +27,7 @@ export function requestBlockHeight() {
} }
export function requestSuggestedNodes() { export function requestSuggestedNodes() {
const BASE_URL = 'http://zap.jackmallers.com/suggested-peers' const BASE_URL = 'https://zap.jackmallers.com/suggested-peers'
return axios({ return axios({
method: 'get', method: 'get',
url: BASE_URL url: BASE_URL

2
app/app.global.scss

@ -8,7 +8,7 @@
font-family: 'Roboto'; font-family: 'Roboto';
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light'), url(http://fonts.gstatic.com/s/roboto/v15/Fl4y0QdOxyyTHEGMXX8kcaCWcynf_cDxXwCLxiixG1c.ttf) format('truetype'); src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/Fl4y0QdOxyyTHEGMXX8kcaCWcynf_cDxXwCLxiixG1c.ttf) format('truetype');
} }
body { body {

8
app/app.html

@ -2,9 +2,15 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
connect-src 'self' http://localhost:* ws://localhost:* https://api.coinmarketcap.com https://zap.jackmallers.com https://testnet-api.smartbit.com.au;
script-src 'self' http://localhost:* 'unsafe-eval' 'unsafe-inline';
font-src 'self' data: http://localhost:* https://fonts.googleapis.com https://s3.amazonaws.com https://fonts.gstatic.com;
style-src 'self' blob: https://fonts.googleapis.com https://s3.amazonaws.com https://fonts.gstatic.com 'unsafe-inline';">
<title>Zap</title> <title>Zap</title>
<link rel="stylesheet" href="https://s3.amazonaws.com/fonts.typotheque.com/WF-018717-007225.css" type="text/css" /> <link rel="stylesheet" href="https://s3.amazonaws.com/fonts.typotheque.com/WF-018717-007225.css" type="text/css" />
<link href='http://fonts.googleapis.com/css?family=Raleway:700' rel='stylesheet' type='text/css'> <link href='https://fonts.googleapis.com/css?family=Raleway:700' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Orbitron' rel='stylesheet' type='text/css'> <link href='https://fonts.googleapis.com/css?family=Orbitron' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Roboto:300' rel='stylesheet' type='text/css'> <link href='https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Roboto:300' rel='stylesheet' type='text/css'>
<script> <script>

16
app/components/Activity/ActivityModal.js

@ -14,7 +14,7 @@ const ActivityModal = ({
modalProps, modalProps,
ticker, ticker,
currentTicker, currentTicker,
isTestnet, network,
hideActivityModal, hideActivityModal,
toggleCurrencyProps toggleCurrencyProps
@ -25,7 +25,9 @@ const ActivityModal = ({
INVOICE: InvoiceModal INVOICE: InvoiceModal
} }
if (!modalType) { return null } if (!modalType) {
return null
}
const SpecificModal = MODAL_COMPONENTS[modalType] const SpecificModal = MODAL_COMPONENTS[modalType]
return ( return (
@ -35,13 +37,7 @@ const ActivityModal = ({
<Isvg src={x} /> <Isvg src={x} />
</span> </span>
</div> </div>
<SpecificModal <SpecificModal {...modalProps} network={network} ticker={ticker} currentTicker={currentTicker} toggleCurrencyProps={toggleCurrencyProps} />
{...modalProps}
isTestnet={isTestnet}
ticker={ticker}
currentTicker={currentTicker}
toggleCurrencyProps={toggleCurrencyProps}
/>
</div> </div>
) )
} }
@ -51,7 +47,7 @@ ActivityModal.propTypes = {
currentTicker: PropTypes.object.isRequired, currentTicker: PropTypes.object.isRequired,
toggleCurrencyProps: PropTypes.object.isRequired, toggleCurrencyProps: PropTypes.object.isRequired,
isTestnet: PropTypes.bool.isRequired, network: PropTypes.object.isRequired,
modalType: PropTypes.string, modalType: PropTypes.string,
modalProps: PropTypes.object.isRequired, modalProps: PropTypes.object.isRequired,

35
app/components/Activity/Countdown.js

@ -34,7 +34,8 @@ class Countdown extends React.Component {
const convertTwoDigits = n => (n > 9 ? n : `0${n}`.slice(-2)) const convertTwoDigits = n => (n > 9 ? n : `0${n}`.slice(-2))
const now = new Date().getTime() const now = new Date().getTime()
const distance = (this.props.countDownDate * 1000) - now const countDownSeconds = this.props.countDownDate * 1000
const distance = countDownSeconds - now
if (distance <= 0) { if (distance <= 0) {
this.setState({ expired: true }) this.setState({ expired: true })
@ -56,32 +57,22 @@ class Countdown extends React.Component {
} }
render() { render() {
const { const { days, hours, minutes, seconds, expired } = this.state
days,
hours,
minutes,
seconds,
expired
} = this.state
if (expired) { return <span className={`${styles.container} ${styles.expired}`}>Expired</span> } if (expired) {
if (!days && !hours && !minutes && !seconds) { return <span className={styles.container} /> } return <span className={`${styles.container} ${styles.expired}`}>Expired</span>
}
if (!days && !hours && !minutes && !seconds) {
return <span className={styles.container} />
}
return ( return (
<span className={styles.container}> <span className={styles.container}>
<i className={styles.caption}>Expires in</i> <i className={styles.caption}>Expires in</i>
<i> <i>{days > 0 && `${days}:`}</i>
{days > 0 && `${days}:`} <i>{hours > 0 && `${hours}:`}</i>
</i> <i>{minutes > 0 && `${minutes}:`}</i>
<i> <i>{seconds >= 0 && `${seconds}`}</i>
{hours > 0 && `${hours}:`}
</i>
<i>
{minutes > 0 && `${minutes}:`}
</i>
<i>
{seconds >= 0 && `${seconds}`}
</i>
</span> </span>
) )
} }

38
app/components/Activity/InvoiceModal.js

@ -20,20 +20,14 @@ const InvoiceModal = ({
ticker, ticker,
currentTicker, currentTicker,
toggleCurrencyProps: { toggleCurrencyProps: { setActivityModalCurrencyFilters, showCurrencyFilters, currencyName, currentCurrencyFilters, onCurrencyFilterClick }
setActivityModalCurrencyFilters,
showCurrencyFilters,
currencyName,
currentCurrencyFilters,
onCurrencyFilterClick
}
}) => { }) => {
const copyPaymentRequest = () => { const copyPaymentRequest = () => {
copy(invoice.payment_request) copy(invoice.payment_request)
showNotification('Noice', 'Successfully copied to clipboard') showNotification('Noice', 'Successfully copied to clipboard')
} }
const countDownDate = (parseInt(invoice.creation_date, 10) + parseInt(invoice.expiry, 10)) const countDownDate = parseInt(invoice.creation_date, 10) + parseInt(invoice.expiry, 10)
return ( return (
<div className={styles.container}> <div className={styles.container}>
@ -42,11 +36,11 @@ const InvoiceModal = ({
<h2>Payment Request</h2> <h2>Payment Request</h2>
<QRCode <QRCode
value={invoice.payment_request} value={invoice.payment_request}
renderAs='svg' renderAs="svg"
size={150} size={150}
bgColor='transparent' bgColor="transparent"
fgColor='white' fgColor="white"
level='L' level="L"
className={styles.qrcode} className={styles.qrcode}
/> />
<Countdown countDownDate={countDownDate} /> <Countdown countDownDate={countDownDate} />
@ -58,22 +52,24 @@ const InvoiceModal = ({
<Value value={invoice.value} currency={ticker.currency} currentTicker={currentTicker} /> <Value value={invoice.value} currency={ticker.currency} currentTicker={currentTicker} />
</h1> </h1>
<section className={styles.currentCurrency} onClick={() => setActivityModalCurrencyFilters(!showCurrencyFilters)}> <section className={styles.currentCurrency} onClick={() => setActivityModalCurrencyFilters(!showCurrencyFilters)}>
<span>{currencyName}</span><span><FaAngleDown /></span> <span>{currencyName}</span>
<span>
<FaAngleDown />
</span>
</section> </section>
<ul className={showCurrencyFilters && styles.active}> <ul className={showCurrencyFilters && styles.active}>
{ {currentCurrencyFilters.map(filter => (
currentCurrencyFilters.map(filter => <li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>
<li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>{filter.name}</li>) {filter.name}
} </li>
))}
</ul> </ul>
</section> </section>
<section className={styles.date}> <section className={styles.date}>
<p> <p>
<Moment format='MM/DD/YYYY'>{invoice.creation_date * 1000}</Moment> <Moment format="MM/DD/YYYY">{invoice.creation_date * 1000}</Moment>
</p>
<p className={styles.notPaid}>
{!invoice.settled && 'Not Paid'}
</p> </p>
<p className={styles.notPaid}>{!invoice.settled && 'Not Paid'}</p>
</section> </section>
</div> </div>

25
app/components/Activity/PaymentModal.js

@ -19,13 +19,7 @@ const PaymentModal = ({
ticker, ticker,
currentTicker, currentTicker,
toggleCurrencyProps: { toggleCurrencyProps: { setActivityModalCurrencyFilters, showCurrencyFilters, currencyName, currentCurrencyFilters, onCurrencyFilterClick }
setActivityModalCurrencyFilters,
showCurrencyFilters,
currencyName,
currentCurrencyFilters,
onCurrencyFilterClick
}
}) => ( }) => (
<div className={styles.container}> <div className={styles.container}>
<header className={styles.header}> <header className={styles.header}>
@ -51,19 +45,22 @@ const PaymentModal = ({
<Value value={payment.value} currency={ticker.currency} currentTicker={currentTicker} /> <Value value={payment.value} currency={ticker.currency} currentTicker={currentTicker} />
</h1> </h1>
<section className={styles.currentCurrency} onClick={() => setActivityModalCurrencyFilters(!showCurrencyFilters)}> <section className={styles.currentCurrency} onClick={() => setActivityModalCurrencyFilters(!showCurrencyFilters)}>
<span>{currencyName}</span><span><FaAngleDown /></span> <span>{currencyName}</span>
<span>
<FaAngleDown />
</span>
<ul className={showCurrencyFilters && styles.active}> <ul className={showCurrencyFilters && styles.active}>
{ {currentCurrencyFilters.map(filter => (
currentCurrencyFilters.map(filter => <li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>
<li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>{filter.name}</li>) {filter.name}
} </li>
))}
</ul> </ul>
</section> </section>
</div> </div>
<div className={styles.date}> <div className={styles.date}>
<Moment format='LLL'>{payment.creation_date * 1000}</Moment> <Moment format="LLL">{payment.creation_date * 1000}</Moment>
</div> </div>
<footer className={styles.footer}> <footer className={styles.footer}>

44
app/components/Activity/TransactionModal.js

@ -19,15 +19,9 @@ const TransactionModal = ({
transaction, transaction,
ticker, ticker,
currentTicker, currentTicker,
isTestnet, network,
toggleCurrencyProps: { toggleCurrencyProps: { setActivityModalCurrencyFilters, showCurrencyFilters, currencyName, currentCurrencyFilters, onCurrencyFilterClick }
setActivityModalCurrencyFilters,
showCurrencyFilters,
currencyName,
currentCurrencyFilters,
onCurrencyFilterClick
}
}) => ( }) => (
<div className={styles.container}> <div className={styles.container}>
<header className={styles.header}> <header className={styles.header}>
@ -38,7 +32,9 @@ const TransactionModal = ({
<section className={styles.details}> <section className={styles.details}>
<div> <div>
<Isvg src={link} /> <Isvg src={link} />
<span className={styles.link} onClick={() => blockExplorer.showTransaction(isTestnet, transaction.tx_hash)}>On-Chain</span> <span className={styles.link} onClick={() => blockExplorer.showTransaction(network, transaction.tx_hash)}>
On-Chain
</span>
</div> </div>
<div> <div>
<Value value={transaction.total_fees} currency={ticker.currency} currentTicker={currentTicker} /> <Value value={transaction.total_fees} currency={ticker.currency} currentTicker={currentTicker} />
@ -49,34 +45,30 @@ const TransactionModal = ({
<div className={styles.amount}> <div className={styles.amount}>
<h1> <h1>
<i className={`${styles.symbol} ${transaction.amount > 0 && styles.active}`}> <i className={`${styles.symbol} ${transaction.amount > 0 && styles.active}`}>{transaction.amount > 0 ? '+' : '-'}</i>
{
transaction.amount > 0 ?
'+'
:
'-'
}
</i>
<Value value={transaction.amount} currency={ticker.currency} currentTicker={currentTicker} /> <Value value={transaction.amount} currency={ticker.currency} currentTicker={currentTicker} />
</h1> </h1>
<section className={styles.currentCurrency} onClick={() => setActivityModalCurrencyFilters(!showCurrencyFilters)}> <section className={styles.currentCurrency} onClick={() => setActivityModalCurrencyFilters(!showCurrencyFilters)}>
<span>{currencyName}</span><span><FaAngleDown /></span> <span>{currencyName}</span>
<span>
<FaAngleDown />
</span>
<ul className={showCurrencyFilters && styles.active}> <ul className={showCurrencyFilters && styles.active}>
{ {currentCurrencyFilters.map(filter => (
currentCurrencyFilters.map(filter => <li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>
<li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>{filter.name}</li>) {filter.name}
} </li>
))}
</ul> </ul>
</section> </section>
</div> </div>
<div className={styles.date}> <div className={styles.date}>
<Moment format='LLL'>{transaction.time_stamp * 1000}</Moment> <Moment format="LLL">{transaction.time_stamp * 1000}</Moment>
</div> </div>
<footer className={styles.footer}> <footer className={styles.footer}>
<p onClick={() => blockExplorer.showTransaction(isTestnet, transaction.tx_hash)}>{transaction.tx_hash}</p> <p onClick={() => blockExplorer.showTransaction(network, transaction.tx_hash)}>{transaction.tx_hash}</p>
</footer> </footer>
</div> </div>
) )
@ -88,7 +80,7 @@ TransactionModal.propTypes = {
toggleCurrencyProps: PropTypes.object.isRequired, toggleCurrencyProps: PropTypes.object.isRequired,
isTestnet: PropTypes.bool.isRequired network: PropTypes.object.isRequired
} }
export default TransactionModal export default TransactionModal

44
app/components/Contacts/AddChannel.js

@ -21,7 +21,7 @@ const AddChannel = ({
showManualForm, showManualForm,
openManualForm openManualForm
}) => { }) => {
const renderRightSide = (node) => { const renderRightSide = node => {
if (loadingChannelPubkeys.includes(node.pub_key)) { if (loadingChannelPubkeys.includes(node.pub_key)) {
return ( return (
<span className={styles.inactive}> <span className={styles.inactive}>
@ -57,11 +57,7 @@ const AddChannel = ({
} }
if (!node.addresses.length) { if (!node.addresses.length) {
return ( return <span className={`${styles.private} ${styles.inactive}`}>Private</span>
<span className={`${styles.private} ${styles.inactive}`}>
Private
</span>
)
} }
return ( return (
@ -79,7 +75,7 @@ const AddChannel = ({
) )
} }
const searchUpdated = (search) => { const searchUpdated = search => {
updateContactFormSearchQuery(search) updateContactFormSearchQuery(search)
if (search.includes('@') && search.split('@')[0].length === 66) { if (search.includes('@') && search.split('@')[0].length === 66) {
@ -91,8 +87,8 @@ const AddChannel = ({
<div className={styles.container}> <div className={styles.container}>
<header className={styles.header}> <header className={styles.header}>
<input <input
type='text' type="text"
placeholder='Search the network...' placeholder="Search the network..."
className={styles.searchInput} className={styles.searchInput}
value={contactsform.searchQuery} value={contactsform.searchQuery}
onChange={event => searchUpdated(event.target.value)} onChange={event => searchUpdated(event.target.value)}
@ -105,38 +101,36 @@ const AddChannel = ({
<section className={styles.nodes}> <section className={styles.nodes}>
<ul className={styles.networkResults}> <ul className={styles.networkResults}>
{ {filteredNetworkNodes.map(node => (
filteredNetworkNodes.map(node => (
<li key={node.pub_key}> <li key={node.pub_key}>
<section> <section>
{ {node.alias.length > 0 ? (
node.alias.length > 0 ?
<h2> <h2>
<span>{node.alias.trim()}</span> <span>{node.alias.trim()}</span>
<span>({node.pub_key.substr(0, 10)}...{node.pub_key.substr(node.pub_key.length - 10)})</span> <span>
({node.pub_key.substr(0, 10)}...{node.pub_key.substr(node.pub_key.length - 10)})
</span>
</h2> </h2>
: ) : (
<h2> <h2>
<span>{node.pub_key}</span> <span>{node.pub_key}</span>
</h2> </h2>
} )}
</section>
<section>
{renderRightSide(node)}
</section> </section>
<section>{renderRightSide(node)}</section>
</li> </li>
)) ))}
}
</ul> </ul>
</section> </section>
{ {showManualForm && (
showManualForm &&
<section className={styles.manualForm}> <section className={styles.manualForm}>
<p>Hm, looks like we can&apos;t see that node from here, wanna try to manually connect?</p> <p>Hm, looks like we can&apos;t see that node from here, wanna try to manually connect?</p>
<div className={styles.manualConnectButton} onClick={openManualForm}>Connect Manually</div> <div className={styles.manualConnectButton} onClick={openManualForm}>
Connect Manually
</div>
</section> </section>
} )}
</div> </div>
) )
} }

5
app/components/Contacts/ChannelForm.js

@ -15,7 +15,9 @@ const FORM_TYPES = {
} }
const ChannelForm = ({ formType, formProps, closeForm }) => { const ChannelForm = ({ formType, formProps, closeForm }) => {
if (!formType) { return null } if (!formType) {
return null
}
const FormComponent = FORM_TYPES[formType] const FormComponent = FORM_TYPES[formType]
return ( return (
@ -30,7 +32,6 @@ const ChannelForm = ({ formType, formProps, closeForm }) => {
) )
} }
ChannelForm.propTypes = { ChannelForm.propTypes = {
formType: PropTypes.string, formType: PropTypes.string,
formProps: PropTypes.object.isRequired, formProps: PropTypes.object.isRequired,

13
app/components/Contacts/ConnectManually.js

@ -55,8 +55,8 @@ class ConnectManually extends React.Component {
<section className={styles.peer}> <section className={styles.peer}>
<div className={styles.input}> <div className={styles.input}>
<input <input
type='text' type="text"
placeholder='pubkey@host' placeholder="pubkey@host"
value={manualSearchQuery} value={manualSearchQuery}
onChange={event => updateManualFormSearchQuery(event.target.value)} onChange={event => updateManualFormSearchQuery(event.target.value)}
/> />
@ -64,16 +64,11 @@ class ConnectManually extends React.Component {
</section> </section>
<section className={`${styles.errorMessage} ${showErrors.manualInput && styles.active}`}> <section className={`${styles.errorMessage} ${showErrors.manualInput && styles.active}`}>
{showErrors.manualInput && {showErrors.manualInput && <span>{manualFormIsValid && manualFormIsValid.errors.manualInput}</span>}
<span>{manualFormIsValid && manualFormIsValid.errors.manualInput}</span>
}
</section> </section>
<section className={styles.submit}> <section className={styles.submit}>
<div <div className={`${styles.button} ${manualFormIsValid.isValid && styles.active}`} onClick={formSubmitted}>
className={`${styles.button} ${manualFormIsValid.isValid && styles.active}`}
onClick={formSubmitted}
>
Submit Submit
</div> </div>
</section> </section>

36
app/components/Contacts/ContactModal.js

@ -9,15 +9,10 @@ import { btc } from 'utils'
import styles from './ContactModal.scss' import styles from './ContactModal.scss'
const ContactModal = ({ const ContactModal = ({ isOpen, channel, closeContactModal, channelNodes, closeChannel, closingChannelIds }) => {
isOpen, if (!channel) {
channel, return <span />
closeContactModal, }
channelNodes,
closeChannel,
closingChannelIds
}) => {
if (!channel) { return <span /> }
const customStyles = { const customStyles = {
overlay: { overlay: {
@ -46,22 +41,19 @@ const ContactModal = ({
return ( return (
<ReactModal <ReactModal
isOpen={isOpen} isOpen={isOpen}
contentLabel='No Overlay Click Modal' contentLabel="No Overlay Click Modal"
ariaHideApp ariaHideApp
shouldCloseOnOverlayClick shouldCloseOnOverlayClick
onRequestClose={closeContactModal} onRequestClose={closeContactModal}
parentSelector={() => document.body} parentSelector={() => document.body}
style={customStyles} style={customStyles}
> >
{ {channel && (
channel &&
<div className={styles.container}> <div className={styles.container}>
<header className={styles.header}> <header className={styles.header}>
<div className={`${styles.status} ${channel.active && styles.online}`}> <div className={`${styles.status} ${channel.active && styles.online}`}>
<FaCircle style={{ verticalAlign: 'top' }} /> <FaCircle style={{ verticalAlign: 'top' }} />
<span> <span>{channel.active ? 'Online' : 'Offline'}</span>
{channel.active ? 'Online' : 'Offline'}
</span>
</div> </div>
<div className={styles.closeContainer}> <div className={styles.closeContainer}>
<span onClick={closeContactModal}> <span onClick={closeContactModal}>
@ -71,10 +63,7 @@ const ContactModal = ({
</header> </header>
<section className={styles.title}> <section className={styles.title}>
{ {node && <h1>{node.alias}</h1>}
node &&
<h1>{node.alias}</h1>
}
<h2>{channel.remote_pubkey}</h2> <h2>{channel.remote_pubkey}</h2>
</section> </section>
@ -106,19 +95,18 @@ const ContactModal = ({
</section> </section>
<footer> <footer>
{ {closingChannelIds.includes(channel.chan_id) ? (
closingChannelIds.includes(channel.chan_id) ?
<span className={styles.inactive}> <span className={styles.inactive}>
<div className={styles.loading}> <div className={styles.loading}>
<div className={styles.spinner} /> <div className={styles.spinner} />
</div> </div>
</span> </span>
: ) : (
<div onClick={removeClicked}>Remove</div> <div onClick={removeClicked}>Remove</div>
} )}
</footer> </footer>
</div> </div>
} )}
</ReactModal> </ReactModal>
) )
} }

75
app/components/Contacts/ContactsForm.js

@ -35,7 +35,7 @@ class ContactsForm extends React.Component {
const { editing } = this.state const { editing } = this.state
const renderRightSide = (node) => { const renderRightSide = node => {
if (loadingChannelPubkeys.includes(node.pub_key)) { if (loadingChannelPubkeys.includes(node.pub_key)) {
return ( return (
<span className={styles.inactive}> <span className={styles.inactive}>
@ -71,11 +71,7 @@ class ContactsForm extends React.Component {
} }
if (!node.addresses.length) { if (!node.addresses.length) {
return ( return <span className={`${styles.private} ${styles.inactive}`}>Private</span>
<span className={`${styles.private} ${styles.inactive}`}>
Private
</span>
)
} }
return ( return (
@ -90,12 +86,13 @@ class ContactsForm extends React.Component {
} }
const inputClicked = () => { const inputClicked = () => {
if (editing) { return } if (editing) {
return
}
this.setState({ editing: true }) this.setState({ editing: true })
} }
const manualFormSubmit = () => { const manualFormSubmit = () => {
if (!manualFormIsValid.isValid) { if (!manualFormIsValid.isValid) {
updateManualFormErrors(manualFormIsValid.errors) updateManualFormErrors(manualFormIsValid.errors)
@ -112,7 +109,7 @@ class ContactsForm extends React.Component {
updateManualFormSearchQuery('') updateManualFormSearchQuery('')
} }
const searchUpdated = (search) => { const searchUpdated = search => {
updateContactFormSearchQuery(search) updateContactFormSearchQuery(search)
if (search.includes('@') && search.split('@')[0].length === 66) { if (search.includes('@') && search.split('@')[0].length === 66) {
@ -124,7 +121,7 @@ class ContactsForm extends React.Component {
<div> <div>
<ReactModal <ReactModal
isOpen={contactsform.isOpen} isOpen={contactsform.isOpen}
contentLabel='No Overlay Click Modal' contentLabel="No Overlay Click Modal"
ariaHideApp ariaHideApp
shouldCloseOnOverlayClick shouldCloseOnOverlayClick
onRequestClose={() => closeContactsForm} onRequestClose={() => closeContactsForm}
@ -143,8 +140,8 @@ class ContactsForm extends React.Component {
<div className={styles.form}> <div className={styles.form}>
<div className={styles.search}> <div className={styles.search}>
<input <input
type='text' type="text"
placeholder='Find contact by alias or pubkey' placeholder="Find contact by alias or pubkey"
className={styles.searchInput} className={styles.searchInput}
value={contactsform.searchQuery} value={contactsform.searchQuery}
onChange={event => searchUpdated(event.target.value)} onChange={event => searchUpdated(event.target.value)}
@ -152,70 +149,63 @@ class ContactsForm extends React.Component {
</div> </div>
<ul className={styles.networkResults}> <ul className={styles.networkResults}>
{ {filteredNetworkNodes.map(node => (
filteredNetworkNodes.map(node => (
<li key={node.pub_key}> <li key={node.pub_key}>
<section> <section>
{ {node.alias.length > 0 ? (
node.alias.length > 0 ?
<h2> <h2>
<span>{node.alias.trim()}</span> <span>{node.alias.trim()}</span>
<span>({node.pub_key.substr(0, 10)}...{node.pub_key.substr(node.pub_key.length - 10)})</span> <span>
({node.pub_key.substr(0, 10)}...{node.pub_key.substr(node.pub_key.length - 10)})
</span>
</h2> </h2>
: ) : (
<h2> <h2>
<span>{node.pub_key}</span> <span>{node.pub_key}</span>
</h2> </h2>
} )}
</section>
<section>
{renderRightSide(node)}
</section> </section>
<section>{renderRightSide(node)}</section>
</li> </li>
)) ))}
}
</ul> </ul>
</div> </div>
{ {showManualForm && (
showManualForm &&
<div className={styles.manualForm}> <div className={styles.manualForm}>
<h2>Hm, looks like we cant see that contact from here. Want to try and manually connect?</h2> <h2>Hm, looks like we cant see that contact from here. Want to try and manually connect?</h2>
<section> <section>
<input <input
type='text' type="text"
placeholder='pubkey@host' placeholder="pubkey@host"
value={contactsform.manualSearchQuery} value={contactsform.manualSearchQuery}
onChange={event => updateManualFormSearchQuery(event.target.value)} onChange={event => updateManualFormSearchQuery(event.target.value)}
/> />
<div className={styles.submit} onClick={manualFormSubmit}>Submit</div> <div className={styles.submit} onClick={manualFormSubmit}>
Submit
</div>
{ {loadingChannelPubkeys.length > 0 && (
loadingChannelPubkeys.length > 0 &&
<div className={styles.manualFormSpinner}> <div className={styles.manualFormSpinner}>
<div className={styles.loading}> <div className={styles.loading}>
<div className={styles.spinner} /> <div className={styles.spinner} />
</div> </div>
</div> </div>
} )}
</section> </section>
<section className={`${styles.errorMessage} ${showErrors.manualInput && styles.active}`}> <section className={`${styles.errorMessage} ${showErrors.manualInput && styles.active}`}>
{showErrors.manualInput && {showErrors.manualInput && <span>{manualFormIsValid && manualFormIsValid.errors.manualInput}</span>}
<span>{manualFormIsValid && manualFormIsValid.errors.manualInput}</span>
}
</section> </section>
</div> </div>
} )}
<footer className={styles.footer}> <footer className={styles.footer}>
<div> <div>
<span> <span>Use</span>
Use
</span>
<span className={styles.amount}> <span className={styles.amount}>
<input <input
type='text' type="text"
value={contactsform.contactCapacity} value={contactsform.contactCapacity}
onChange={event => updateContactCapacity(event.target.value)} onChange={event => updateContactCapacity(event.target.value)}
onClick={inputClicked} onClick={inputClicked}
@ -226,10 +216,7 @@ class ContactsForm extends React.Component {
</span> </span>
<span className={styles.caption}> <span className={styles.caption}>
BTC per contact BTC per contact
<i <i data-hint="You aren't spending anything, just moving money onto the Lightning Network" className="hint--top">
data-hint="You aren't spending anything, just moving money onto the Lightning Network"
className='hint--top'
>
<FaQuestionCircle style={{ verticalAlign: 'top' }} /> <FaQuestionCircle style={{ verticalAlign: 'top' }} />
</i> </i>
</span> </span>

152
app/components/Contacts/Network.js

@ -23,14 +23,7 @@ class Network extends Component {
render() { render() {
const { const {
channels: { channels: { searchQuery, filterPulldown, filter, selectedChannel, loadingChannelPubkeys, closingChannelIds, channels },
searchQuery,
filterPulldown,
filter,
selectedChannel,
loadingChannelPubkeys,
closingChannelIds
},
currentChannels, currentChannels,
balance, balance,
ticker, ticker,
@ -53,7 +46,7 @@ class Network extends Component {
suggestedNodesProps, suggestedNodesProps,
isTestnet network
} = this.props } = this.props
const refreshClicked = () => { const refreshClicked = () => {
@ -84,12 +77,12 @@ class Network extends Component {
} }
// when the user clicks the action to close the channel // when the user clicks the action to close the channel
const removeClicked = (channel) => { const removeClicked = channel => {
closeChannel({ channel_point: channel.channel_point, chan_id: channel.chan_id, force: !channel.active }) closeChannel({ channel_point: channel.channel_point, chan_id: channel.chan_id, force: !channel.active })
} }
// when a user clicks a channel // when a user clicks a channel
const channelClicked = (channel) => { const channelClicked = channel => {
// selectedChannel === channel ? setSelectedChannel(null) : setSelectedChannel(channel) // selectedChannel === channel ? setSelectedChannel(null) : setSelectedChannel(channel)
if (selectedChannel === channel) { if (selectedChannel === channel) {
setSelectedChannel(null) setSelectedChannel(null)
@ -98,27 +91,36 @@ class Network extends Component {
} }
} }
const displayNodeName = (channel) => { const displayNodeName = channel => {
const node = find(nodes, n => channel.remote_pubkey === n.pub_key) const node = find(nodes, n => channel.remote_pubkey === n.pub_key)
if (node && node.alias.length) { return node.alias } if (node && node.alias.length) {
return node.alias
}
return channel.remote_pubkey ? channel.remote_pubkey.substring(0, 10) : channel.remote_node_pub.substring(0, 10) return channel.remote_pubkey ? channel.remote_pubkey.substring(0, 10) : channel.remote_node_pub.substring(0, 10)
} }
const channelStatus = (channel) => { const channelStatus = channel => {
// if the channel has a confirmation_height property that means it's pending // if the channel has a confirmation_height property that means it's pending
if (Object.prototype.hasOwnProperty.call(channel, 'confirmation_height')) { return 'pending' } if (Object.prototype.hasOwnProperty.call(channel, 'confirmation_height')) {
return 'pending'
}
// if the channel has a closing tx that means it's closing // if the channel has a closing tx that means it's closing
if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) { return 'closing' } if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) {
return 'closing'
}
// if we are in the process of closing this channel // if we are in the process of closing this channel
if (closingChannelIds.includes(channel.chan_id)) { return 'closing' } if (closingChannelIds.includes(channel.chan_id)) {
return 'closing'
}
// if the channel isn't active that means the remote peer isn't online // if the channel isn't active that means the remote peer isn't online
if (!channel.active) { return 'offline' } if (!channel.active) {
return 'offline'
}
// if all of the above conditionals fail we can assume the node is online :) // if all of the above conditionals fail we can assume the node is online :)
return 'online' return 'online'
@ -135,7 +137,7 @@ class Network extends Component {
{btc.satoshisToBtc(balance.channelBalance)}BTC ${usdAmount ? usdAmount.toLocaleString() : ''} {btc.satoshisToBtc(balance.channelBalance)}BTC ${usdAmount ? usdAmount.toLocaleString() : ''}
</span> </span>
</section> </section>
<section className={`${styles.addChannel} hint--bottom-left`} onClick={openContactsForm} data-hint='Open a channel'> <section className={`${styles.addChannel} hint--bottom-left`} onClick={openContactsForm} data-hint="Open a channel">
<span className={styles.plusContainer}> <span className={styles.plusContainer}>
<Isvg src={plus} /> <Isvg src={plus} />
</span> </span>
@ -143,48 +145,48 @@ class Network extends Component {
</header> </header>
<div className={styles.channels}> <div className={styles.channels}>
{ {!loadingChannelPubkeys.length && !channels.length && <SuggestedNodes {...suggestedNodesProps} />}
!loadingChannelPubkeys.length && !currentChannels.length &&
<SuggestedNodes {...suggestedNodesProps} />
}
{ {(loadingChannelPubkeys.length || channels.length) && (
(loadingChannelPubkeys.length > 0 || currentChannels.length) > 0 &&
<header className={styles.listHeader}> <header className={styles.listHeader}>
<section> <section>
<h2 onClick={toggleFilterPulldown} className={styles.filterTitle}> <h2 onClick={toggleFilterPulldown} className={styles.filterTitle}>
{filter.name} <span className={filterPulldown && styles.pulldown}><FaAngleDown /></span> {filter.name}{' '}
<span className={filterPulldown && styles.pulldown}>
<FaAngleDown />
</span>
</h2> </h2>
<ul className={`${styles.filters} ${filterPulldown && styles.active}`}> <ul className={`${styles.filters} ${filterPulldown && styles.active}`}>
{ {nonActiveFilters.map(f => (
nonActiveFilters.map(f => (
<li key={f.key} onClick={() => changeFilter(f)}> <li key={f.key} onClick={() => changeFilter(f)}>
{f.name} {f.name}
</li> </li>
)) ))}
}
</ul> </ul>
</section> </section>
<section className={styles.refreshContainer}> <section className={styles.refreshContainer}>
<span className={styles.refresh} onClick={refreshClicked} ref={(ref) => { this.repeat = ref }}> <span
{ className={styles.refresh}
this.state.refreshing ? onClick={refreshClicked}
<FaRepeat /> ref={ref => {
: this.repeat = ref
'Refresh' }}
} >
{this.state.refreshing ? <FaRepeat /> : 'Refresh'}
</span> </span>
</section> </section>
</header> </header>
} )}
<ul className={filterPulldown && styles.fade}> <ul className={filterPulldown && styles.fade}>
{ {loadingChannelPubkeys.length &&
loadingChannelPubkeys.length > 0 && loadingChannelPubkeys.map((loadingPubkey) => { loadingChannelPubkeys.map(loadingPubkey => {
// TODO(jimmymow): refactor this out. same logic is in displayNodeName above // TODO(jimmymow): refactor this out. same logic is in displayNodeName above
const node = find(nodes, n => loadingPubkey === n.pub_key) const node = find(nodes, n => loadingPubkey === n.pub_key)
const nodeDisplay = () => { const nodeDisplay = () => {
if (node && node.alias.length) { return node.alias } if (node && node.alias.length) {
return node.alias
}
return loadingPubkey.substring(0, 10) return loadingPubkey.substring(0, 10)
} }
@ -192,17 +194,16 @@ class Network extends Component {
return ( return (
<li key={loadingPubkey} className={styles.channel}> <li key={loadingPubkey} className={styles.channel}>
<section className={styles.channelTitle}> <section className={styles.channelTitle}>
<span className={`${styles.loading} hint--left`} data-hint='loading'> <span className={`${styles.loading} hint--left`} data-hint="loading">
<i className={styles.spinner} /> <i className={styles.spinner} />
</span> </span>
<span>{nodeDisplay()}</span> <span>{nodeDisplay()}</span>
</section> </section>
</li> </li>
) )
}) })}
} {currentChannels.length &&
{ currentChannels.map((channelObj, index) => {
currentChannels.length > 0 && currentChannels.map((channelObj, index) => {
const channel = Object.prototype.hasOwnProperty.call(channelObj, 'channel') ? channelObj.channel : channelObj const channel = Object.prototype.hasOwnProperty.call(channelObj, 'channel') ? channelObj.channel : channelObj
const pubkey = channel.remote_node_pub || channel.remote_pubkey const pubkey = channel.remote_node_pub || channel.remote_pubkey
@ -214,22 +215,20 @@ class Network extends Component {
> >
<section className={styles.channelTitle}> <section className={styles.channelTitle}>
<span className={`${styles[channelStatus(channelObj)]} hint--right`} data-hint={channelStatus(channelObj)}> <span className={`${styles[channelStatus(channelObj)]} hint--right`} data-hint={channelStatus(channelObj)}>
{ {closingChannelIds.includes(channel.chan_id) ? (
closingChannelIds.includes(channel.chan_id) ?
<span className={styles.loading}> <span className={styles.loading}>
<i className={`${styles.spinner} ${styles.closing}`} /> <i className={`${styles.spinner} ${styles.closing}`} />
</span> </span>
: ) : (
<FaCircle /> <FaCircle />
} )}
</span> </span>
<span>{displayNodeName(channel)}</span> <span>{displayNodeName(channel)}</span>
{ {selectedChannel === channel && (
selectedChannel === channel && <span onClick={() => blockExplorer.showTransaction(network, channel.channel_point.split(':')[0])}>
<span onClick={() => blockExplorer.showTransaction(isTestnet, channel.channel_point.split(':')[0])}>
<FaExternalLink /> <FaExternalLink />
</span> </span>
} )}
</section> </section>
<section className={styles.channelDetails}> <section className={styles.channelDetails}>
@ -241,65 +240,54 @@ class Network extends Component {
<section> <section>
<h5>Pay Limit</h5> <h5>Pay Limit</h5>
<p> <p>
<Value <Value value={channel.local_balance} currency={ticker.currency} currentTicker={currentTicker} />
value={channel.local_balance}
currency={ticker.currency}
currentTicker={currentTicker}
/>
<i> {ticker.currency.toUpperCase()}</i> <i> {ticker.currency.toUpperCase()}</i>
</p> </p>
</section> </section>
<section> <section>
<h5>Request Limit</h5> <h5>Request Limit</h5>
<p> <p>
<Value <Value value={channel.remote_balance} currency={ticker.currency} currentTicker={currentTicker} />
value={channel.remote_balance}
currency={ticker.currency}
currentTicker={currentTicker}
/>
<i>{ticker.currency.toUpperCase()}</i> <i>{ticker.currency.toUpperCase()}</i>
</p> </p>
</section> </section>
</div> </div>
<div className={styles.actions}> <div className={styles.actions}>
{ {closingChannelIds.includes(channel.chan_id) && (
closingChannelIds.includes(channel.chan_id) &&
<section> <section>
<span className={`${styles.loading} hint--left`} data-hint='closing'> <span className={`${styles.loading} hint--left`} data-hint="closing">
<i>Closing</i> <i className={`${styles.spinner} ${styles.closing}`} /> <i>Closing</i> <i className={`${styles.spinner} ${styles.closing}`} />
</span> </span>
</section> </section>
} )}
{ {Object.prototype.hasOwnProperty.call(channel, 'active') &&
(Object.prototype.hasOwnProperty.call(channel, 'active') && !closingChannelIds.includes(channel.chan_id)) && !closingChannelIds.includes(channel.chan_id) && (
<section onClick={() => removeClicked(channel)}> <section onClick={() => removeClicked(channel)}>
<div>Disconnect</div> <div>Disconnect</div>
</section> </section>
} )}
</div> </div>
</section> </section>
</li> </li>
) )
}) })}
}
</ul> </ul>
</div> </div>
{ {(loadingChannelPubkeys.length || channels.length) && (
(loadingChannelPubkeys.length > 0 || currentChannels.length) > 0 &&
<footer className={styles.search}> <footer className={styles.search}>
<label htmlFor='search' className={`${styles.label} ${styles.input}`}> <label htmlFor="search" className={`${styles.label} ${styles.input}`}>
<Isvg src={search} /> <Isvg src={search} />
</label> </label>
<input <input
id='search' id="search"
type='text' type="text"
className={`${styles.text} ${styles.input}`} className={`${styles.text} ${styles.input}`}
placeholder='search by alias or pubkey' placeholder="search by alias or pubkey"
value={searchQuery} value={searchQuery}
onChange={event => updateChannelSearchQuery(event.target.value)} onChange={event => updateChannelSearchQuery(event.target.value)}
/> />
</footer> </footer>
} )}
</div> </div>
) )
} }
@ -316,7 +304,7 @@ Network.propTypes = {
ticker: PropTypes.object.isRequired, ticker: PropTypes.object.isRequired,
suggestedNodesProps: PropTypes.object.isRequired, suggestedNodesProps: PropTypes.object.isRequired,
isTestnet: PropTypes.bool.isRequired, network: PropTypes.object.isRequired,
fetchChannels: PropTypes.func.isRequired, fetchChannels: PropTypes.func.isRequired,
openContactsForm: PropTypes.func.isRequired, openContactsForm: PropTypes.func.isRequired,

46
app/components/Contacts/SubmitChannelForm.js

@ -38,7 +38,9 @@ class SubmitChannelForm extends React.Component {
const formSubmitted = () => { const formSubmitted = () => {
// dont submit to LND if they havent set channel capacity amount // dont submit to LND if they havent set channel capacity amount
if (contactCapacity <= 0) { return } if (contactCapacity <= 0) {
return
}
// submit the channel to LND // submit the channel to LND
openChannel({ pubkey: node.pub_key, host: node.addresses[0].addr, local_amt: contactCapacity }) openChannel({ pubkey: node.pub_key, host: node.addresses[0].addr, local_amt: contactCapacity })
@ -55,8 +57,8 @@ class SubmitChannelForm extends React.Component {
<header className={styles.header}> <header className={styles.header}>
<h1>Add Funds to Network</h1> <h1>Add Funds to Network</h1>
<p> <p>
Adding a connection will help you send and receive money on the Lightning Network. Adding a connection will help you send and receive money on the Lightning Network. You aren&apos;t spening any money, rather moving the
You aren&apos;t spening any money, rather moving the money you plan to use onto the network. money you plan to use onto the network.
</p> </p>
</header> </header>
@ -67,37 +69,36 @@ class SubmitChannelForm extends React.Component {
<section className={styles.amount}> <section className={styles.amount}>
<div className={styles.input}> <div className={styles.input}>
<input <input
type='number' type="number"
min='0' min="0"
size='' size=""
placeholder='0.00000000' placeholder="0.00000000"
value={contactCapacity || ''} value={contactCapacity || ''}
onChange={event => updateContactCapacity(event.target.value)} onChange={event => updateContactCapacity(event.target.value)}
id='amount' id="amount"
/> />
<div className={styles.currency}> <div className={styles.currency}>
<section className={styles.currentCurrency} onClick={() => setContactsCurrencyFilters(!showCurrencyFilters)}> <section className={styles.currentCurrency} onClick={() => setContactsCurrencyFilters(!showCurrencyFilters)}>
<span>{currencyName}</span><span><FaAngleDown /></span> <span>{currencyName}</span>
<span>
<FaAngleDown />
</span>
</section> </section>
<ul className={showCurrencyFilters && styles.active}> <ul className={showCurrencyFilters && styles.active}>
{ {currentCurrencyFilters.map(filter => (
currentCurrencyFilters.map(filter => <li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>
<li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>{filter.name}</li>) {filter.name}
} </li>
))}
</ul> </ul>
</div> </div>
</div> </div>
<div className={styles.usdAmount}> <div className={styles.usdAmount}>{`${contactFormUsdAmount || 0} USD`}</div>
{`${contactFormUsdAmount || 0} USD`}
</div>
</section> </section>
<section className={styles.submit}> <section className={styles.submit}>
<div <div className={`${styles.button} ${contactCapacity > 0 && styles.active}`} onClick={formSubmitted}>
className={`${styles.button} ${contactCapacity > 0 && styles.active}`}
onClick={formSubmitted}
>
Submit Submit
</div> </div>
</section> </section>
@ -111,10 +112,7 @@ SubmitChannelForm.propTypes = {
closeContactsForm: PropTypes.func.isRequired, closeContactsForm: PropTypes.func.isRequired,
node: PropTypes.object.isRequired, node: PropTypes.object.isRequired,
contactCapacity: PropTypes.PropTypes.oneOfType([ contactCapacity: PropTypes.PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
PropTypes.number,
PropTypes.string
]),
updateContactCapacity: PropTypes.func.isRequired, updateContactCapacity: PropTypes.func.isRequired,
openChannel: PropTypes.func.isRequired, openChannel: PropTypes.func.isRequired,

20
app/components/Contacts/SubmitChannelForm.scss

@ -4,9 +4,11 @@
padding: 0 40px; padding: 0 40px;
font-family: Roboto; font-family: Roboto;
color: $white; color: $white;
margin: 0 auto;
width: 500px;
.header { .header {
padding: 20px 100px; padding: 20px;
h1 { h1 {
margin-bottom: 15px; margin-bottom: 15px;
@ -35,13 +37,14 @@
} }
.title { .title {
margin: 50px 0; margin: 20px 0;
line-height: 32px;
h2 { h2 {
font-size: 14px; font-size: 14px;
background: $spaceblue; background: $spaceblue;
padding: 10px; padding: 5px;
border-radius: 17.5px; border-radius: 0px;
display: inline; display: inline;
} }
} }
@ -52,15 +55,17 @@
align-items: center; align-items: center;
input { input {
font-size: 40px; font-size: 20px;
max-width: 230px; max-width: 110px;
} }
} }
.input input { .input input {
background: transparent; background: transparent;
outline: none; outline: none;
border: 0; border:1px solid #404040;
border-radius: 4px;
padding: 15px;
color: $gold; color: $gold;
-webkit-text-fill-color: $white; -webkit-text-fill-color: $white;
width: 100%; width: 100%;
@ -77,6 +82,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding-left: 10px;
.currentCurrency { .currentCurrency {
cursor: pointer; cursor: pointer;

19
app/components/Contacts/SuggestedNodes.js

@ -2,13 +2,8 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styles from './SuggestedNodes.scss' import styles from './SuggestedNodes.scss'
const SuggestedNodes = ({ const SuggestedNodes = ({ suggestedNodesLoading, suggestedNodes, setNode, openSubmitChannelForm }) => {
suggestedNodesLoading, const nodeClicked = n => {
suggestedNodes,
setNode,
openSubmitChannelForm
}) => {
const nodeClicked = (n) => {
// set the node public key for the submit form // set the node public key for the submit form
setNode({ pub_key: n.pubkey, addresses: [{ addr: n.host }] }) setNode({ pub_key: n.pubkey, addresses: [{ addr: n.host }] })
// open the submit form // open the submit form
@ -26,13 +21,10 @@ const SuggestedNodes = ({
return ( return (
<div className={styles.container}> <div className={styles.container}>
<header> <header>Hmmm, looks like you don&apos;t have any channels yet. Here are some suggested nodes to open a channel with to get started</header>
{'Hmmm, looks like you don\'t have any channels yet. Here are some suggested nodes to open a channel with to get started'}
</header>
<ul className={styles.suggestedNodes}> <ul className={styles.suggestedNodes}>
{ {suggestedNodes.map(node => (
suggestedNodes.map(node => (
<li key={node.pubkey}> <li key={node.pubkey}>
<section> <section>
<span>{node.nickname}</span> <span>{node.nickname}</span>
@ -42,8 +34,7 @@ const SuggestedNodes = ({
<span onClick={() => nodeClicked(node)}>Connect</span> <span onClick={() => nodeClicked(node)}>Connect</span>
</section> </section>
</li> </li>
)) ))}
}
</ul> </ul>
</div> </div>
) )

7
app/components/CurrencyIcon/CurrencyIcon.js

@ -3,10 +3,9 @@ import PropTypes from 'prop-types'
import { FaDollar } from 'react-icons/lib/fa' import { FaDollar } from 'react-icons/lib/fa'
import CryptoIcon from '../CryptoIcon' import CryptoIcon from '../CryptoIcon'
const CurrencyIcon = ({ currency, crypto, styles }) => (currency === 'usd' ? const CurrencyIcon = ({ currency, crypto, styles }) => {
<FaDollar style={styles} /> return currency === 'usd' ? <FaDollar style={styles} /> : <CryptoIcon styles={styles} currency={crypto} />
: }
<CryptoIcon styles={styles} currency={crypto} />)
CurrencyIcon.propTypes = { CurrencyIcon.propTypes = {
currency: PropTypes.string.isRequired, currency: PropTypes.string.isRequired,

5
app/components/Form/Form.js

@ -15,7 +15,9 @@ const FORM_TYPES = {
} }
const Form = ({ formType, formProps, closeForm }) => { const Form = ({ formType, formProps, closeForm }) => {
if (!formType) { return null } if (!formType) {
return null
}
const FormComponent = FORM_TYPES[formType] const FormComponent = FORM_TYPES[formType]
return ( return (
@ -30,7 +32,6 @@ const Form = ({ formType, formProps, closeForm }) => {
) )
} }
Form.propTypes = { Form.propTypes = {
formType: PropTypes.string, formType: PropTypes.string,
formProps: PropTypes.object.isRequired, formProps: PropTypes.object.isRequired,

106
app/components/Form/Pay.js

@ -14,26 +14,26 @@ import styles from './Pay.scss'
class Pay extends Component { class Pay extends Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { const {
isOnchain, isLn, payform: { payInput }, fetchInvoice isOnchain,
isLn,
payform: { payInput },
fetchInvoice
} = this.props } = this.props
// If on-chain, focus on amount to let user know it's editable // If on-chain, focus on amount to let user know it's editable
if (isOnchain) { this.amountInput.focus() } if (isOnchain) {
this.amountInput.focus()
}
// If LN go retrieve invoice details // If LN go retrieve invoice details
if ((prevProps.payform.payInput !== payInput) && isLn) { if (prevProps.payform.payInput !== payInput && isLn) {
fetchInvoice(payInput) fetchInvoice(payInput)
} }
} }
render() { render() {
const { const {
payform: { payform: { payInput, showErrors, invoice, showCurrencyFilters },
payInput,
showErrors,
invoice,
showCurrencyFilters
},
nodes, nodes,
ticker, ticker,
@ -58,15 +58,17 @@ class Pay extends Component {
setCurrency setCurrency
} = this.props } = this.props
const displayNodeName = (pubkey) => { const displayNodeName = pubkey => {
const node = find(nodes, n => n.pub_key === pubkey) const node = find(nodes, n => n.pub_key === pubkey)
if (node && node.alias.length) { return node.alias } if (node && node.alias.length) {
return node.alias
}
return pubkey ? pubkey.substring(0, 10) : '' return pubkey ? pubkey.substring(0, 10) : ''
} }
const onCurrencyFilterClick = (currency) => { const onCurrencyFilterClick = currency => {
if (!isLn) { if (!isLn) {
// change the input amount // change the input amount
setPayAmount(btc.convert(ticker.currency, currency, currentAmount)) setPayAmount(btc.convert(ticker.currency, currency, currentAmount))
@ -86,85 +88,87 @@ class Pay extends Component {
<div className={styles.content}> <div className={styles.content}>
<section className={styles.destination}> <section className={styles.destination}>
<div className={styles.top}> <div className={styles.top}>
<label htmlFor='paymentRequest'>Destination</label> <label htmlFor="paymentRequest">Destination</label>
<span className={`${styles.description} ${(isOnchain || isLn) && styles.active}`}> <span className={`${styles.description} ${(isOnchain || isLn) && styles.active}`}>
{isOnchain && {isOnchain && (
<i> <i>
<Isvg src={link} /> <Isvg src={link} />
<span>On-Chain (~10 minutes)</span> <span>On-Chain (~10 minutes)</span>
</i> </i>
} )}
{isLn && {isLn && (
<i> <i>
<span> <span>
{displayNodeName(invoice.destination)} ({invoice.description}) {displayNodeName(invoice.destination)} ({invoice.description})
</span> </span>
</i> </i>
} )}
</span> </span>
</div> </div>
<div className={styles.bottom}> <div className={styles.bottom}>
<textarea <textarea
type='text' type="text"
placeholder='Paste payment request or bitcoin address here' placeholder="Paste payment request or bitcoin address here"
value={payInput} value={payInput}
onChange={event => setPayInput(event.target.value)} onChange={event => setPayInput(event.target.value)}
onBlur={onPayInputBlur} onBlur={onPayInputBlur}
id='paymentRequest' id="paymentRequest"
rows='4' rows="4"
/> />
<section className={`${styles.errorMessage} ${showErrors.payInput && styles.active}`}> <section className={`${styles.errorMessage} ${showErrors.payInput && styles.active}`}>
{showErrors.payInput && {showErrors.payInput && <span>{errors.payInput}</span>}
<span>{errors.payInput}</span>
}
</section> </section>
</div> </div>
</section> </section>
<section className={styles.amount}> <section className={styles.amount}>
<div className={styles.top}> <div className={styles.top}>
<label htmlFor='amount'>Amount</label> <label htmlFor="amount">Amount</label>
<span /> <span />
</div> </div>
<div className={styles.bottom}> <div className={styles.bottom}>
<input <input
type='number' type="number"
min='0' min="0"
ref={(input) => { this.amountInput = input }} ref={input => {
size='' this.amountInput = input
placeholder='0.00000000' }}
size=""
placeholder="0.00000000"
value={currentAmount || ''} value={currentAmount || ''}
onChange={event => setPayAmount(event.target.value)} onChange={event => setPayAmount(event.target.value)}
onBlur={onPayAmountBlur} onBlur={onPayAmountBlur}
id='amount' id="amount"
readOnly={isLn} readOnly={isLn}
/> />
<div className={styles.currency}> <div className={styles.currency}>
<section className={styles.currentCurrency} onClick={() => setCurrencyFilters(!showCurrencyFilters)}> <section className={styles.currentCurrency} onClick={() => setCurrencyFilters(!showCurrencyFilters)}>
<span>{currencyName}</span><span><FaAngleDown /></span> <span>{currencyName}</span>
<span>
<FaAngleDown />
</span>
</section> </section>
<ul className={showCurrencyFilters && styles.active}> <ul className={showCurrencyFilters && styles.active}>
{ {currentCurrencyFilters.map(filter => (
currentCurrencyFilters.map(filter => <li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>
<li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>{filter.name}</li>) {filter.name}
} </li>
))}
</ul> </ul>
</div> </div>
</div> </div>
<div className={styles.usdAmount}> <div className={styles.usdAmount}>{`${usdAmount || 0} USD`}</div>
{`${usdAmount || 0} USD`}
</div>
<section className={`${styles.errorMessage} ${styles.amount} ${showErrors.amount && styles.active}`}> <section className={`${styles.errorMessage} ${styles.amount} ${showErrors.amount && styles.active}`}>
{showErrors.amount && {showErrors.amount && <span>{errors.amount}</span>}
<span>{errors.amount}</span>
}
</section> </section>
</section> </section>
<section className={styles.submit}> <section className={styles.submit}>
<div className={`${styles.button} ${isValid && styles.active}`} onClick={onPaySubmit}>Pay</div> <div className={`${styles.button} ${isValid && styles.active}`} onClick={onPaySubmit}>
Pay
</div>
</section> </section>
</div> </div>
</div> </div>
@ -172,13 +176,9 @@ class Pay extends Component {
} }
} }
Pay.propTypes = { Pay.propTypes = {
payform: PropTypes.shape({ payform: PropTypes.shape({
amount: PropTypes.oneOfType([ amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
PropTypes.string,
PropTypes.number
]),
payInput: PropTypes.string.isRequired, payInput: PropTypes.string.isRequired,
invoice: PropTypes.object.isRequired, invoice: PropTypes.object.isRequired,
showErrors: PropTypes.object.isRequired showErrors: PropTypes.object.isRequired
@ -187,14 +187,8 @@ Pay.propTypes = {
isOnchain: PropTypes.bool.isRequired, isOnchain: PropTypes.bool.isRequired,
isLn: PropTypes.bool.isRequired, isLn: PropTypes.bool.isRequired,
currentAmount: PropTypes.oneOfType([ currentAmount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
PropTypes.string, usdAmount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
PropTypes.number
]),
usdAmount: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
payFormIsValid: PropTypes.shape({ payFormIsValid: PropTypes.shape({
errors: PropTypes.object, errors: PropTypes.object,
isValid: PropTypes.bool isValid: PropTypes.bool

48
app/components/Form/Request.js

@ -23,7 +23,7 @@ const Request = ({
onRequestSubmit onRequestSubmit
}) => { }) => {
const onCurrencyFilterClick = (currency) => { const onCurrencyFilterClick = currency => {
// change the input amount // change the input amount
setRequestAmount(btc.convert(ticker.currency, currency, amount)) setRequestAmount(btc.convert(ticker.currency, currency, amount))
@ -41,46 +41,42 @@ const Request = ({
<div className={styles.content}> <div className={styles.content}>
<section className={styles.amount}> <section className={styles.amount}>
<div className={styles.top}> <div className={styles.top}>
<label htmlFor='amount'>Amount</label> <label htmlFor="amount">Amount</label>
<span /> <span />
</div> </div>
<div className={styles.bottom}> <div className={styles.bottom}>
<input <input type="number" value={amount || ''} onChange={event => setRequestAmount(event.target.value)} id="amount" placeholder="0.00000000" />
type='number'
value={amount || ''}
onChange={event => setRequestAmount(event.target.value)}
id='amount'
placeholder='0.00000000'
/>
<div className={styles.currency}> <div className={styles.currency}>
<section className={styles.currentCurrency} onClick={() => setRequestCurrencyFilters(!showCurrencyFilters)}> <section className={styles.currentCurrency} onClick={() => setRequestCurrencyFilters(!showCurrencyFilters)}>
<span>{currencyName}</span><span><FaAngleDown /></span> <span>{currencyName}</span>
<span>
<FaAngleDown />
</span>
</section> </section>
<ul className={showCurrencyFilters && styles.active}> <ul className={showCurrencyFilters && styles.active}>
{ {currentCurrencyFilters.map(filter => (
currentCurrencyFilters.map(filter => <li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>
<li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>{filter.name}</li>) {filter.name}
} </li>
))}
</ul> </ul>
</div> </div>
</div> </div>
<div className={styles.usdAmount}> <div className={styles.usdAmount}>{`${requestUsdAmount || 0} USD`}</div>
{`${requestUsdAmount || 0} USD`}
</div>
</section> </section>
<section className={styles.memo}> <section className={styles.memo}>
<div className={styles.top}> <div className={styles.top}>
<label htmlFor='memo'>Memo</label> <label htmlFor="memo">Memo</label>
</div> </div>
<div className={styles.bottom}> <div className={styles.bottom}>
<input <input
type='text' type="text"
placeholder='Details about the request' placeholder="Details about the request"
value={memo} value={memo}
onChange={event => setRequestMemo(event.target.value)} onChange={event => setRequestMemo(event.target.value)}
id='memo' id="memo"
/> />
</div> </div>
</section> </section>
@ -97,17 +93,11 @@ const Request = ({
Request.propTypes = { Request.propTypes = {
requestform: PropTypes.shape({ requestform: PropTypes.shape({
amount: PropTypes.oneOfType([ amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
PropTypes.string,
PropTypes.number
]),
memo: PropTypes.string memo: PropTypes.string
}).isRequired, }).isRequired,
requestUsdAmount: PropTypes.oneOfType([ requestUsdAmount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
PropTypes.string,
PropTypes.number
]),
currencyName: PropTypes.string.isRequired, currencyName: PropTypes.string.isRequired,
currentCurrencyFilters: PropTypes.array.isRequired, currentCurrencyFilters: PropTypes.array.isRequired,

4
app/components/Onboarding/Alias.js

@ -5,8 +5,8 @@ import styles from './Alias.scss'
const Alias = ({ alias, updateAlias }) => ( const Alias = ({ alias, updateAlias }) => (
<div className={styles.container}> <div className={styles.container}>
<input <input
type='text' type="text"
placeholder='Satoshi' placeholder="Satoshi"
className={styles.alias} className={styles.alias}
ref={input => input && input.focus()} ref={input => input && input.focus()}
value={alias} value={alias}

28
app/components/Onboarding/ConnectionDetails.js

@ -2,16 +2,14 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styles from './ConnectionDetails.scss' import styles from './ConnectionDetails.scss'
const ConnectionDetails = ({ const ConnectionDetails = ({ connectionHost, connectionCert, connectionMacaroon, setConnectionHost, setConnectionCert, setConnectionMacaroon }) => (
connectionHost, connectionCert, connectionMacaroon, setConnectionHost, setConnectionCert, setConnectionMacaroon
}) => (
<div className={styles.container}> <div className={styles.container}>
<div> <div>
<label htmlFor='connectionHost'>Host:</label> <label htmlFor="connectionHost">Host:</label>
<input <input
type='text' type="text"
id='connectionHost' id="connectionHost"
placeholder='Hostname / Port of the Lnd gRPC interface' placeholder="Hostname / Port of the Lnd gRPC interface"
className={styles.host} className={styles.host}
ref={input => input} ref={input => input}
value={connectionHost} value={connectionHost}
@ -19,11 +17,11 @@ const ConnectionDetails = ({
/> />
</div> </div>
<div> <div>
<label htmlFor='connectionCert'>TLS Certificate:</label> <label htmlFor="connectionCert">TLS Certificate:</label>
<input <input
type='text' type="text"
id='connectionCert' id="connectionCert"
placeholder='Path to the lnd tls cert' placeholder="Path to the lnd tls cert"
className={styles.cert} className={styles.cert}
ref={input => input} ref={input => input}
value={connectionCert} value={connectionCert}
@ -31,11 +29,11 @@ const ConnectionDetails = ({
/> />
</div> </div>
<div> <div>
<label htmlFor='connectionMacaroon'>Macaroon:</label> <label htmlFor="connectionMacaroon">Macaroon:</label>
<input <input
type='text' type="text"
id='connectionMacaroon' id="connectionMacaroon"
placeholder='Path to the lnd macaroon file' placeholder="Path to the lnd macaroon file"
className={styles.macaroon} className={styles.macaroon}
ref={input => input} ref={input => input}
value={connectionMacaroon} value={connectionMacaroon}

9
app/components/Onboarding/InitWallet.js

@ -5,14 +5,7 @@ import Signup from './Signup'
import styles from './InitWallet.scss' import styles from './InitWallet.scss'
const InitWallet = ({ hasSeed, loginProps, signupProps }) => ( const InitWallet = ({ hasSeed, loginProps, signupProps }) => (
<div className={styles.container}> <div className={styles.container}>{hasSeed ? <Login {...loginProps} /> : <Signup {...signupProps} />}</div>
{
hasSeed ?
<Login {...loginProps} />
:
<Signup {...signupProps} />
}
</div>
) )
InitWallet.propTypes = { InitWallet.propTypes = {

23
app/components/Onboarding/Login.js

@ -2,35 +2,22 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styles from './Login.scss' import styles from './Login.scss'
const Login = ({ const Login = ({ password, updatePassword, unlockingWallet, unlockWallet, unlockWalletError }) => (
password,
updatePassword,
unlockingWallet,
unlockWallet,
unlockWalletError
}) => (
<div className={styles.container}> <div className={styles.container}>
<input <input
type='password' type="password"
placeholder='Password' placeholder="Password"
className={`${styles.password} ${unlockWalletError.isError && styles.inputError}`} className={`${styles.password} ${unlockWalletError.isError && styles.inputError}`}
ref={input => input && input.focus()} ref={input => input && input.focus()}
value={password} value={password}
onChange={event => updatePassword(event.target.value)} onChange={event => updatePassword(event.target.value)}
/> />
<p className={`${unlockWalletError.isError && styles.active} ${styles.error}`}> <p className={`${unlockWalletError.isError && styles.active} ${styles.error}`}>{unlockWalletError.message}</p>
{unlockWalletError.message}
</p>
<section className={styles.buttons}> <section className={styles.buttons}>
<div> <div>
<span className={`${!unlockingWallet && styles.active} ${styles.button}`} onClick={() => unlockWallet(password)}> <span className={`${!unlockingWallet && styles.active} ${styles.button}`} onClick={() => unlockWallet(password)}>
{ {unlockingWallet ? <i className={styles.spinner} /> : 'Unlock'}
unlockingWallet ?
<i className={styles.spinner} />
:
'Unlock'
}
</span> </span>
</div> </div>
</section> </section>

8
app/components/Onboarding/NewAezeedPassword.js

@ -12,8 +12,8 @@ const NewAezeedPassword = ({
<div className={styles.container}> <div className={styles.container}>
<section className={styles.input}> <section className={styles.input}>
<input <input
type='password' type="password"
placeholder='Password' placeholder="Password"
className={styles.password} className={styles.password}
value={aezeedPassword} value={aezeedPassword}
onChange={event => updateAezeedPassword(event.target.value)} onChange={event => updateAezeedPassword(event.target.value)}
@ -22,8 +22,8 @@ const NewAezeedPassword = ({
<section className={styles.input}> <section className={styles.input}>
<input <input
type='password' type="password"
placeholder='Confirm Password' placeholder="Confirm Password"
className={`${styles.password} ${showAezeedPasswordConfirmationError && styles.error}`} className={`${styles.password} ${showAezeedPasswordConfirmationError && styles.error}`}
value={aezeedPasswordConfirmation} value={aezeedPasswordConfirmation}
onChange={event => updateAezeedPasswordConfirmation(event.target.value)} onChange={event => updateAezeedPasswordConfirmation(event.target.value)}

8
app/components/Onboarding/NewWalletPassword.js

@ -12,8 +12,8 @@ const NewWalletPassword = ({
<div className={styles.container}> <div className={styles.container}>
<section className={styles.input}> <section className={styles.input}>
<input <input
type='password' type="password"
placeholder='Password' placeholder="Password"
className={styles.password} className={styles.password}
value={createWalletPassword} value={createWalletPassword}
onChange={event => updateCreateWalletPassword(event.target.value)} onChange={event => updateCreateWalletPassword(event.target.value)}
@ -22,8 +22,8 @@ const NewWalletPassword = ({
<section className={styles.input}> <section className={styles.input}>
<input <input
type='password' type="password"
placeholder='Confirm Password' placeholder="Confirm Password"
className={`${styles.password} ${showCreateWalletPasswordConfirmationError && styles.error}`} className={`${styles.password} ${showCreateWalletPasswordConfirmationError && styles.error}`}
value={createWalletPasswordConfirmation} value={createWalletPasswordConfirmation}
onChange={event => updateCreateWalletPasswordConfirmation(event.target.value)} onChange={event => updateCreateWalletPasswordConfirmation(event.target.value)}

6
app/components/Onboarding/NewWalletSeed.js

@ -5,8 +5,7 @@ import styles from './NewWalletSeed.scss'
const NewWalletSeed = ({ seed }) => ( const NewWalletSeed = ({ seed }) => (
<div className={styles.container}> <div className={styles.container}>
<ul className={styles.seedContainer}> <ul className={styles.seedContainer}>
{ {seed.map((word, index) => (
seed.map((word, index) => (
<li key={index}> <li key={index}>
<section> <section>
<label htmlFor={word}>{index + 1}</label> <label htmlFor={word}>{index + 1}</label>
@ -15,8 +14,7 @@ const NewWalletSeed = ({ seed }) => (
<span>{word}</span> <span>{word}</span>
</section> </section>
</li> </li>
)) ))}
}
</ul> </ul>
</div> </div>
) )

54
app/components/Onboarding/Onboarding.js

@ -51,12 +51,12 @@ const Onboarding = ({
case 0.1: case 0.1:
return ( return (
<FormContainer <FormContainer
title='How do you want to connect to the Lightning Network?' title="How do you want to connect to the Lightning Network?"
description=' description="
By default Zap will spin up a node for you and handle all the nerdy stuff By default Zap will spin up a node for you and handle all the nerdy stuff
in the background. However you can also setup a custom node connection and in the background. However you can also setup a custom node connection and
use Zap to control a remote node if you desire (for advanced users). use Zap to control a remote node if you desire (for advanced users).
' "
back={null} back={null}
next={() => changeStep(connectionType === 'local' ? 1 : 0.2)} next={() => changeStep(connectionType === 'local' ? 1 : 0.2)}
> >
@ -67,8 +67,8 @@ const Onboarding = ({
case 0.2: case 0.2:
return ( return (
<FormContainer <FormContainer
title='Connection details' title="Connection details"
description='Enter the connection details for your Lightning node.' description="Enter the connection details for your Lightning node."
back={() => changeStep(0.1)} back={() => changeStep(0.1)}
next={() => next={() =>
startLnd({ startLnd({
@ -86,8 +86,8 @@ const Onboarding = ({
case 1: case 1:
return ( return (
<FormContainer <FormContainer
title='What should we call you?' title="What should we call you?"
description='Set your nickname to help others connect with you on the Lightning Network' description="Set your nickname to help others connect with you on the Lightning Network"
back={() => changeStep(0.1)} back={() => changeStep(0.1)}
next={() => changeStep(2)} next={() => changeStep(2)}
> >
@ -97,8 +97,8 @@ const Onboarding = ({
case 2: case 2:
return ( return (
<FormContainer <FormContainer
title='Autopilot' title="Autopilot"
description='Autopilot is an automatic network manager. Instead of manually adding people to build your network to make payments, enable autopilot to automatically connect you to the Lightning Network using 60% of your balance.' // eslint-disable-line description="Autopilot is an automatic network manager. Instead of manually adding people to build your network to make payments, enable autopilot to automatically connect you to the Lightning Network using 60% of your balance." // eslint-disable-line
back={() => changeStep(1)} back={() => changeStep(1)}
next={() => startLnd({ connectionType, alias, autopilot })} next={() => startLnd({ connectionType, alias, autopilot })}
> >
@ -108,8 +108,8 @@ const Onboarding = ({
case 3: case 3:
return ( return (
<FormContainer <FormContainer
title='Welcome back!' title="Welcome back!"
description='Enter your wallet password or create a new wallet' // eslint-disable-line description="Enter your wallet password or create a new wallet" // eslint-disable-line
back={null} back={null}
next={null} next={null}
> >
@ -119,8 +119,8 @@ const Onboarding = ({
case 4: case 4:
return ( return (
<FormContainer <FormContainer
title='Welcome!' title="Welcome!"
description='Looks like you are new here. Set a password to encrypt your wallet. This password will be needed to unlock Zap in the future' // eslint-disable-line description="Looks like you are new here. Set a password to encrypt your wallet. This password will be needed to unlock Zap in the future" // eslint-disable-line
back={null} back={null}
next={() => { next={() => {
// dont allow the user to move on if the confirmation password doesnt match the original password // dont allow the user to move on if the confirmation password doesnt match the original password
@ -137,8 +137,8 @@ const Onboarding = ({
case 5: case 5:
return ( return (
<FormContainer <FormContainer
title={'Alright, let\'s get set up'} title={"Alright, let's get set up"}
description='Would you like to create a new wallet or import an existing one?' // eslint-disable-line description="Would you like to create a new wallet or import an existing one?" // eslint-disable-line
back={() => changeStep(4)} back={() => changeStep(4)}
next={() => (initWalletProps.signupProps.signupForm.create ? changeStep(6) : changeStep(5.1))} next={() => (initWalletProps.signupProps.signupForm.create ? changeStep(6) : changeStep(5.1))}
> >
@ -148,8 +148,8 @@ const Onboarding = ({
case 5.1: case 5.1:
return ( return (
<FormContainer <FormContainer
title='Import your seed' title="Import your seed"
description={'Recovering a wallet, nice. You don\'t need anyone else, you got yourself :)'} // eslint-disable-line description={"Recovering a wallet, nice. You don't need anyone else, you got yourself :)"} // eslint-disable-line
back={() => changeStep(5)} back={() => changeStep(5)}
next={() => changeStep(5.2)} next={() => changeStep(5.2)}
> >
@ -159,8 +159,8 @@ const Onboarding = ({
case 5.2: case 5.2:
return ( return (
<FormContainer <FormContainer
title='Seed passphrase' title="Seed passphrase"
description={'Enter your cipherseed passphrase (or just submit if you don\'t have one)'} // eslint-disable-line description={"Enter your cipherseed passphrase (or just submit if you don't have one)"} // eslint-disable-line
back={() => changeStep(5)} back={() => changeStep(5)}
next={() => { next={() => {
const recoverySeed = recoverFormProps.seedInput.map(input => input.word) const recoverySeed = recoverFormProps.seedInput.map(input => input.word)
@ -174,8 +174,8 @@ const Onboarding = ({
case 6: case 6:
return ( return (
<FormContainer <FormContainer
title='Save your wallet seed' title="Save your wallet seed"
description='Please save these 24 words securely! This will allow you to recover your wallet in the future' // eslint-disable-line description="Please save these 24 words securely! This will allow you to recover your wallet in the future" // eslint-disable-line
back={() => changeStep(5)} back={() => changeStep(5)}
next={() => changeStep(7)} next={() => changeStep(7)}
> >
@ -185,8 +185,8 @@ const Onboarding = ({
case 7: case 7:
return ( return (
<FormContainer <FormContainer
title='Re-enter your seed' title="Re-enter your seed"
description='Yeah I know, might be annoying, but just to be safe!' // eslint-disable-line description="Yeah I know, might be annoying, but just to be safe!" // eslint-disable-line
back={() => changeStep(6)} back={() => changeStep(6)}
next={() => { next={() => {
// don't allow them to move on if they havent re-entered the seed correctly // don't allow them to move on if they havent re-entered the seed correctly
@ -203,12 +203,14 @@ const Onboarding = ({
case 8: case 8:
return ( return (
<FormContainer <FormContainer
title='Encrypt your seed' title="Encrypt your seed"
description='Totally optional, but we encourage it. Set a password that will be used to encrypt your wallet seed' // eslint-disable-line description="Totally optional, but we encourage it. Set a password that will be used to encrypt your wallet seed" // eslint-disable-line
back={() => changeStep(6)} back={() => changeStep(6)}
next={() => { next={() => {
// dont allow the user to move on if the confirmation password doesnt match the original password // dont allow the user to move on if the confirmation password doesnt match the original password
if (newAezeedPasswordProps.showAezeedPasswordConfirmationError) { return } if (newAezeedPasswordProps.showAezeedPasswordConfirmationError) {
return
}
submitNewWallet(createWalletPassword, seed, aezeedPassword) submitNewWallet(createWalletPassword, seed, aezeedPassword)
}} }}

11
app/components/Onboarding/ReEnterSeed.js

@ -5,30 +5,27 @@ import styles from './ReEnterSeed.scss'
const ReEnterSeed = ({ seed, seedInput, updateSeedInput }) => ( const ReEnterSeed = ({ seed, seedInput, updateSeedInput }) => (
<div className={styles.container}> <div className={styles.container}>
<ul className={styles.seedContainer}> <ul className={styles.seedContainer}>
{ {seed.map((word, index) => (
seed.map((word, index) => (
<li key={index}> <li key={index}>
<section> <section>
<label htmlFor={word}>{index + 1}</label> <label htmlFor={word}>{index + 1}</label>
</section> </section>
<section> <section>
<input <input
type='text' type="text"
id={word} id={word}
placeholder='word' placeholder="word"
value={seedInput[index] ? seedInput[index].word : ''} value={seedInput[index] ? seedInput[index].word : ''}
onChange={event => updateSeedInput({ word: event.target.value, index })} onChange={event => updateSeedInput({ word: event.target.value, index })}
className={`${styles.word} ${seedInput[index] && word === seedInput[index].word ? styles.valid : styles.invalid}`} className={`${styles.word} ${seedInput[index] && word === seedInput[index].word ? styles.valid : styles.invalid}`}
/> />
</section> </section>
</li> </li>
)) ))}
}
</ul> </ul>
</div> </div>
) )
ReEnterSeed.propTypes = { ReEnterSeed.propTypes = {
seed: PropTypes.array.isRequired, seed: PropTypes.array.isRequired,
seedInput: PropTypes.array.isRequired, seedInput: PropTypes.array.isRequired,

13
app/components/Onboarding/RecoverForm.js

@ -5,30 +5,29 @@ import styles from './RecoverForm.scss'
const RecoverForm = ({ seedInput, updateSeedInput }) => ( const RecoverForm = ({ seedInput, updateSeedInput }) => (
<div className={styles.container}> <div className={styles.container}>
<ul className={styles.seedContainer}> <ul className={styles.seedContainer}>
{ {Array(24)
Array(24).fill('').map((word, index) => ( .fill('')
.map((word, index) => (
<li key={index}> <li key={index}>
<section> <section>
<label htmlFor={index}>{index + 1}</label> <label htmlFor={index}>{index + 1}</label>
</section> </section>
<section> <section>
<input <input
type='text' type="text"
id={index} id={index}
placeholder='word' placeholder="word"
value={seedInput[index] ? seedInput[index].word : ''} value={seedInput[index] ? seedInput[index].word : ''}
onChange={event => updateSeedInput({ word: event.target.value, index })} onChange={event => updateSeedInput({ word: event.target.value, index })}
className={styles.word} className={styles.word}
/> />
</section> </section>
</li> </li>
)) ))}
}
</ul> </ul>
</div> </div>
) )
RecoverForm.propTypes = { RecoverForm.propTypes = {
seedInput: PropTypes.array.isRequired, seedInput: PropTypes.array.isRequired,
updateSeedInput: PropTypes.func.isRequired updateSeedInput: PropTypes.func.isRequired

22
app/components/Onboarding/Signup.js

@ -7,28 +7,14 @@ const Signup = ({ signupForm, setSignupCreate, setSignupImport }) => (
<div className={styles.container}> <div className={styles.container}>
<section className={`${styles.enable} ${signupForm.create && styles.active}`}> <section className={`${styles.enable} ${signupForm.create && styles.active}`}>
<div onClick={setSignupCreate}> <div onClick={setSignupCreate}>
{ {signupForm.create ? <FaCircle /> : <FaCircleThin />}
signupForm.create ? <span className={styles.label}>Create new wallet</span>
<FaCircle />
:
<FaCircleThin />
}
<span className={styles.label}>
Create new wallet
</span>
</div> </div>
</section> </section>
<section className={`${styles.disable} ${signupForm.import && styles.active}`}> <section className={`${styles.disable} ${signupForm.import && styles.active}`}>
<div onClick={setSignupImport}> <div onClick={setSignupImport}>
{ {signupForm.import ? <FaCircle /> : <FaCircleThin />}
signupForm.import ? <span className={styles.label}>Import existing wallet</span>
<FaCircle />
:
<FaCircleThin />
}
<span className={styles.label}>
Import existing wallet
</span>
</div> </div>
</section> </section>
</div> </div>

6
app/components/Onboarding/Syncing.js

@ -4,7 +4,6 @@ import Isvg from 'react-inlinesvg'
import zapLogo from 'icons/zap_logo.svg' import zapLogo from 'icons/zap_logo.svg'
import styles from './Syncing.scss' import styles from './Syncing.scss'
class Syncing extends Component { class Syncing extends Component {
componentWillMount() { componentWillMount() {
this.props.fetchBlockHeight() this.props.fetchBlockHeight()
@ -36,10 +35,7 @@ class Syncing extends Component {
Syncing.propTypes = { Syncing.propTypes = {
fetchBlockHeight: PropTypes.func.isRequired, fetchBlockHeight: PropTypes.func.isRequired,
syncPercentage: PropTypes.oneOfType([ syncPercentage: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired
PropTypes.number,
PropTypes.string
]).isRequired
} }
export default Syncing export default Syncing

13
app/components/Value/Value.js

@ -3,18 +3,15 @@ import PropTypes from 'prop-types'
import { btc } from 'utils' import { btc } from 'utils'
const Value = ({ value, currency, currentTicker }) => { const Value = ({ value, currency, currentTicker }) => {
if (currency === 'sats') { return <i>{value > 0 ? value : value * -1}</i> } if (currency === 'sats') {
return <i>{value > 0 ? value : value * -1}</i>
}
return ( return <i>{btc.convert('sats', currency, value, currentTicker.price_usd)}</i>
<i>{btc.convert('sats', currency, value, currentTicker.price_usd)}</i>
)
} }
Value.propTypes = { Value.propTypes = {
value: PropTypes.oneOfType([ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
PropTypes.string,
PropTypes.number
]),
currency: PropTypes.string.isRequired, currency: PropTypes.string.isRequired,
currentTicker: PropTypes.object.isRequired currentTicker: PropTypes.object.isRequired
} }

40
app/components/Wallet/ReceiveModal.js

@ -20,7 +20,7 @@ class ReceiveModal extends React.Component {
} }
render() { render() {
const copyOnClick = (data) => { const copyOnClick = data => {
copy(data) copy(data)
showNotification('Noice', 'Successfully copied to clipboard') showNotification('Noice', 'Successfully copied to clipboard')
} }
@ -33,17 +33,13 @@ class ReceiveModal extends React.Component {
} }
} }
const { const { isOpen, pubkey, address, alias, closeReceiveModal, network } = this.props
isOpen,
pubkey,
address,
alias,
closeReceiveModal
} = this.props
const { qrCodeType } = this.state const { qrCodeType } = this.state
if (!isOpen) { return null } if (!isOpen) {
return null
}
return ( return (
<div className={styles.container}> <div className={styles.container}>
@ -59,20 +55,17 @@ class ReceiveModal extends React.Component {
<h2>{alias && alias.length ? alias : pubkey.substring(0, 10)}</h2> <h2>{alias && alias.length ? alias : pubkey.substring(0, 10)}</h2>
<div className={styles.qrCodeOptions}> <div className={styles.qrCodeOptions}>
<div className={qrCodeType === 1 && styles.active} onClick={changeQrCode}>Node Pubkey</div> <div className={qrCodeType === 1 && styles.active} onClick={changeQrCode}>
<div className={qrCodeType === 2 && styles.active} onClick={changeQrCode}>Bitcoin Address</div> Node Pubkey
</div>
<div className={qrCodeType === 2 && styles.active} onClick={changeQrCode}>
Bitcoin Address
</div>
</div> </div>
</header> </header>
<div className={styles.qrCodeContainer}> <div className={styles.qrCodeContainer}>
<QRCode <QRCode value={qrCodeType === 1 ? pubkey : address} renderAs="svg" size={150} bgColor="transparent" fgColor="white" level="L" />
value={qrCodeType === 1 ? pubkey : address}
renderAs='svg'
size={150}
bgColor='transparent'
fgColor='white'
level='L'
/>
</div> </div>
</section> </section>
<section className={styles.right}> <section className={styles.right}>
@ -80,17 +73,17 @@ class ReceiveModal extends React.Component {
<h4>Node Public Key</h4> <h4>Node Public Key</h4>
<p> <p>
<span className={styles.data}>{pubkey}</span> <span className={styles.data}>{pubkey}</span>
<span onClick={() => copyOnClick(pubkey)} className={`${styles.copy} hint--left`} data-hint='Copy pubkey'> <span onClick={() => copyOnClick(pubkey)} className={`${styles.copy} hint--left`} data-hint="Copy pubkey">
<Isvg src={copyIcon} /> <Isvg src={copyIcon} />
</span> </span>
</p> </p>
</div> </div>
<div className={styles.address}> <div className={styles.address}>
<h4>Bitcoin Address</h4> <h4>Bitcoin {network.name} Address</h4>
<p> <p>
<span className={styles.data}>{address}</span> <span className={styles.data}>{address}</span>
<span onClick={() => copyOnClick(address)} className={`${styles.copy} hint--left`} data-hint='Copy address'> <span onClick={() => copyOnClick(address)} className={`${styles.copy} hint--left`} data-hint="Copy address">
<Isvg src={copyIcon} /> <Isvg src={copyIcon} />
</span> </span>
</p> </p>
@ -103,6 +96,9 @@ class ReceiveModal extends React.Component {
} }
ReceiveModal.propTypes = { ReceiveModal.propTypes = {
network: PropTypes.shape({
name: PropTypes.string
}).isRequired,
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
pubkey: PropTypes.string, pubkey: PropTypes.string,
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,

50
app/components/Wallet/Wallet.js

@ -27,11 +27,11 @@ const Wallet = ({
currencyName, currencyName,
setCurrency, setCurrency,
setWalletCurrencyFilters, setWalletCurrencyFilters,
isTestnet network
}) => { }) => {
const usdAmount = btc.satoshisToUsd((parseInt(balance.walletBalance, 10) + parseInt(balance.channelBalance, 10)), currentTicker.price_usd) const usdAmount = btc.satoshisToUsd(parseInt(balance.walletBalance, 10) + parseInt(balance.channelBalance, 10), currentTicker.price_usd)
const onCurrencyFilterClick = (currency) => { const onCurrencyFilterClick = currency => {
setCurrency(currency) setCurrency(currency)
setWalletCurrencyFilters(false) setWalletCurrencyFilters(false)
} }
@ -73,10 +73,11 @@ const Wallet = ({
</span> </span>
<ul className={info.showWalletCurrencyFilters && styles.active}> <ul className={info.showWalletCurrencyFilters && styles.active}>
{ {currentCurrencyFilters.map(filter => (
currentCurrencyFilters.map(filter => <li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>
<li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>{filter.name}</li>) {filter.name}
} </li>
))}
</ul> </ul>
</section> </section>
</span> </span>
@ -87,36 +88,41 @@ const Wallet = ({
</div> </div>
<div className={styles.right}> <div className={styles.right}>
<div className={styles.rightContent}> <div className={styles.rightContent}>
<div className={styles.pay} onClick={openPayForm}>Pay</div> <div className={styles.pay} onClick={openPayForm}>
<div className={styles.request} onClick={openRequestForm}>Request</div> Pay
</div>
<div className={styles.request} onClick={openRequestForm}>
Request
</div>
</div> </div>
<div className={styles.notificationBox}> <div className={styles.notificationBox}>
{ {showPayLoadingScreen && (
showPayLoadingScreen &&
<span> <span>
<section className={`${styles.spinner} ${styles.icon}`} /> <section className={`${styles.spinner} ${styles.icon}`} />
<section>Sending your transaction...</section> <section>Sending your transaction...</section>
</span> </span>
} )}
{ {showSuccessPayScreen && (
showSuccessPayScreen &&
<span> <span>
<section className={styles.icon}><AnimatedCheckmark /></section> <section className={styles.icon}>
<AnimatedCheckmark />
</section>
<section>Successfully sent payment</section> <section>Successfully sent payment</section>
</span> </span>
} )}
{ {successTransactionScreen.show && (
successTransactionScreen.show &&
<span> <span>
<section className={styles.icon}><AnimatedCheckmark /></section> <section className={styles.icon}>
<AnimatedCheckmark />
</section>
<section> <section>
{ {
// TODO(jimmymow): remove this // TODO(jimmymow): remove this
// eslint-disable-next-line // eslint-disable-next-line
}Successfully <span className={styles.txLink} onClick={() => blockExplorer.showTransaction(isTestnet, successTransactionScreen.txid)}>sent</span> transaction }Successfully <span className={styles.txLink} onClick={() => blockExplorer.showTransaction(network, successTransactionScreen.txid)}>sent</span> transaction
</section> </section>
</span> </span>
} )}
</div> </div>
</div> </div>
</div> </div>
@ -134,7 +140,7 @@ Wallet.propTypes = {
openReceiveModal: PropTypes.func.isRequired, openReceiveModal: PropTypes.func.isRequired,
showPayLoadingScreen: PropTypes.bool.isRequired, showPayLoadingScreen: PropTypes.bool.isRequired,
showSuccessPayScreen: PropTypes.bool.isRequired, showSuccessPayScreen: PropTypes.bool.isRequired,
isTestnet: PropTypes.bool.isRequired, network: PropTypes.object.isRequired,
successTransactionScreen: PropTypes.object.isRequired, successTransactionScreen: PropTypes.object.isRequired,
currentCurrencyFilters: PropTypes.array.isRequired, currentCurrencyFilters: PropTypes.array.isRequired,
currencyName: PropTypes.string.isRequired, currencyName: PropTypes.string.isRequired,

6
app/containers/Root.js

@ -219,4 +219,8 @@ Root.propTypes = {
syncingProps: PropTypes.object.isRequired syncingProps: PropTypes.object.isRequired
} }
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Root) export default connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(Root)

12
app/lnd/index.js

@ -4,22 +4,24 @@ import walletUnlocker from './lib/walletUnlocker'
import subscribe from './subscribe' import subscribe from './subscribe'
import methods from './methods' import methods from './methods'
import walletUnlockerMethods from './walletUnlockerMethods' import walletUnlockerMethods from './walletUnlockerMethods'
// use mainLog because lndLog is reserved for the lnd binary itself
import { mainLog } from '../utils/log'
const initLnd = (callback) => { const initLnd = callback => {
const lndConfig = config.lnd() const lndConfig = config.lnd()
const lnd = lightning(lndConfig.lightningRpc, lndConfig.lightningHost) const lnd = lightning(lndConfig.lightningRpc, lndConfig.lightningHost)
const lndSubscribe = mainWindow => subscribe(mainWindow, lnd) const lndSubscribe = mainWindow => subscribe(mainWindow, lnd, mainLog)
const lndMethods = (event, msg, data) => methods(lnd, event, msg, data) const lndMethods = (event, msg, data) => methods(lnd, mainLog, event, msg, data)
callback(lndSubscribe, lndMethods) callback(lndSubscribe, lndMethods)
} }
const initWalletUnlocker = (callback) => { const initWalletUnlocker = callback => {
const lndConfig = config.lnd() const lndConfig = config.lnd()
const walletUnlockerObj = walletUnlocker(lndConfig.lightningRpc, lndConfig.lightningHost) const walletUnlockerObj = walletUnlocker(lndConfig.lightningRpc, lndConfig.lightningHost)
const walletUnlockerMethodsCallback = (event, msg, data) => walletUnlockerMethods(walletUnlockerObj, event, msg, data) const walletUnlockerMethodsCallback = (event, msg, data) => walletUnlockerMethods(walletUnlockerObj, mainLog, event, msg, data)
callback(walletUnlockerMethodsCallback) callback(walletUnlockerMethodsCallback)
} }

9
app/lnd/lib/lightning.js

@ -10,12 +10,9 @@ import config from '../config'
// //
// We order the suites by priority, based on the recommendations provided by SSL Labs here: // We order the suites by priority, based on the recommendations provided by SSL Labs here:
// https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices#23-use-secure-cipher-suites // https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices#23-use-secure-cipher-suites
process.env.GRPC_SSL_CIPHER_SUITES = process.env.GRPC_SSL_CIPHER_SUITES || [ process.env.GRPC_SSL_CIPHER_SUITES =
'ECDHE-ECDSA-AES128-GCM-SHA256', process.env.GRPC_SSL_CIPHER_SUITES ||
'ECDHE-ECDSA-AES256-GCM-SHA384', ['ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-AES128-CBC-SHA256', 'ECDHE-ECDSA-CHACHA20-POLY1305'].join(':')
'ECDHE-ECDSA-AES128-CBC-SHA256',
'ECDHE-ECDSA-CHACHA20-POLY1305'
].join(':')
const lightning = (rpcpath, host) => { const lightning = (rpcpath, host) => {
const lndConfig = config.lnd() const lndConfig = config.lnd()

9
app/lnd/lib/walletUnlocker.js

@ -10,12 +10,9 @@ import config from '../config'
// //
// We order the suites by priority, based on the recommendations provided by SSL Labs here: // We order the suites by priority, based on the recommendations provided by SSL Labs here:
// https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices#23-use-secure-cipher-suites // https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices#23-use-secure-cipher-suites
process.env.GRPC_SSL_CIPHER_SUITES = process.env.GRPC_SSL_CIPHER_SUITES || [ process.env.GRPC_SSL_CIPHER_SUITES =
'ECDHE-ECDSA-AES128-GCM-SHA256', process.env.GRPC_SSL_CIPHER_SUITES ||
'ECDHE-ECDSA-AES256-GCM-SHA384', ['ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-AES128-CBC-SHA256', 'ECDHE-ECDSA-CHACHA20-POLY1305'].join(':')
'ECDHE-ECDSA-AES128-CBC-SHA256',
'ECDHE-ECDSA-CHACHA20-POLY1305'
].join(':')
const walletUnlocker = (rpcpath, host) => { const walletUnlocker = (rpcpath, host) => {
const lndConfig = config.lnd() const lndConfig = config.lnd()

16
app/lnd/methods/channelController.js

@ -34,7 +34,7 @@ export function connectAndOpen(lnd, event, payload) {
return call return call
}) })
.catch((err) => { .catch(err => {
event.sender.send('pushchannelerror', { pubkey, error: err.toString() }) event.sender.send('pushchannelerror', { pubkey, error: err.toString() })
throw err throw err
}) })
@ -58,7 +58,8 @@ export function openChannel(lnd, event, payload) {
return new Promise((resolve, reject) => return new Promise((resolve, reject) =>
pushopenchannel(lnd, event, res) pushopenchannel(lnd, event, res)
.then(data => resolve(data)) .then(data => resolve(data))
.catch(error => reject(error))) .catch(error => reject(error))
)
} }
/** /**
@ -103,8 +104,15 @@ export function listChannels(lnd) {
* @return {[type]} [description] * @return {[type]} [description]
*/ */
export function closeChannel(lnd, event, payload) { export function closeChannel(lnd, event, payload) {
const { channel_point: { funding_txid, output_index }, chan_id, force } = payload const {
const tx = funding_txid.match(/.{2}/g).reverse().join('') channel_point: { funding_txid, output_index },
chan_id,
force
} = payload
const tx = funding_txid
.match(/.{2}/g)
.reverse()
.join('')
const res = { const res = {
channel_point: { channel_point: {

91
app/lnd/methods/index.js

@ -1,4 +1,3 @@
/* eslint no-console: 0 */ // --> OFF
// import grpc from 'grpc' // import grpc from 'grpc'
import * as invoicesController from './invoicesController' import * as invoicesController from './invoicesController'
@ -18,12 +17,12 @@ import * as networkController from './networkController'
// TODO - SendPayment // TODO - SendPayment
// TODO - DeleteAllPayments // TODO - DeleteAllPayments
export default function (lnd, event, msg, data) { export default function(lnd, log, event, msg, data) {
switch (msg) { switch (msg) {
case 'info': case 'info':
networkController networkController
.getInfo(lnd) .getInfo(lnd)
.then((infoData) => { .then(infoData => {
event.sender.send('receiveInfo', infoData) event.sender.send('receiveInfo', infoData)
event.sender.send('receiveCryptocurrency', infoData.chains[0]) event.sender.send('receiveCryptocurrency', infoData.chains[0])
return infoData return infoData
@ -34,14 +33,14 @@ export default function (lnd, event, msg, data) {
networkController networkController
.describeGraph(lnd) .describeGraph(lnd)
.then(networkData => event.sender.send('receiveDescribeNetwork', networkData)) .then(networkData => event.sender.send('receiveDescribeNetwork', networkData))
.catch(error => console.log('describeGraph error: ', error)) .catch(error => log.error('describeGraph:', error))
break break
case 'queryRoutes': case 'queryRoutes':
// Data looks like { pubkey: String, amount: Number } // Data looks like { pubkey: String, amount: Number }
networkController networkController
.queryRoutes(lnd, data) .queryRoutes(lnd, data)
.then(routes => event.sender.send('receiveQueryRoutes', routes)) .then(routes => event.sender.send('receiveQueryRoutes', routes))
.catch(error => console.log('queryRoutes error: ', error)) .catch(error => log.error('queryRoutes:', error))
break break
case 'getInvoiceAndQueryRoutes': case 'getInvoiceAndQueryRoutes':
// Data looks like { pubkey: String, amount: Number } // Data looks like { pubkey: String, amount: Number }
@ -51,74 +50,75 @@ export default function (lnd, event, msg, data) {
networkController.queryRoutes(lnd, { networkController.queryRoutes(lnd, {
pubkey: invoiceData.destination, pubkey: invoiceData.destination,
amount: invoiceData.num_satoshis amount: invoiceData.num_satoshis
})) })
)
.then(routes => event.sender.send('receiveInvoiceAndQueryRoutes', routes)) .then(routes => event.sender.send('receiveInvoiceAndQueryRoutes', routes))
.catch(error => console.log('getInvoiceAndQueryRoutes invoice error: ', error)) .catch(error => log.error('getInvoiceAndQueryRoutes invoice:', error))
break break
case 'newaddress': case 'newaddress':
// Data looks like { address: '' } // Data looks like { address: '' }
walletController walletController
.newAddress(lnd, data.type) .newAddress(lnd, data.type)
.then(({ address }) => event.sender.send('receiveAddress', address)) .then(({ address }) => event.sender.send('receiveAddress', address))
.catch(error => console.log('newaddress error: ', error)) .catch(error => log.error('newaddress:', error))
break break
case 'setAlias': case 'setAlias':
// Data looks like { new_alias: '' } // Data looks like { new_alias: '' }
walletController walletController
.setAlias(lnd, data) .setAlias(lnd, data)
.then(() => event.sender.send('aliasSet')) .then(() => event.sender.send('aliasSet'))
.catch(error => console.log('setAlias error: ', error)) .catch(error => log.error('setAlias:', error))
break break
case 'peers': case 'peers':
// Data looks like { peers: [] } // Data looks like { peers: [] }
peersController peersController
.listPeers(lnd) .listPeers(lnd)
.then(peersData => event.sender.send('receivePeers', peersData)) .then(peersData => event.sender.send('receivePeers', peersData))
.catch(error => console.log('peers error: ', error)) .catch(error => log.error('peers:', error))
break break
case 'channels': case 'channels':
// Data looks like // Data looks like
// [ { channels: [] }, { total_limbo_balance: 0, pending_open_channels: [], pending_closing_channels: [], pending_force_closing_channels: [] } ] // [ { channels: [] }, { total_limbo_balance: 0, pending_open_channels: [], pending_closing_channels: [], pending_force_closing_channels: [] } ]
Promise.all([channelController.listChannels, channelController.pendingChannels].map(func => func(lnd))) Promise.all([channelController.listChannels, channelController.pendingChannels].map(func => func(lnd)))
.then(channelsData => event.sender.send('receiveChannels', { channels: channelsData[0].channels, pendingChannels: channelsData[1] })) .then(channelsData => event.sender.send('receiveChannels', { channels: channelsData[0].channels, pendingChannels: channelsData[1] }))
.catch(error => console.log('channels error: ', error)) .catch(error => log.error('channels:', error))
break break
case 'transactions': case 'transactions':
// Data looks like { transactions: [] } // Data looks like { transactions: [] }
walletController walletController
.getTransactions(lnd) .getTransactions(lnd)
.then(transactionsData => event.sender.send('receiveTransactions', transactionsData)) .then(transactionsData => event.sender.send('receiveTransactions', transactionsData))
.catch(error => console.log('transactions error: ', error)) .catch(error => log.error('transactions:', error))
break break
case 'payments': case 'payments':
// Data looks like { payments: [] } // Data looks like { payments: [] }
paymentsController paymentsController
.listPayments(lnd) .listPayments(lnd)
.then(paymentsData => event.sender.send('receivePayments', paymentsData)) .then(paymentsData => event.sender.send('receivePayments', paymentsData))
.catch(error => console.log('payments error: ', error)) .catch(error => log.error('payments:', error))
break break
case 'invoices': case 'invoices':
// Data looks like { invoices: [] } // Data looks like { invoices: [] }
invoicesController invoicesController
.listInvoices(lnd) .listInvoices(lnd)
.then(invoicesData => event.sender.send('receiveInvoices', invoicesData)) .then(invoicesData => event.sender.send('receiveInvoices', invoicesData))
.catch(error => console.log('invoices error: ', error)) .catch(error => log.error('invoices:', error))
break break
case 'invoice': case 'invoice':
// Data looks like { invoices: [] } // Data looks like { invoices: [] }
invoicesController invoicesController
.getInvoice(lnd, { pay_req: data.payreq }) .getInvoice(lnd, { pay_req: data.payreq })
.then(invoiceData => event.sender.send('receiveInvoice', invoiceData)) .then(invoiceData => event.sender.send('receiveInvoice', invoiceData))
.catch(error => console.log('invoice error: ', error)) .catch(error => log.error('invoice:', error))
break break
case 'balance': case 'balance':
// Balance looks like [ { balance: '129477456' }, { balance: '243914' } ] // Balance looks like [ { balance: '129477456' }, { balance: '243914' } ]
Promise.all([walletController.walletBalance, channelController.channelBalance].map(func => func(lnd))) Promise.all([walletController.walletBalance, channelController.channelBalance].map(func => func(lnd)))
.then((balance) => { .then(balance => {
event.sender.send('receiveBalance', { walletBalance: balance[0].total_balance, channelBalance: balance[1].balance }) event.sender.send('receiveBalance', { walletBalance: balance[0].total_balance, channelBalance: balance[1].balance })
return balance return balance
}) })
.catch(error => console.log('balance error: ', error)) .catch(error => log.error('balance:', error))
break break
case 'createInvoice': case 'createInvoice':
// Invoice looks like { r_hash: Buffer, payment_request: '' } // Invoice looks like { r_hash: Buffer, payment_request: '' }
@ -138,13 +138,15 @@ export default function (lnd, event, msg, data) {
payment_request: newinvoice.payment_request, payment_request: newinvoice.payment_request,
creation_date: Date.now() / 1000 creation_date: Date.now() / 1000
}) })
)) )
.catch((error) => { )
console.log('decodedInvoice error: ', error) .catch(error => {
log.error('decodedInvoice:', error)
event.sender.send('invoiceFailed', { error: error.toString() }) event.sender.send('invoiceFailed', { error: error.toString() })
})) })
.catch((error) => { )
console.log('addInvoice error: ', error) .catch(error => {
log.error('addInvoice:', error)
event.sender.send('invoiceFailed', { error: error.toString() }) event.sender.send('invoiceFailed', { error: error.toString() })
}) })
break break
@ -153,14 +155,15 @@ export default function (lnd, event, msg, data) {
// { paymentRequest } = data // { paymentRequest } = data
paymentsController paymentsController
.sendPaymentSync(lnd, data) .sendPaymentSync(lnd, data)
.then((payment) => { .then(payment => {
log.info('payment:', payment)
const { payment_route } = payment const { payment_route } = payment
console.log('payinvoice success: ', payment_route) log.error('payinvoice success:', payment_route)
event.sender.send('paymentSuccessful', Object.assign(data, { payment_route })) event.sender.send('paymentSuccessful', Object.assign(data, { payment_route }))
return payment return payment
}) })
.catch(({ error }) => { .catch(({ error }) => {
console.log('error: ', error) log.error('error: ', error)
event.sender.send('paymentFailed', { error: error.toString() }) event.sender.send('paymentFailed', { error: error.toString() })
}) })
break break
@ -170,8 +173,8 @@ export default function (lnd, event, msg, data) {
walletController walletController
.sendCoins(lnd, data) .sendCoins(lnd, data)
.then(({ txid }) => event.sender.send('transactionSuccessful', { amount: data.amount, addr: data.addr, txid })) .then(({ txid }) => event.sender.send('transactionSuccessful', { amount: data.amount, addr: data.addr, txid }))
.catch((error) => { .catch(error => {
console.log('error: ', error) log.error('error: ', error)
event.sender.send('transactionError', { error: error.toString() }) event.sender.send('transactionError', { error: error.toString() })
}) })
break break
@ -180,39 +183,39 @@ export default function (lnd, event, msg, data) {
// { pubkey, localamt, pushamt } = data // { pubkey, localamt, pushamt } = data
channelController channelController
.openChannel(lnd, event, data) .openChannel(lnd, event, data)
.then((channel) => { .then(channel => {
console.log('CHANNEL: ', channel) log.error('CHANNEL: ', channel)
event.sender.send('channelSuccessful', { channel }) event.sender.send('channelSuccessful', { channel })
return channel return channel
}) })
.catch(error => console.log('openChannel error: ', error)) .catch(error => log.error('openChannel:', error))
break break
case 'closeChannel': case 'closeChannel':
// Response is empty. Streaming updates on channel status and updates // Response is empty. Streaming updates on channel status and updates
// { channel_point, force } = data // { channel_point, force } = data
channelController channelController
.closeChannel(lnd, event, data) .closeChannel(lnd, event, data)
.then((result) => { .then(result => {
console.log('CLOSE CHANNEL: ', result) log.error('CLOSE CHANNEL: ', result)
event.sender.send('closeChannelSuccessful') event.sender.send('closeChannelSuccessful')
return result return result
}) })
.catch(error => console.log('closeChannel error: ', error)) .catch(error => log.error('closeChannel:', error))
break break
case 'connectPeer': case 'connectPeer':
// Returns a peer_id. Pass the pubkey, host and peer_id so we can add a new peer to the list // Returns a peer_id. Pass the pubkey, host and peer_id so we can add a new peer to the list
// { pubkey, host } = data // { pubkey, host } = data
peersController peersController
.connectPeer(lnd, data) .connectPeer(lnd, data)
.then((peer) => { .then(peer => {
const { peer_id } = peer const { peer_id } = peer
console.log('peer_id: ', peer_id) log.error('peer_id: ', peer_id)
event.sender.send('connectSuccess', { pub_key: data.pubkey, address: data.host, peer_id }) event.sender.send('connectSuccess', { pub_key: data.pubkey, address: data.host, peer_id })
return peer return peer
}) })
.catch((error) => { .catch(error => {
event.sender.send('connectFailure', { error: error.toString() }) event.sender.send('connectFailure', { error: error.toString() })
console.log('connectPeer error: ', error) log.error('connectPeer:', error)
}) })
break break
case 'disconnectPeer': case 'disconnectPeer':
@ -221,25 +224,25 @@ export default function (lnd, event, msg, data) {
peersController peersController
.disconnectPeer(lnd, data) .disconnectPeer(lnd, data)
.then(() => { .then(() => {
console.log('pubkey: ', data.pubkey) log.error('pubkey: ', data.pubkey)
event.sender.send('disconnectSuccess', { pubkey: data.pubkey }) event.sender.send('disconnectSuccess', { pubkey: data.pubkey })
return null return null
}) })
.catch(error => console.log('disconnectPeer error: ', error)) .catch(error => log.error('disconnectPeer:', error))
break break
case 'connectAndOpen': case 'connectAndOpen':
// Connects to a peer if we aren't connected already and then attempt to open a channel // Connects to a peer if we aren't connected already and then attempt to open a channel
// {} = data // {} = data
channelController channelController
.connectAndOpen(lnd, event, data) .connectAndOpen(lnd, event, data)
.then((channelData) => { .then(channelData => {
console.log('connectAndOpen data: ', channelData) log.error('connectAndOpen data: ', channelData)
// event.sender.send('connectSuccess', { pub_key: data.pubkey, address: data.host, peer_id }) // event.sender.send('connectSuccess', { pub_key: data.pubkey, address: data.host, peer_id })
return channelData return channelData
}) })
.catch((error) => { .catch(error => {
// event.sender.send('connectFailure', { error: error.toString() }) // event.sender.send('connectFailure', { error: error.toString() })
console.log('connectAndOpen error: ', error) log.error('connectAndOpen:', error)
}) })
break break
default: default:

18
app/lnd/methods/invoicesController.js

@ -10,7 +10,9 @@ import pushinvoices from '../push/subscribeinvoice'
export function addInvoice(lnd, { memo, value }) { export function addInvoice(lnd, { memo, value }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.addInvoice({ memo, value }, (err, data) => { lnd.addInvoice({ memo, value }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
@ -25,7 +27,9 @@ export function addInvoice(lnd, { memo, value }) {
export function listInvoices(lnd) { export function listInvoices(lnd) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.listInvoices({}, (err, data) => { lnd.listInvoices({}, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
@ -39,14 +43,15 @@ export function listInvoices(lnd) {
export function getInvoice(lnd, { pay_req }) { export function getInvoice(lnd, { pay_req }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.decodePayReq({ pay_req }, (err, data) => { lnd.decodePayReq({ pay_req }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
}) })
} }
/** /**
* Attemps to look up an invoice according to its payment hash * Attemps to look up an invoice according to its payment hash
* @param {[type]} lnd [description] * @param {[type]} lnd [description]
@ -56,14 +61,15 @@ export function getInvoice(lnd, { pay_req }) {
export function lookupInvoice(lnd, { rhash }) { export function lookupInvoice(lnd, { rhash }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.lookupInvoice({ r_hash: rhash }, (err, data) => { lnd.lookupInvoice({ r_hash: rhash }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
}) })
} }
/** /**
* Returns a uni-directional stream (server -> client) for notifying the client of newly added/settled invoices * Returns a uni-directional stream (server -> client) for notifying the client of newly added/settled invoices
* @param {[type]} lnd [description] * @param {[type]} lnd [description]

24
app/lnd/methods/networkController.js

@ -6,14 +6,15 @@
export function getInfo(lnd) { export function getInfo(lnd) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.getInfo({}, (err, data) => { lnd.getInfo({}, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
}) })
} }
/** /**
* Returns general information concerning the lightning node * Returns general information concerning the lightning node
* @param {[type]} lnd [description] * @param {[type]} lnd [description]
@ -23,14 +24,15 @@ export function getInfo(lnd) {
export function getNodeInfo(lnd, { pubkey }) { export function getNodeInfo(lnd, { pubkey }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.getNodeInfo({ pub_key: pubkey }, (err, data) => { lnd.getNodeInfo({ pub_key: pubkey }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
}) })
} }
/** /**
* Returns a description of the latest graph state from the point of view of the node * Returns a description of the latest graph state from the point of view of the node
* @param {[type]} lnd [description] * @param {[type]} lnd [description]
@ -39,14 +41,15 @@ export function getNodeInfo(lnd, { pubkey }) {
export function describeGraph(lnd) { export function describeGraph(lnd) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.describeGraph({}, (err, data) => { lnd.describeGraph({}, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
}) })
} }
/** /**
* Attempts to query the daemons Channel Router for a possible route to a target destination capable of carrying a specific amount of satoshis * Attempts to query the daemons Channel Router for a possible route to a target destination capable of carrying a specific amount of satoshis
* @param {[type]} lnd [description] * @param {[type]} lnd [description]
@ -57,14 +60,15 @@ export function describeGraph(lnd) {
export function queryRoutes(lnd, { pubkey, amount }) { export function queryRoutes(lnd, { pubkey, amount }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.queryRoutes({ pub_key: pubkey, amt: amount }, (err, data) => { lnd.queryRoutes({ pub_key: pubkey, amt: amount }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
}) })
} }
/** /**
* Returns some basic stats about the known channel graph from the point of view of the node * Returns some basic stats about the known channel graph from the point of view of the node
* @param {[type]} lnd [description] * @param {[type]} lnd [description]
@ -73,7 +77,9 @@ export function queryRoutes(lnd, { pubkey, amount }) {
export function getNetworkInfo(lnd) { export function getNetworkInfo(lnd) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.getNetworkInfo({}, (err, data) => { lnd.getNetworkInfo({}, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })

27
app/lnd/methods/paymentsController.js

@ -9,18 +9,15 @@ export function sendPaymentSync(lnd, { paymentRequest }) {
lnd.sendPaymentSync({ payment_request: paymentRequest }, (error, data) => { lnd.sendPaymentSync({ payment_request: paymentRequest }, (error, data) => {
if (error) { if (error) {
reject({ error }) reject({ error })
return } else if (!data || !data.payment_route) {
} reject({ error: data.payment_error })
} else {
if (!data || !data.payment_route) { reject({ error: data.payment_error }) }
console.log('data: ', data)
resolve(data) resolve(data)
}
}) })
}) })
} }
/** /**
* Synchronous non-streaming version of SendPayment * Synchronous non-streaming version of SendPayment
* @param {[type]} lnd [description] * @param {[type]} lnd [description]
@ -30,7 +27,9 @@ export function sendPaymentSync(lnd, { paymentRequest }) {
export function sendPayment(lnd, { paymentRequest }) { export function sendPayment(lnd, { paymentRequest }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.sendPayment({ payment_request: paymentRequest }, (err, data) => { lnd.sendPayment({ payment_request: paymentRequest }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
@ -46,7 +45,9 @@ export function sendPayment(lnd, { paymentRequest }) {
export function decodePayReq(lnd, { payReq }) { export function decodePayReq(lnd, { payReq }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.decodePayReq({ pay_req: payReq }, (err, data) => { lnd.decodePayReq({ pay_req: payReq }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
@ -61,7 +62,9 @@ export function decodePayReq(lnd, { payReq }) {
export function listPayments(lnd) { export function listPayments(lnd) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.listPayments({}, (err, data) => { lnd.listPayments({}, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
@ -76,7 +79,9 @@ export function listPayments(lnd) {
export function deleteAllPayments(lnd) { export function deleteAllPayments(lnd) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.deleteAllPayments({}, (err, data) => { lnd.deleteAllPayments({}, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })

14
app/lnd/methods/peersController.js

@ -8,14 +8,15 @@
export function connectPeer(lnd, { pubkey, host }) { export function connectPeer(lnd, { pubkey, host }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.connectPeer({ addr: { pubkey, host } }, (err, data) => { lnd.connectPeer({ addr: { pubkey, host } }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
}) })
} }
/** /**
* Attempts to disconnect one peer from another * Attempts to disconnect one peer from another
* @param {[type]} lnd [description] * @param {[type]} lnd [description]
@ -25,14 +26,15 @@ export function connectPeer(lnd, { pubkey, host }) {
export function disconnectPeer(lnd, { pubkey }) { export function disconnectPeer(lnd, { pubkey }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.disconnectPeer({ pub_key: pubkey }, (err, data) => { lnd.disconnectPeer({ pub_key: pubkey }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
}) })
} }
/** /**
* Returns a verbose listing of all currently active peers * Returns a verbose listing of all currently active peers
* @param {[type]} lnd [description] * @param {[type]} lnd [description]
@ -41,7 +43,9 @@ export function disconnectPeer(lnd, { pubkey }) {
export function listPeers(lnd) { export function listPeers(lnd) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.listPeers({}, (err, data) => { lnd.listPeers({}, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })

48
app/lnd/methods/walletController.js

@ -6,14 +6,15 @@
export function walletBalance(lnd) { export function walletBalance(lnd) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.walletBalance({}, (err, data) => { lnd.walletBalance({}, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
}) })
} }
/** /**
* Creates a new address under control of the local wallet * Creates a new address under control of the local wallet
* @param {[type]} lnd [description] * @param {[type]} lnd [description]
@ -23,7 +24,9 @@ export function walletBalance(lnd) {
export function newAddress(lnd, type) { export function newAddress(lnd, type) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.newAddress({ type }, (err, data) => { lnd.newAddress({ type }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
@ -38,14 +41,15 @@ export function newAddress(lnd, type) {
export function newWitnessAddress(lnd, { addr }) { export function newWitnessAddress(lnd, { addr }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.newWitnessAddress({ address: addr }, (err, data) => { lnd.newWitnessAddress({ address: addr }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
}) })
} }
/** /**
* Returns a list describing all the known transactions relevant to the wallet * Returns a list describing all the known transactions relevant to the wallet
* @param {[type]} lnd [description] * @param {[type]} lnd [description]
@ -54,14 +58,15 @@ export function newWitnessAddress(lnd, { addr }) {
export function getTransactions(lnd) { export function getTransactions(lnd) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.getTransactions({}, (err, data) => { lnd.getTransactions({}, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
}) })
} }
/** /**
* Executes a request to send coins to a particular address * Executes a request to send coins to a particular address
* @param {[type]} lnd [description] * @param {[type]} lnd [description]
@ -72,7 +77,9 @@ export function getTransactions(lnd) {
export function sendCoins(lnd, { addr, amount }) { export function sendCoins(lnd, { addr, amount }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.sendCoins({ addr, amount }, (err, data) => { lnd.sendCoins({ addr, amount }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
@ -86,7 +93,9 @@ export function sendCoins(lnd, { addr, amount }) {
export function setAlias(lnd, { new_alias }) { export function setAlias(lnd, { new_alias }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.setAlias({ new_alias }, (err, data) => { lnd.setAlias({ new_alias }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
@ -99,7 +108,9 @@ export function setAlias(lnd, { new_alias }) {
export function genSeed(walletUnlocker) { export function genSeed(walletUnlocker) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
walletUnlocker.genSeed({}, (err, data) => { walletUnlocker.genSeed({}, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
@ -113,7 +124,9 @@ export function genSeed(walletUnlocker) {
export function unlockWallet(walletUnlocker, { wallet_password }) { export function unlockWallet(walletUnlocker, { wallet_password }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
walletUnlocker.unlockWallet({ wallet_password: Buffer.from(wallet_password) }, (err, data) => { walletUnlocker.unlockWallet({ wallet_password: Buffer.from(wallet_password) }, (err, data) => {
if (err) { reject(err) } if (err) {
reject(err)
}
resolve(data) resolve(data)
}) })
@ -127,15 +140,20 @@ export function unlockWallet(walletUnlocker, { wallet_password }) {
*/ */
export function initWallet(walletUnlocker, { wallet_password, cipher_seed_mnemonic, aezeed_passphrase }) { export function initWallet(walletUnlocker, { wallet_password, cipher_seed_mnemonic, aezeed_passphrase }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
walletUnlocker.initWallet({ walletUnlocker.initWallet(
{
wallet_password: Buffer.from(wallet_password), wallet_password: Buffer.from(wallet_password),
cipher_seed_mnemonic, cipher_seed_mnemonic,
aezeed_passphrase: Buffer.from(aezeed_passphrase, 'hex'), aezeed_passphrase: Buffer.from(aezeed_passphrase, 'hex'),
recovery_window: 250 recovery_window: 250
}, (err, data) => { },
if (err) { reject(err) } (err, data) => {
if (err) {
reject(err)
}
resolve(data) resolve(data)
}) }
)
}) })
} }

6
app/lnd/subscribe/index.js

@ -2,8 +2,8 @@ import subscribeToTransactions from './transactions'
import subscribeToInvoices from './invoices' import subscribeToInvoices from './invoices'
import subscribeToChannelGraph from './channelgraph' import subscribeToChannelGraph from './channelgraph'
export default (mainWindow, lnd) => { export default (mainWindow, lnd, log) => {
subscribeToTransactions(mainWindow, lnd) subscribeToTransactions(mainWindow, lnd, log)
subscribeToInvoices(mainWindow, lnd) subscribeToInvoices(mainWindow, lnd, log)
subscribeToChannelGraph(mainWindow, lnd) subscribeToChannelGraph(mainWindow, lnd)
} }

8
app/lnd/subscribe/invoices.js

@ -1,8 +1,8 @@
export default function subscribeToInvoices(mainWindow, lnd) { export default function subscribeToInvoices(mainWindow, lnd, log) {
const call = lnd.subscribeInvoices({}) const call = lnd.subscribeInvoices({})
call.on('data', invoice => mainWindow.send('invoiceUpdate', { invoice })) call.on('data', invoice => mainWindow.send('invoiceUpdate', { invoice }))
call.on('end', () => console.log('end')) call.on('end', () => log.info('end'))
call.on('error', error => console.log('error: ', error)) call.on('error', error => log.error(error))
call.on('status', status => console.log('status: ', status)) call.on('status', status => log.info('status:', status))
} }

12
app/lnd/subscribe/transactions.js

@ -1,10 +1,10 @@
export default function subscribeToTransactions(mainWindow, lnd) { export default function subscribeToTransactions(mainWindow, lnd, log) {
const call = lnd.subscribeTransactions({}) const call = lnd.subscribeTransactions({})
call.on('data', (transaction) => { call.on('data', transaction => {
console.log('TRANSACTION: ', transaction) lnd.log.info('TRANSACTION:', transaction)
mainWindow.send('newTransaction', { transaction }) mainWindow.send('newTransaction', { transaction })
}) })
call.on('end', () => console.log('end')) call.on('end', () => log.info('end'))
call.on('error', error => console.log('error: ', error)) call.on('error', error => log.error('error: ', error))
call.on('status', status => console.log('TRANSACTION STATUS: ', status)) call.on('status', status => log.info('TRANSACTION STATUS: ', status))
} }

15
app/lnd/walletUnlockerMethods/index.js

@ -1,23 +1,24 @@
/* eslint no-console: 0 */ // --> OFF
import * as walletController from '../methods/walletController' import * as walletController from '../methods/walletController'
export default function (walletUnlocker, event, msg, data) { export default function(walletUnlocker, log, event, msg, data) {
switch (msg) { switch (msg) {
case 'genSeed': case 'genSeed':
walletController.genSeed(walletUnlocker) walletController
.genSeed(walletUnlocker)
.then(genSeedData => event.sender.send('receiveSeed', genSeedData)) .then(genSeedData => event.sender.send('receiveSeed', genSeedData))
.catch(error => event.sender.send('receiveSeedError', error)) .catch(error => event.sender.send('receiveSeedError', error))
break break
case 'unlockWallet': case 'unlockWallet':
walletController.unlockWallet(walletUnlocker, data) walletController
.unlockWallet(walletUnlocker, data)
.then(() => event.sender.send('walletUnlocked')) .then(() => event.sender.send('walletUnlocked'))
.catch(() => event.sender.send('unlockWalletError')) .catch(() => event.sender.send('unlockWalletError'))
break break
case 'initWallet': case 'initWallet':
walletController.initWallet(walletUnlocker, data) walletController
.initWallet(walletUnlocker, data)
.then(() => event.sender.send('successfullyCreatedWallet')) .then(() => event.sender.send('successfullyCreatedWallet'))
.catch(error => console.log('initWallet error: ', error)) .catch(error => log.error('initWallet:', error))
break break
default: default:
} }

97
app/main.dev.js

@ -13,12 +13,14 @@
import { app, BrowserWindow, ipcMain, dialog } from 'electron' import { app, BrowserWindow, ipcMain, dialog } from 'electron'
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import split2 from 'split2'
import { spawn } from 'child_process' import { spawn } from 'child_process'
import { lookup } from 'ps-node' import { lookup } from 'ps-node'
import Store from 'electron-store' import Store from 'electron-store'
import MenuBuilder from './menu' import MenuBuilder from './menu'
import lnd from './lnd' import lnd from './lnd'
import config from './lnd/config' import config from './lnd/config'
import { mainLog, lndLog, lndLogGetLevel } from './utils/log'
let mainWindow = null let mainWindow = null
@ -43,7 +45,7 @@ const installExtensions = async () => {
const forceDownload = !!process.env.UPGRADE_EXTENSIONS const forceDownload = !!process.env.UPGRADE_EXTENSIONS
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'] const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']
return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload))).catch(console.log) return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload))).catch(mainLog.error)
} }
// Send the front end event letting them know the gRPC connection is disconnected // Send the front end event letting them know the gRPC connection is disconnected
@ -67,7 +69,7 @@ const sendLndSyncing = () => {
clearInterval(sendLndSyncingInterval) clearInterval(sendLndSyncingInterval)
if (mainWindow) { if (mainWindow) {
console.log('SENDING SYNCING') mainLog.info('SENDING SYNCING')
startedSync = true startedSync = true
mainWindow.webContents.send('lndSyncing') mainWindow.webContents.send('lndSyncing')
} }
@ -81,7 +83,7 @@ const sendStartOnboarding = () => {
clearInterval(sendStartOnboardingInterval) clearInterval(sendStartOnboardingInterval)
if (mainWindow) { if (mainWindow) {
console.log('STARTING ONBOARDING') mainLog.info('STARTING ONBOARDING')
mainWindow.webContents.send('startOnboarding') mainWindow.webContents.send('startOnboarding')
} }
} }
@ -118,7 +120,7 @@ const startGrpc = () => {
// Create and subscribe the grpc object // Create and subscribe the grpc object
const startWalletUnlocker = () => { const startWalletUnlocker = () => {
lnd.initWalletUnlocker((walletUnlockerMethods) => { lnd.initWalletUnlocker(walletUnlockerMethods => {
// Listen for all gRPC restful methods // Listen for all gRPC restful methods
ipcMain.on('walletUnlocker', (event, { msg, data }) => { ipcMain.on('walletUnlocker', (event, { msg, data }) => {
walletUnlockerMethods(event, msg, data) walletUnlockerMethods(event, msg, data)
@ -135,7 +137,7 @@ const sendLndSynced = () => {
clearInterval(sendLndSyncedInterval) clearInterval(sendLndSyncedInterval)
if (mainWindow) { if (mainWindow) {
console.log('SENDING SYNCED') mainLog.info('SENDING SYNCED')
mainWindow.webContents.send('lndSynced') mainWindow.webContents.send('lndSynced')
} }
} }
@ -145,7 +147,12 @@ const sendLndSynced = () => {
// Starts the LND node // Starts the LND node
const startLnd = (alias, autopilot) => { const startLnd = (alias, autopilot) => {
const lndConfig = config.lnd() const lndConfig = config.lnd()
console.log('lndConfig', lndConfig) mainLog.info('STARTING BUNDLED LND')
mainLog.debug(' > lndPath', lndConfig.lndPath)
mainLog.debug(' > lightningRpc:', lndConfig.lightningRpc)
mainLog.debug(' > lightningHost:', lndConfig.lightningHost)
mainLog.debug(' > cert:', lndConfig.cert)
mainLog.debug(' > macaroon:', lndConfig.macaroon)
const neutrinoArgs = [ const neutrinoArgs = [
'--bitcoin.active', '--bitcoin.active',
@ -159,35 +166,38 @@ const startLnd = (alias, autopilot) => {
] ]
const neutrino = spawn(lndConfig.lndPath, neutrinoArgs) const neutrino = spawn(lndConfig.lndPath, neutrinoArgs)
.on('error', (error) => { .on('error', error => {
console.log(`lnd error: ${error}`) lndLog.error(`lnd error: ${error}`)
dialog.showMessageBox({ dialog.showMessageBox({
type: 'error', type: 'error',
message: `lnd error: ${error}` message: `lnd error: ${error}`
}) })
}) })
.on('close', (code) => { .on('close', code => {
console.log(`lnd shutting down ${code}`) lndLog.info(`lnd shutting down ${code}`)
app.quit() app.quit()
}) })
// Listen for when neutrino prints out data // Listen for when neutrino prints odata to stderr.
neutrino.stdout.on('data', (data) => { neutrino.stderr.pipe(split2()).on('data', line => {
// Data stored in variable line, log line to the console if (process.env.NODE_ENV === 'development') {
const line = data.toString('utf8') lndLog[lndLogGetLevel(line)](line)
}
})
// Listen for when neutrino prints data to stdout.
neutrino.stdout.pipe(split2()).on('data', line => {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.log(line) lndLog[lndLogGetLevel(line)](line)
} }
// If the gRPC proxy has started we can start ours // If the gRPC proxy has started we can start ours
if (line.includes('gRPC proxy started')) { if (line.includes('gRPC proxy started')) {
const certInterval = setInterval(() => { const certInterval = setInterval(() => {
console.log('lndConfig', lndConfig)
if (fs.existsSync(lndConfig.cert)) { if (fs.existsSync(lndConfig.cert)) {
clearInterval(certInterval) clearInterval(certInterval)
console.log('CERT EXISTS, STARTING WALLET UNLOCKER') mainLog.info('CERT EXISTS, STARTING WALLET UNLOCKER')
startWalletUnlocker() startWalletUnlocker()
if (mainWindow) { if (mainWindow) {
@ -198,7 +208,7 @@ const startLnd = (alias, autopilot) => {
} }
if (line.includes('gRPC proxy started') && !line.includes('password')) { if (line.includes('gRPC proxy started') && !line.includes('password')) {
console.log('WALLET OPENED, STARTING LIGHTNING GRPC CONNECTION') mainLog.info('WALLET OPENED, STARTING LIGHTNING GRPC CONNECTION')
sendLndSyncing() sendLndSyncing()
startGrpc() startGrpc()
} }
@ -212,7 +222,7 @@ const startLnd = (alias, autopilot) => {
// When LND is all caught up to the blockchain // When LND is all caught up to the blockchain
if (line.includes('Chain backend is fully synced')) { if (line.includes('Chain backend is fully synced')) {
// Log that LND is caught up to the current block height // Log that LND is caught up to the current block height
console.log('NEUTRINO IS SYNCED') mainLog.info('NEUTRINO IS SYNCED')
// Let the front end know we have stopped syncing LND // Let the front end know we have stopped syncing LND
sendLndSynced() sendLndSynced()
@ -274,14 +284,30 @@ app.on('ready', async () => {
sendGrpcDisconnected() sendGrpcDisconnected()
mainLog.info('LOOKING FOR EXISTING LND PROCESS')
// Check to see if an LND process is running.
lookup({ command: 'lnd' }, (err, results) => {
// There was an error checking for the LND process.
if (err) {
throw new Error(err)
}
if (!results.length) {
// An LND process was found, no need to start our own.
mainLog.info('EXISTING LND PROCESS NOT FOUND')
// Let the application know onboarding has started. // Let the application know onboarding has started.
sendStartOnboarding() sendStartOnboarding()
} else {
// An LND process was found, no need to start our own.
mainLog.info('FOUND EXISTING LND PROCESS')
startGrpc()
mainWindow.webContents.send('successfullyCreatedWallet')
}
})
// Start LND // Start LND
// once the onboarding has enough information, start or connect to LND. // once the onboarding has enough information, start or connect to LND.
ipcMain.on('startLnd', (event, options = {}) => { ipcMain.on('startLnd', (event, options = {}) => {
console.log('STARTING LND', options)
const store = new Store({ name: 'connection' }) const store = new Store({ name: 'connection' })
store.store = { store.store = {
type: options.connectionType, type: options.connectionType,
@ -292,29 +318,20 @@ app.on('ready', async () => {
autopilot: options.autopilot autopilot: options.autopilot
} }
console.log('SAVED CONFIG TO:', store.path, 'AS', store.store) mainLog.info('GOT LND CONFIG')
mainLog.debug(' > connectionType:', options.connectionType)
mainLog.debug(' > connectionHost:', options.connectionHost)
mainLog.debug(' > connectionCert:', options.connectionCert)
mainLog.debug(' > connectionMacaroon:', options.connectionMacaroon)
mainLog.debug(' > alias:', options.alias)
mainLog.debug(' > autopilot:', options.autopilot)
if (options.connectionType === 'local') { mainLog.info('SAVED LND CONFIG TO:', store.path)
console.log('LOOKING FOR LOCAL LND')
// Check to see if an LND process is running.
lookup({ command: 'lnd' }, (err, results) => {
// There was an error checking for the LND process.
if (err) {
throw new Error(err)
}
// No LND process was found. if (options.connectionType === 'local') {
if (!results.length) {
startLnd(options.alias, options.autopilot) startLnd(options.alias, options.autopilot)
} else { } else {
// An LND process was found, no need to start our own. mainLog.info('CONNECTING TO CUSTOM LND INSTANCE')
console.log('LND ALREADY RUNNING')
startGrpc()
mainWindow.webContents.send('successfullyCreatedWallet')
}
})
} else {
console.log('USING CUSTOM LND')
startGrpc() startGrpc()
mainWindow.webContents.send('successfullyCreatedWallet') mainWindow.webContents.send('successfullyCreatedWallet')
} }

136
app/menu.js

@ -2,7 +2,7 @@
import { app, Menu, shell, BrowserWindow } from 'electron' import { app, Menu, shell, BrowserWindow } from 'electron'
export default class MenuBuilder { export default class MenuBuilder {
mainWindow: BrowserWindow; mainWindow: BrowserWindow
constructor(mainWindow: BrowserWindow) { constructor(mainWindow: BrowserWindow) {
this.mainWindow = mainWindow this.mainWindow = mainWindow
@ -26,16 +26,11 @@ export default class MenuBuilder {
const menu = Menu.buildFromTemplate(template) const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu) Menu.setApplicationMenu(menu)
return menu return menu
} }
setupInputTemplate() { setupInputTemplate() {
const selectionMenu = Menu.buildFromTemplate([ const selectionMenu = Menu.buildFromTemplate([{ role: 'copy' }, { type: 'separator' }, { role: 'selectall' }])
{ role: 'copy' },
{ type: 'separator' },
{ role: 'selectall' }
])
const inputMenu = Menu.buildFromTemplate([ const inputMenu = Menu.buildFromTemplate([
{ role: 'undo' }, { role: 'undo' },
@ -73,7 +68,13 @@ export default class MenuBuilder {
{ label: 'Hide Others', accelerator: 'Command+Shift+H', selector: 'hideOtherApplications:' }, { label: 'Hide Others', accelerator: 'Command+Shift+H', selector: 'hideOtherApplications:' },
{ label: 'Show All', selector: 'unhideAllApplications:' }, { label: 'Show All', selector: 'unhideAllApplications:' },
{ type: 'separator' }, { type: 'separator' },
{ label: 'Quit', accelerator: 'Command+Q', click: () => { app.quit() } } {
label: 'Quit',
accelerator: 'Command+Q',
click: () => {
app.quit()
}
}
] ]
} }
const subMenuEdit = { const subMenuEdit = {
@ -91,13 +92,27 @@ export default class MenuBuilder {
const subMenuViewDev = { const subMenuViewDev = {
label: 'View', label: 'View',
submenu: [ submenu: [
{ label: 'Reload', accelerator: 'Command+R', click: () => { this.mainWindow.webContents.reload() } }, {
label: 'Reload',
accelerator: 'Command+R',
click: () => {
this.mainWindow.webContents.reload()
}
},
{ {
label: 'Toggle Full Screen', label: 'Toggle Full Screen',
accelerator: 'Ctrl+Command+F', accelerator: 'Ctrl+Command+F',
click: () => { this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()) } click: () => {
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen())
}
}, },
{ label: 'Toggle Developer Tools', accelerator: 'Alt+Command+I', click: () => { this.mainWindow.toggleDevTools() } } {
label: 'Toggle Developer Tools',
accelerator: 'Alt+Command+I',
click: () => {
this.mainWindow.toggleDevTools()
}
}
] ]
} }
const subMenuViewProd = { const subMenuViewProd = {
@ -106,7 +121,9 @@ export default class MenuBuilder {
{ {
label: 'Toggle Full Screen', label: 'Toggle Full Screen',
accelerator: 'Ctrl+Command+F', accelerator: 'Ctrl+Command+F',
click: () => { this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()) } click: () => {
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen())
}
} }
] ]
} }
@ -122,90 +139,123 @@ export default class MenuBuilder {
const subMenuHelp = { const subMenuHelp = {
label: 'Help', label: 'Help',
submenu: [ submenu: [
{ label: 'Learn More', click() { shell.openExternal('https://zap.jackmallers.com/') } }, {
{ label: 'Documentation', click() { shell.openExternal('https://github.com/LN-Zap/zap-desktop') } }, label: 'Learn More',
{ label: 'Community Discussions', click() { shell.openExternal('zaphq.slack.com') } }, click() {
{ label: 'Search Issues', click() { shell.openExternal('https://github.com/LN-Zap/zap-desktop/issues') } } shell.openExternal('https://zap.jackmallers.com/')
}
},
{
label: 'Documentation',
click() {
shell.openExternal('https://github.com/LN-Zap/zap-desktop')
}
},
{
label: 'Community Discussions',
click() {
shell.openExternal('zaphq.slack.com')
}
},
{
label: 'Search Issues',
click() {
shell.openExternal('https://github.com/LN-Zap/zap-desktop/issues')
}
}
] ]
} }
const subMenuView = process.env.NODE_ENV === 'development' const subMenuView = process.env.NODE_ENV === 'development' ? subMenuViewDev : subMenuViewProd
? subMenuViewDev
: subMenuViewProd
return [ return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp]
subMenuAbout,
subMenuEdit,
subMenuView,
subMenuWindow,
subMenuHelp
]
} }
buildDefaultTemplate() { buildDefaultTemplate() {
const templateDefault = [{ const templateDefault = [
{
label: '&File', label: '&File',
submenu: [{ submenu: [
{
label: '&Open', label: '&Open',
accelerator: 'Ctrl+O' accelerator: 'Ctrl+O'
}, { },
{
label: '&Close', label: '&Close',
accelerator: 'Ctrl+W', accelerator: 'Ctrl+W',
click: () => { click: () => {
this.mainWindow.close() this.mainWindow.close()
} }
}] }
}, { ]
},
{
label: '&View', label: '&View',
submenu: (process.env.NODE_ENV === 'development') ? [{ submenu:
process.env.NODE_ENV === 'development'
? [
{
label: '&Reload', label: '&Reload',
accelerator: 'Ctrl+R', accelerator: 'Ctrl+R',
click: () => { click: () => {
this.mainWindow.webContents.reload() this.mainWindow.webContents.reload()
} }
}, { },
{
label: 'Toggle &Full Screen', label: 'Toggle &Full Screen',
accelerator: 'F11', accelerator: 'F11',
click: () => { click: () => {
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()) this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen())
} }
}, { },
{
label: 'Toggle &Developer Tools', label: 'Toggle &Developer Tools',
accelerator: 'Alt+Ctrl+I', accelerator: 'Alt+Ctrl+I',
click: () => { click: () => {
this.mainWindow.toggleDevTools() this.mainWindow.toggleDevTools()
} }
}] : [{ }
]
: [
{
label: 'Toggle &Full Screen', label: 'Toggle &Full Screen',
accelerator: 'F11', accelerator: 'F11',
click: () => { click: () => {
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()) this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen())
} }
}] }
}, { ]
},
{
label: 'Help', label: 'Help',
submenu: [{ submenu: [
{
label: 'Learn More', label: 'Learn More',
click() { click() {
shell.openExternal('https://zap.jackmallers.com/') shell.openExternal('https://zap.jackmallers.com/')
} }
}, { },
{
label: 'Documentation', label: 'Documentation',
click() { click() {
shell.openExternal('https://github.com/LN-Zap/zap-desktop') shell.openExternal('https://github.com/LN-Zap/zap-desktop')
} }
}, { },
{
label: 'Community Discussions', label: 'Community Discussions',
click() { click() {
shell.openExternal('zaphq.slack.com') shell.openExternal('zaphq.slack.com')
} }
}, { },
{
label: 'Search Issues', label: 'Search Issues',
click() { click() {
shell.openExternal('https://github.com/LN-Zap/zap-desktop/issues') shell.openExternal('https://github.com/LN-Zap/zap-desktop/issues')
} }
}] }
}] ]
}
]
return templateDefault return templateDefault
} }

5
app/package.json

@ -10,12 +10,11 @@
"url": "https://github.com/LN-Zap/zap-desktop" "url": "https://github.com/LN-Zap/zap-desktop"
}, },
"scripts": { "scripts": {
"postinstall": "npm rebuild --runtime=electron --target=1.8.4 --disturl=https://atom.io/download/atom-shell --build-from-source", "postinstall": "npm rebuild --runtime=electron --target=2.0.2 --disturl=https://atom.io/download/electron"
"install-grpc": "cd node_modules/grpc && git submodule update --init && npm run electron-build -- --target=1.8.4"
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"grpc": "1.12.1", "grpc": "^1.12.3",
"ps-node": "^0.1.6", "ps-node": "^0.1.6",
"react-icons": "^2.2.5" "react-icons": "^2.2.5"
} }

65
app/reducers/activity.js

@ -96,9 +96,10 @@ const ACTION_HANDLERS = {
[CHANGE_FILTER]: (state, { filter }) => ({ ...state, filter, filterPulldown: false }), [CHANGE_FILTER]: (state, { filter }) => ({ ...state, filter, filterPulldown: false }),
[TOGGLE_PULLDOWN]: state => ({ ...state, filterPulldown: !state.filterPulldown }), [TOGGLE_PULLDOWN]: state => ({ ...state, filterPulldown: !state.filterPulldown }),
[SET_ACTIVITY_MODAL_CURRENCY_FILTERS]: (state, { showCurrencyFilters }) => ( [SET_ACTIVITY_MODAL_CURRENCY_FILTERS]: (state, { showCurrencyFilters }) => ({
{ ...state, modal: { modalType: state.modal.modalType, modalProps: state.modal.modalProps, showCurrencyFilters } } ...state,
), modal: { modalType: state.modal.modalType, modalProps: state.modal.modalProps, showCurrencyFilters }
}),
[UPDATE_SEARCH_ACTIVE]: (state, { searchActive }) => ({ ...state, searchActive }), [UPDATE_SEARCH_ACTIVE]: (state, { searchActive }) => ({ ...state, searchActive }),
[UPDATE_SEARCH_TEXT]: (state, { searchText }) => ({ ...state, searchText }) [UPDATE_SEARCH_TEXT]: (state, { searchText }) => ({ ...state, searchText })
@ -115,17 +116,21 @@ const paymentsSelector = state => state.payment.payments
const invoicesSelector = state => state.invoice.invoices const invoicesSelector = state => state.invoice.invoices
const transactionsSelector = state => state.transaction.transactions const transactionsSelector = state => state.transaction.transactions
const invoiceExpired = (invoice) => { const invoiceExpired = invoice => {
const expiresAt = (parseInt(invoice.creation_date, 10) + parseInt(invoice.expiry, 10)) const expiresAt = parseInt(invoice.creation_date, 10) + parseInt(invoice.expiry, 10)
return expiresAt < (Date.now() / 1000) return expiresAt < Date.now() / 1000
} }
// helper function that returns invoice, payment or transaction timestamp // helper function that returns invoice, payment or transaction timestamp
function returnTimestamp(transaction) { function returnTimestamp(transaction) {
// if on-chain txn // if on-chain txn
if (Object.prototype.hasOwnProperty.call(transaction, 'time_stamp')) { return transaction.time_stamp } if (Object.prototype.hasOwnProperty.call(transaction, 'time_stamp')) {
return transaction.time_stamp
}
// if invoice that has been paid // if invoice that has been paid
if (transaction.settled) { return transaction.settle_date } if (transaction.settled) {
return transaction.settle_date
}
// if invoice that has not been paid or an LN payment // if invoice that has not been paid or an LN payment
return transaction.creation_date return transaction.creation_date
} }
@ -141,7 +146,9 @@ function groupData(data) {
const date = d.getDate() const date = d.getDate()
const title = `${months[d.getMonth()]} ${date}, ${d.getFullYear()}` const title = `${months[d.getMonth()]} ${date}, ${d.getFullYear()}`
if (!arr[title]) { arr[title] = [] } if (!arr[title]) {
arr[title] = []
}
arr[title].push({ el }) arr[title].push({ el })
@ -175,36 +182,34 @@ const allActivity = createSelector(
invoicesSelector, invoicesSelector,
transactionsSelector, transactionsSelector,
(searchText, payments, invoices, transactions) => { (searchText, payments, invoices, transactions) => {
const searchedArr = [...payments, ...invoices, ...transactions].filter((tx) => { const searchedArr = [...payments, ...invoices, ...transactions].filter(tx => {
if ((tx.tx_hash && tx.tx_hash.includes(searchText)) || if (
(tx.tx_hash && tx.tx_hash.includes(searchText)) ||
(tx.payment_hash && tx.payment_hash.includes(searchText)) || (tx.payment_hash && tx.payment_hash.includes(searchText)) ||
(tx.payment_request && tx.payment_request.includes(searchText))) { (tx.payment_request && tx.payment_request.includes(searchText))
) {
return true return true
} }
return false return false
}) })
if (!searchedArr.length) { return [] } if (!searchedArr.length) {
return []
}
return groupAll(searchedArr) return groupAll(searchedArr)
} }
) )
const invoiceActivity = createSelector( const invoiceActivity = createSelector(invoicesSelector, invoices => groupAll(invoices))
invoicesSelector,
invoices => groupAll(invoices)
)
const sentActivity = createSelector( const sentActivity = createSelector(transactionsSelector, paymentsSelector, (transactions, payments) =>
transactionsSelector, groupAll([...transactions.filter(transaction => transaction.amount < 0), ...payments])
paymentsSelector,
(transactions, payments) => groupAll([...transactions.filter(transaction => transaction.amount < 0), ...payments])
) )
const pendingActivity = createSelector( const pendingActivity = createSelector(invoicesSelector, invoices =>
invoicesSelector, groupAll(invoices.filter(invoice => !invoice.settled && !invoiceExpired(invoice)))
invoices => groupAll(invoices.filter(invoice => !invoice.settled && !invoiceExpired(invoice)))
) )
const FILTERS = { const FILTERS = {
@ -214,20 +219,12 @@ const FILTERS = {
PENDING_ACTIVITY: pendingActivity PENDING_ACTIVITY: pendingActivity
} }
activitySelectors.currentActivity = createSelector( activitySelectors.currentActivity = createSelector(filterSelector, filter => FILTERS[filter.key])
filterSelector,
filter => FILTERS[filter.key]
)
activitySelectors.nonActiveFilters = createSelector( activitySelectors.nonActiveFilters = createSelector(filtersSelector, filterSelector, (filters, filter) => filters.filter(f => f.key !== filter.key))
filtersSelector,
filterSelector,
(filters, filter) => filters.filter(f => f.key !== filter.key)
)
export { activitySelectors } export { activitySelectors }
// ------------------------------------ // ------------------------------------
// Reducer // Reducer
// ------------------------------------ // ------------------------------------

2
app/reducers/address.js

@ -37,7 +37,7 @@ export function closeWalletModal() {
} }
// Send IPC event for getinfo // Send IPC event for getinfo
export const newAddress = type => async (dispatch) => { export const newAddress = type => async dispatch => {
dispatch(getAddress()) dispatch(getAddress())
ipcRenderer.send('lnd', { msg: 'newaddress', data: { type: addressTypes[type] } }) ipcRenderer.send('lnd', { msg: 'newaddress', data: { type: addressTypes[type] } })
} }

15
app/reducers/balance.js

@ -15,13 +15,13 @@ export function getBalance() {
} }
// Send IPC event for balance // Send IPC event for balance
export const fetchBalance = () => async (dispatch) => { export const fetchBalance = () => async dispatch => {
dispatch(getBalance()) dispatch(getBalance())
ipcRenderer.send('lnd', { msg: 'balance' }) ipcRenderer.send('lnd', { msg: 'balance' })
} }
// Receive IPC event for balance // Receive IPC event for balance
export const receiveBalance = (event, { walletBalance, channelBalance }) => (dispatch) => { export const receiveBalance = (event, { walletBalance, channelBalance }) => dispatch => {
dispatch({ type: RECEIVE_BALANCE, walletBalance, channelBalance }) dispatch({ type: RECEIVE_BALANCE, walletBalance, channelBalance })
} }
@ -30,11 +30,12 @@ export const receiveBalance = (event, { walletBalance, channelBalance }) => (dis
// ------------------------------------ // ------------------------------------
const ACTION_HANDLERS = { const ACTION_HANDLERS = {
[GET_BALANCE]: state => ({ ...state, balanceLoading: true }), [GET_BALANCE]: state => ({ ...state, balanceLoading: true }),
[RECEIVE_BALANCE]: (state, { walletBalance, channelBalance }) => ( [RECEIVE_BALANCE]: (state, { walletBalance, channelBalance }) => ({
{ ...state,
...state, balanceLoading: false, walletBalance, channelBalance balanceLoading: false,
} walletBalance,
) channelBalance
})
} }
// ------------------------------------ // ------------------------------------

133
app/reducers/channels.js

@ -54,7 +54,6 @@ export function setChannelForm(form) {
} }
} }
export function setChannel(channel) { export function setChannel(channel) {
return { return {
type: SET_CHANNEL, type: SET_CHANNEL,
@ -167,7 +166,7 @@ export function receiveSuggestedNodes(suggestedNodes) {
} }
} }
export const fetchSuggestedNodes = () => async (dispatch) => { export const fetchSuggestedNodes = () => async dispatch => {
dispatch(getSuggestedNodes()) dispatch(getSuggestedNodes())
const suggestedNodes = await requestSuggestedNodes() const suggestedNodes = await requestSuggestedNodes()
@ -175,7 +174,7 @@ export const fetchSuggestedNodes = () => async (dispatch) => {
} }
// Send IPC event for peers // Send IPC event for peers
export const fetchChannels = () => async (dispatch) => { export const fetchChannels = () => async dispatch => {
dispatch(getChannels()) dispatch(getChannels())
ipcRenderer.send('lnd', { msg: 'channels' }) ipcRenderer.send('lnd', { msg: 'channels' })
} }
@ -184,9 +183,7 @@ export const fetchChannels = () => async (dispatch) => {
export const receiveChannels = (event, { channels, pendingChannels }) => dispatch => dispatch({ type: RECEIVE_CHANNELS, channels, pendingChannels }) export const receiveChannels = (event, { channels, pendingChannels }) => dispatch => dispatch({ type: RECEIVE_CHANNELS, channels, pendingChannels })
// Send IPC event for opening a channel // Send IPC event for opening a channel
export const openChannel = ({ export const openChannel = ({ pubkey, host, local_amt }) => dispatch => {
pubkey, host, local_amt
}) => (dispatch) => {
const localamt = btc.btcToSatoshis(local_amt) const localamt = btc.btcToSatoshis(local_amt)
dispatch(openingChannel()) dispatch(openingChannel())
@ -197,42 +194,42 @@ export const openChannel = ({
// TODO: Decide how to handle streamed updates for channels // TODO: Decide how to handle streamed updates for channels
// Receive IPC event for openChannel // Receive IPC event for openChannel
export const channelSuccessful = () => (dispatch) => { export const channelSuccessful = () => dispatch => {
dispatch(fetchChannels()) dispatch(fetchChannels())
} }
// Receive IPC event for updated channel // Receive IPC event for updated channel
export const pushchannelupdated = (event, { pubkey }) => (dispatch) => { export const pushchannelupdated = (event, { pubkey }) => dispatch => {
dispatch(fetchChannels()) dispatch(fetchChannels())
dispatch(removeLoadingPubkey(pubkey)) dispatch(removeLoadingPubkey(pubkey))
} }
// Receive IPC event for channel end // Receive IPC event for channel end
export const pushchannelend = event => (dispatch) => { // eslint-disable-line no-unused-vars // eslint-disable-next-line no-unused-vars
export const pushchannelend = event => dispatch => {
dispatch(fetchChannels()) dispatch(fetchChannels())
} }
// Receive IPC event for channel error // Receive IPC event for channel error
export const pushchannelerror = (event, { pubkey, error }) => (dispatch) => { export const pushchannelerror = (event, { pubkey, error }) => dispatch => {
dispatch(openingFailure()) dispatch(openingFailure())
dispatch(setError(error)) dispatch(setError(error))
dispatch(removeLoadingPubkey(pubkey)) dispatch(removeLoadingPubkey(pubkey))
} }
// Receive IPC event for channel status // Receive IPC event for channel status
export const pushchannelstatus = (event, data) => (dispatch) => { // eslint-disable-line no-unused-vars // eslint-disable-next-line no-unused-vars
export const pushchannelstatus = (event, data) => dispatch => {
dispatch(fetchChannels()) dispatch(fetchChannels())
} }
// Send IPC event for opening a channel // Send IPC event for opening a channel
export const closeChannel = ({ channel_point, chan_id, force }) => (dispatch) => { export const closeChannel = ({ channel_point, chan_id, force }) => dispatch => {
dispatch(closingChannel()) dispatch(closingChannel())
dispatch(addClosingChanId(chan_id)) dispatch(addClosingChanId(chan_id))
const [funding_txid, output_index] = channel_point.split(':') const [funding_txid, output_index] = channel_point.split(':')
ipcRenderer.send( ipcRenderer.send('lnd', {
'lnd',
{
msg: 'closeChannel', msg: 'closeChannel',
data: { data: {
channel_point: { channel_point: {
@ -241,43 +238,44 @@ export const closeChannel = ({ channel_point, chan_id, force }) => (dispatch) =>
}, },
force force
} }
} })
)
} }
// TODO: Decide how to handle streamed updates for closing channels // TODO: Decide how to handle streamed updates for closing channels
// Receive IPC event for closeChannel // Receive IPC event for closeChannel
export const closeChannelSuccessful = () => (dispatch) => { export const closeChannelSuccessful = () => dispatch => {
dispatch(fetchChannels()) dispatch(fetchChannels())
} }
// Receive IPC event for updated closing channel // Receive IPC event for updated closing channel
export const pushclosechannelupdated = (event, { chan_id }) => (dispatch) => { export const pushclosechannelupdated = (event, { chan_id }) => dispatch => {
dispatch(fetchChannels()) dispatch(fetchChannels())
dispatch(removeClosingChanId(chan_id)) dispatch(removeClosingChanId(chan_id))
dispatch(closeContactModal()) dispatch(closeContactModal())
} }
// Receive IPC event for closing channel end // Receive IPC event for closing channel end
export const pushclosechannelend = () => (dispatch) => { export const pushclosechannelend = () => dispatch => {
dispatch(fetchChannels()) dispatch(fetchChannels())
} }
// Receive IPC event for closing channel error // Receive IPC event for closing channel error
export const pushclosechannelerror = (event, { error, chan_id }) => (dispatch) => { export const pushclosechannelerror = (event, { error, chan_id }) => dispatch => {
dispatch(setError(error)) dispatch(setError(error))
dispatch(removeClosingChanId(chan_id)) dispatch(removeClosingChanId(chan_id))
} }
// Receive IPC event for closing channel status // Receive IPC event for closing channel status
export const pushclosechannelstatus = () => (dispatch) => { export const pushclosechannelstatus = () => dispatch => {
dispatch(fetchChannels()) dispatch(fetchChannels())
} }
// IPC event for channel graph data // IPC event for channel graph data
export const channelGraphData = (event, data) => (dispatch, getState) => { export const channelGraphData = (event, data) => (dispatch, getState) => {
const { info } = getState() const { info } = getState()
const { channelGraphData: { channel_updates } } = data const {
channelGraphData: { channel_updates }
} = data
// if there are any new channel updates // if there are any new channel updates
if (channel_updates.length) { if (channel_updates.length) {
@ -328,18 +326,17 @@ export function changeFilter(channelFilter) {
// Action Handlers // Action Handlers
// ------------------------------------ // ------------------------------------
const ACTION_HANDLERS = { const ACTION_HANDLERS = {
[SET_CHANNEL_FORM]: (state, { form }) => ( [SET_CHANNEL_FORM]: (state, { form }) => ({ ...state, channelForm: Object.assign({}, state.channelForm, form) }),
{ ...state, channelForm: Object.assign({}, state.channelForm, form) }
),
[SET_CHANNEL]: (state, { channel }) => ({ ...state, channel }), [SET_CHANNEL]: (state, { channel }) => ({ ...state, channel }),
[GET_CHANNELS]: state => ({ ...state, channelsLoading: true }), [GET_CHANNELS]: state => ({ ...state, channelsLoading: true }),
[RECEIVE_CHANNELS]: (state, { channels, pendingChannels }) => ( [RECEIVE_CHANNELS]: (state, { channels, pendingChannels }) => ({
{ ...state,
...state, channelsLoading: false, channels, pendingChannels channelsLoading: false,
} channels,
), pendingChannels
}),
[OPENING_CHANNEL]: state => ({ ...state, openingChannel: true }), [OPENING_CHANNEL]: state => ({ ...state, openingChannel: true }),
[OPENING_FAILURE]: state => ({ ...state, openingChannel: false }), [OPENING_FAILURE]: state => ({ ...state, openingChannel: false }),
@ -351,19 +348,19 @@ const ACTION_HANDLERS = {
[SET_VIEW_TYPE]: (state, { viewType }) => ({ ...state, viewType }), [SET_VIEW_TYPE]: (state, { viewType }) => ({ ...state, viewType }),
[TOGGLE_CHANNEL_PULLDOWN]: state => ({ ...state, filterPulldown: !state.filterPulldown }), [TOGGLE_CHANNEL_PULLDOWN]: state => ({ ...state, filterPulldown: !state.filterPulldown }),
[CHANGE_CHANNEL_FILTER]: (state, { channelFilter }) => ( [CHANGE_CHANNEL_FILTER]: (state, { channelFilter }) => ({ ...state, filterPulldown: false, filter: channelFilter }),
{ ...state, filterPulldown: false, filter: channelFilter }
),
[ADD_LOADING_PUBKEY]: (state, { pubkey }) => ({ ...state, loadingChannelPubkeys: [pubkey, ...state.loadingChannelPubkeys] }), [ADD_LOADING_PUBKEY]: (state, { pubkey }) => ({ ...state, loadingChannelPubkeys: [pubkey, ...state.loadingChannelPubkeys] }),
[REMOVE_LOADING_PUBKEY]: (state, { pubkey }) => ( [REMOVE_LOADING_PUBKEY]: (state, { pubkey }) => ({
{ ...state, loadingChannelPubkeys: state.loadingChannelPubkeys.filter(loadingPubkey => loadingPubkey !== pubkey) } ...state,
), loadingChannelPubkeys: state.loadingChannelPubkeys.filter(loadingPubkey => loadingPubkey !== pubkey)
}),
[ADD_ClOSING_CHAN_ID]: (state, { chanId }) => ({ ...state, closingChannelIds: [chanId, ...state.closingChannelIds] }), [ADD_ClOSING_CHAN_ID]: (state, { chanId }) => ({ ...state, closingChannelIds: [chanId, ...state.closingChannelIds] }),
[REMOVE_ClOSING_CHAN_ID]: (state, { chanId }) => ( [REMOVE_ClOSING_CHAN_ID]: (state, { chanId }) => ({
{ ...state, closingChannelIds: state.closingChannelIds.filter(closingChanId => closingChanId !== chanId) } ...state,
), closingChannelIds: state.closingChannelIds.filter(closingChanId => closingChanId !== chanId)
}),
[OPEN_CONTACT_MODAL]: (state, { channel }) => ({ ...state, contactModal: { isOpen: true, channel } }), [OPEN_CONTACT_MODAL]: (state, { channel }) => ({ ...state, contactModal: { isOpen: true, channel } }),
[CLOSE_CONTACT_MODAL]: state => ({ ...state, contactModal: { isOpen: false, channel: null } }), [CLOSE_CONTACT_MODAL]: state => ({ ...state, contactModal: { isOpen: false, channel: null } }),
@ -397,39 +394,24 @@ const channelMatchesQuery = (channel, nodes, searchQuery) => {
return remoteNodePub.includes(query) || remotePubkey.includes(query) || displayName.includes(query) return remoteNodePub.includes(query) || remotePubkey.includes(query) || displayName.includes(query)
} }
channelsSelectors.channelModalOpen = createSelector( channelsSelectors.channelModalOpen = createSelector(channelSelector, channel => !!channel)
channelSelector,
channel => (!!channel)
)
channelsSelectors.activeChannels = createSelector( channelsSelectors.activeChannels = createSelector(channelsSelector, openChannels => openChannels.filter(channel => channel.active))
channelsSelector,
openChannels => openChannels.filter(channel => channel.active)
)
channelsSelectors.activeChannelPubkeys = createSelector( channelsSelectors.activeChannelPubkeys = createSelector(channelsSelector, openChannels =>
channelsSelector, openChannels.filter(channel => channel.active).map(c => c.remote_pubkey)
openChannels => openChannels.filter(channel => channel.active).map(c => c.remote_pubkey)
) )
channelsSelectors.nonActiveChannels = createSelector( channelsSelectors.nonActiveChannels = createSelector(channelsSelector, openChannels => openChannels.filter(channel => !channel.active))
channelsSelector,
openChannels => openChannels.filter(channel => !channel.active)
)
channelsSelectors.nonActiveChannelPubkeys = createSelector( channelsSelectors.nonActiveChannelPubkeys = createSelector(channelsSelector, openChannels =>
channelsSelector, openChannels.filter(channel => !channel.active).map(c => c.remote_pubkey)
openChannels => openChannels.filter(channel => !channel.active).map(c => c.remote_pubkey)
) )
channelsSelectors.pendingOpenChannels = createSelector( channelsSelectors.pendingOpenChannels = createSelector(pendingOpenChannelsSelector, pendingOpenChannels => pendingOpenChannels)
pendingOpenChannelsSelector,
pendingOpenChannels => pendingOpenChannels
)
channelsSelectors.pendingOpenChannelPubkeys = createSelector( channelsSelectors.pendingOpenChannelPubkeys = createSelector(pendingOpenChannelsSelector, pendingOpenChannels =>
pendingOpenChannelsSelector, pendingOpenChannels.map(pendingChannel => pendingChannel.channel.remote_node_pub)
pendingOpenChannels => pendingOpenChannels.map(pendingChannel => pendingChannel.channel.remote_node_pub)
) )
channelsSelectors.closingPendingChannels = createSelector( channelsSelectors.closingPendingChannels = createSelector(
@ -438,26 +420,17 @@ channelsSelectors.closingPendingChannels = createSelector(
(pendingClosedChannels, pendingForcedClosedChannels) => [...pendingClosedChannels, ...pendingForcedClosedChannels] (pendingClosedChannels, pendingForcedClosedChannels) => [...pendingClosedChannels, ...pendingForcedClosedChannels]
) )
channelsSelectors.activeChanIds = createSelector( channelsSelectors.activeChanIds = createSelector(channelsSelector, channels => channels.map(channel => channel.chan_id))
channelsSelector,
channels => channels.map(channel => channel.chan_id)
)
channelsSelectors.nonActiveFilters = createSelector( channelsSelectors.nonActiveFilters = createSelector(filtersSelector, filterSelector, (filters, channelFilter) =>
filtersSelector, filters.filter(f => f.key !== channelFilter.key)
filterSelector,
(filters, channelFilter) => filters.filter(f => f.key !== channelFilter.key)
) )
channelsSelectors.channelNodes = createSelector( channelsSelectors.channelNodes = createSelector(channelsSelector, nodesSelector, (channels, nodes) => {
channelsSelector,
nodesSelector,
(channels, nodes) => {
const chanPubkeys = channels.map(channel => channel.remote_pubkey) const chanPubkeys = channels.map(channel => channel.remote_pubkey)
return filter(nodes, node => chanPubkeys.includes(node.pub_key)) return filter(nodes, node => chanPubkeys.includes(node.pub_key))
} })
)
const allChannels = createSelector( const allChannels = createSelector(
channelsSelectors.activeChannels, channelsSelectors.activeChannels,
@ -523,7 +496,7 @@ export const currentChannels = createSelector(
nodes nodes
) => { ) => {
// Helper function to deliver correct channel array based on filter // Helper function to deliver correct channel array based on filter
const filteredArray = (filterKey) => { const filteredArray = filterKey => {
switch (filterKey) { switch (filterKey) {
case 'ALL_CHANNELS': case 'ALL_CHANNELS':
return allChannelsArr return allChannelsArr

36
app/reducers/contactsform.js

@ -195,9 +195,7 @@ const manualSearchQuerySelector = state => state.contactsform.manualSearchQuery
const contactCapacitySelector = state => state.contactsform.contactCapacity const contactCapacitySelector = state => state.contactsform.contactCapacity
const currencySelector = state => state.ticker.currency const currencySelector = state => state.ticker.currency
const contactable = node => ( const contactable = node => node.addresses.length > 0
node.addresses.length > 0
)
// comparator to sort the contacts list with contactable contacts first // comparator to sort the contacts list with contactable contacts first
const contactableFirst = (a, b) => { const contactableFirst = (a, b) => {
@ -209,16 +207,15 @@ const contactableFirst = (a, b) => {
return 0 return 0
} }
contactFormSelectors.filteredNetworkNodes = createSelector( contactFormSelectors.filteredNetworkNodes = createSelector(networkNodesSelector, searchQuerySelector, (nodes, searchQuery) => {
networkNodesSelector,
searchQuerySelector,
(nodes, searchQuery) => {
// If there is no search query default to showing the first 20 nodes from the nodes array // If there is no search query default to showing the first 20 nodes from the nodes array
// (performance hit to render the entire thing by default) // (performance hit to render the entire thing by default)
// if (!searchQuery.length) { return nodes.sort(contactableFirst).slice(0, 20) } // if (!searchQuery.length) { return nodes.sort(contactableFirst).slice(0, 20) }
// return an empty array if there is no search query // return an empty array if there is no search query
if (!searchQuery.length) { return [] } if (!searchQuery.length) {
return []
}
// if there is an '@' in the search query we are assuming they are using the format pubkey@host // if there is an '@' in the search query we are assuming they are using the format pubkey@host
// we can ignore the '@' and the host and just grab the pubkey for our search // we can ignore the '@' and the host and just grab the pubkey for our search
@ -230,26 +227,27 @@ contactFormSelectors.filteredNetworkNodes = createSelector(
// if we don't limit the nodes returned then we take a huge performance hit // if we don't limit the nodes returned then we take a huge performance hit
// rendering thousands of nodes potentially, so we just render 20 for the time being // rendering thousands of nodes potentially, so we just render 20 for the time being
return list.slice(0, 20) return list.slice(0, 20)
} })
)
contactFormSelectors.showManualForm = createSelector( contactFormSelectors.showManualForm = createSelector(
searchQuerySelector, searchQuerySelector,
contactFormSelectors.filteredNetworkNodes, contactFormSelectors.filteredNetworkNodes,
(searchQuery, filteredNetworkNodes) => { (searchQuery, filteredNetworkNodes) => {
if (!searchQuery.length) { return false } if (!searchQuery.length) {
return false
}
const connectableNodes = filteredNetworkNodes.filter(node => node.addresses.length > 0) const connectableNodes = filteredNetworkNodes.filter(node => node.addresses.length > 0)
if (!filteredNetworkNodes.length || !connectableNodes.length) { return true } if (!filteredNetworkNodes.length || !connectableNodes.length) {
return true
}
return false return false
} }
) )
contactFormSelectors.manualFormIsValid = createSelector( contactFormSelectors.manualFormIsValid = createSelector(manualSearchQuerySelector, input => {
manualSearchQuerySelector,
(input) => {
const errors = {} const errors = {}
if (!input.length || !input.includes('@')) { if (!input.length || !input.includes('@')) {
errors.manualInput = 'Invalid format' errors.manualInput = 'Invalid format'
@ -258,21 +256,21 @@ contactFormSelectors.manualFormIsValid = createSelector(
errors, errors,
isValid: isEmpty(errors) isValid: isEmpty(errors)
} }
} })
)
contactFormSelectors.contactFormUsdAmount = createSelector( contactFormSelectors.contactFormUsdAmount = createSelector(
contactCapacitySelector, contactCapacitySelector,
currencySelector, currencySelector,
tickerSelectors.currentTicker, tickerSelectors.currentTicker,
(amount, currency, ticker) => { (amount, currency, ticker) => {
if (!ticker || !ticker.price_usd) { return false } if (!ticker || !ticker.price_usd) {
return false
}
return btc.convert(currency, 'usd', amount, ticker.price_usd) return btc.convert(currency, 'usd', amount, ticker.price_usd)
} }
) )
export { contactFormSelectors } export { contactFormSelectors }
// ------------------------------------ // ------------------------------------

2
app/reducers/error.js

@ -32,7 +32,7 @@ export function clearError() {
// ------------------------------------ // ------------------------------------
const ACTION_HANDLERS = { const ACTION_HANDLERS = {
[SET_ERROR]: (state, { error }) => ({ ...state, error }), [SET_ERROR]: (state, { error }) => ({ ...state, error }),
[CLEAR_ERROR]: () => (initialState) [CLEAR_ERROR]: () => initialState
} }
// ------------------------------------ // ------------------------------------

43
app/reducers/info.js

@ -1,6 +1,6 @@
import { createSelector } from 'reselect' import bitcoin from 'bitcoinjs-lib'
import { ipcRenderer } from 'electron' import { ipcRenderer } from 'electron'
import { blockExplorer } from 'utils'
// ------------------------------------ // ------------------------------------
// Constants // Constants
@ -26,16 +26,30 @@ export function setWalletCurrencyFilters(showWalletCurrencyFilters) {
} }
// Send IPC event for getinfo // Send IPC event for getinfo
export const fetchInfo = () => async (dispatch) => { export const fetchInfo = () => async dispatch => {
dispatch(getInfo()) dispatch(getInfo())
ipcRenderer.send('lnd', { msg: 'info' }) ipcRenderer.send('lnd', { msg: 'info' })
} }
// Receive IPC event for info // Receive IPC event for info
export const receiveInfo = (event, data) => (dispatch) => { export const receiveInfo = (event, data) => dispatch => {
dispatch({ type: RECEIVE_INFO, data }) dispatch({ type: RECEIVE_INFO, data })
} }
const networks = {
testnet: {
name: 'Testnet',
explorerUrl: 'https://testnet.smartbit.com.au',
bitcoinJsNetwork: bitcoin.networks.testnet,
unitPrefix: 't'
},
mainnet: {
name: null, // no name since it is the presumed default
explorerUrl: 'https://smartbit.com.au',
bitcoinJsNetwork: bitcoin.networks.bitcoin,
unitPrefix: ''
}
}
// IPC info fetch failed // IPC info fetch failed
// export const infoFailed = (event, data) => dispatch => {} // export const infoFailed = (event, data) => dispatch => {}
@ -44,7 +58,12 @@ export const receiveInfo = (event, data) => (dispatch) => {
// ------------------------------------ // ------------------------------------
const ACTION_HANDLERS = { const ACTION_HANDLERS = {
[GET_INFO]: state => ({ ...state, infoLoading: true }), [GET_INFO]: state => ({ ...state, infoLoading: true }),
[RECEIVE_INFO]: (state, { data }) => ({ ...state, infoLoading: false, data }), [RECEIVE_INFO]: (state, { data }) => ({
...state,
infoLoading: false,
network: data.testnet ? networks.testnet : networks.mainnet,
data
}),
[SET_WALLET_CURRENCY_FILTERS]: (state, { showWalletCurrencyFilters }) => ({ ...state, showWalletCurrencyFilters }) [SET_WALLET_CURRENCY_FILTERS]: (state, { showWalletCurrencyFilters }) => ({ ...state, showWalletCurrencyFilters })
} }
@ -53,23 +72,15 @@ const ACTION_HANDLERS = {
// ------------------------------------ // ------------------------------------
const initialState = { const initialState = {
infoLoading: false, infoLoading: false,
network: {},
data: {}, data: {},
showWalletCurrencyFilters: false showWalletCurrencyFilters: false
} }
// Selectors // Selectors
const infoSelectors = {} const infoSelectors = {}
const testnetSelector = state => state.info.data.testnet infoSelectors.testnetSelector = state => state.info.data.testnet
infoSelectors.networkSelector = state => state.info.network
infoSelectors.isTestnet = createSelector(
testnetSelector,
isTestnet => (!!isTestnet)
)
infoSelectors.explorerLinkBase = createSelector(
infoSelectors.isTestnet,
isTestnet => (isTestnet ? blockExplorer.testnetUrl : blockExplorer.mainnetUrl)
)
export { infoSelectors } export { infoSelectors }

43
app/reducers/invoice.js

@ -75,19 +75,19 @@ export function sendInvoice() {
} }
// Send IPC event for a specific invoice // Send IPC event for a specific invoice
export const fetchInvoice = payreq => (dispatch) => { export const fetchInvoice = payreq => dispatch => {
dispatch(getInvoice()) dispatch(getInvoice())
ipcRenderer.send('lnd', { msg: 'invoice', data: { payreq } }) ipcRenderer.send('lnd', { msg: 'invoice', data: { payreq } })
} }
// Receive IPC event for form invoice // Receive IPC event for form invoice
export const receiveFormInvoice = (event, invoice) => (dispatch) => { export const receiveFormInvoice = (event, invoice) => dispatch => {
dispatch(setPayInvoice(invoice)) dispatch(setPayInvoice(invoice))
dispatch({ type: RECEIVE_FORM_INVOICE }) dispatch({ type: RECEIVE_FORM_INVOICE })
} }
// Send IPC event for invoices // Send IPC event for invoices
export const fetchInvoices = () => (dispatch) => { export const fetchInvoices = () => dispatch => {
dispatch(getInvoices()) dispatch(getInvoices())
ipcRenderer.send('lnd', { msg: 'invoices' }) ipcRenderer.send('lnd', { msg: 'invoices' })
} }
@ -96,7 +96,7 @@ export const fetchInvoices = () => (dispatch) => {
export const receiveInvoices = (event, { invoices }) => dispatch => dispatch({ type: RECEIVE_INVOICES, invoices }) export const receiveInvoices = (event, { invoices }) => dispatch => dispatch({ type: RECEIVE_INVOICES, invoices })
// Send IPC event for creating an invoice // Send IPC event for creating an invoice
export const createInvoice = (amount, memo, currency) => (dispatch) => { export const createInvoice = (amount, memo, currency) => dispatch => {
// backend needs value in satoshis no matter what currency we are using // backend needs value in satoshis no matter what currency we are using
const value = btc.convert(currency, 'sats', amount) const value = btc.convert(currency, 'sats', amount)
@ -105,7 +105,7 @@ export const createInvoice = (amount, memo, currency) => (dispatch) => {
} }
// Receive IPC event for newly created invoice // Receive IPC event for newly created invoice
export const createdInvoice = (event, invoice) => (dispatch) => { export const createdInvoice = (event, invoice) => dispatch => {
// Close the form modal once the payment was succesful // Close the form modal once the payment was succesful
dispatch(setFormType(null)) dispatch(setFormType(null))
@ -122,20 +122,20 @@ export const createdInvoice = (event, invoice) => (dispatch) => {
dispatch(showActivityModal('INVOICE', { invoice })) dispatch(showActivityModal('INVOICE', { invoice }))
} }
export const invoiceFailed = (event, { error }) => (dispatch) => { export const invoiceFailed = (event, { error }) => dispatch => {
dispatch({ type: INVOICE_FAILED }) dispatch({ type: INVOICE_FAILED })
dispatch(setError(error)) dispatch(setError(error))
} }
// Listen for invoice updates pushed from backend from subscribeToInvoices // Listen for invoice updates pushed from backend from subscribeToInvoices
export const invoiceUpdate = (event, { invoice }) => (dispatch) => { export const invoiceUpdate = (event, { invoice }) => dispatch => {
dispatch({ type: UPDATE_INVOICE, invoice }) dispatch({ type: UPDATE_INVOICE, invoice })
// Fetch new balance // Fetch new balance
dispatch(fetchBalance()) dispatch(fetchBalance())
// HTML 5 desktop notification for the invoice update // HTML 5 desktop notification for the invoice update
const notifTitle = 'You\'ve been Zapped' const notifTitle = "You've been Zapped"
const notifBody = 'Congrats, someone just paid an invoice of yours' const notifBody = 'Congrats, someone just paid an invoice of yours'
showNotification(notifTitle, notifBody) showNotification(notifTitle, notifBody)
@ -156,14 +156,14 @@ const ACTION_HANDLERS = {
[RECEIVE_INVOICES]: (state, { invoices }) => ({ ...state, invoiceLoading: false, invoices }), [RECEIVE_INVOICES]: (state, { invoices }) => ({ ...state, invoiceLoading: false, invoices }),
[SEND_INVOICE]: state => ({ ...state, invoiceLoading: true }), [SEND_INVOICE]: state => ({ ...state, invoiceLoading: true }),
[INVOICE_SUCCESSFUL]: (state, { invoice }) => ( [INVOICE_SUCCESSFUL]: (state, { invoice }) => ({ ...state, invoiceLoading: false, invoices: [invoice, ...state.invoices] }),
{ ...state, invoiceLoading: false, invoices: [invoice, ...state.invoices] }
),
[INVOICE_FAILED]: state => ({ ...state, invoiceLoading: false, data: null }), [INVOICE_FAILED]: state => ({ ...state, invoiceLoading: false, data: null }),
[UPDATE_INVOICE]: (state, action) => { [UPDATE_INVOICE]: (state, action) => {
const updatedInvoices = state.invoices.map((invoice) => { const updatedInvoices = state.invoices.map(invoice => {
if (invoice.r_hash.toString('hex') !== action.invoice.r_hash.toString('hex')) { return invoice } if (invoice.r_hash.toString('hex') !== action.invoice.r_hash.toString('hex')) {
return invoice
}
return { return {
...invoice, ...invoice,
@ -180,21 +180,14 @@ const invoiceSelector = state => state.invoice.invoice
const invoicesSelector = state => state.invoice.invoices const invoicesSelector = state => state.invoice.invoices
const invoicesSearchTextSelector = state => state.invoice.invoicesSearchText const invoicesSearchTextSelector = state => state.invoice.invoicesSearchText
invoiceSelectors.invoiceModalOpen = createSelector( invoiceSelectors.invoiceModalOpen = createSelector(invoiceSelector, invoice => !!invoice)
invoiceSelector,
invoice => (!!invoice)
)
invoiceSelectors.invoices = createSelector( invoiceSelectors.invoices = createSelector(invoicesSelector, invoicesSearchTextSelector, (invoices, invoicesSearchText) =>
invoicesSelector, invoices.filter(invoice => invoice.memo.includes(invoicesSearchText))
invoicesSearchTextSelector,
(invoices, invoicesSearchText) => invoices.filter(invoice => invoice.memo.includes(invoicesSearchText))
) )
invoiceSelectors.invoices = createSelector( invoiceSelectors.invoices = createSelector(invoicesSelector, invoicesSearchTextSelector, (invoices, invoicesSearchText) =>
invoicesSelector, invoices.filter(invoice => invoice.memo.includes(invoicesSearchText))
invoicesSearchTextSelector,
(invoices, invoicesSearchText) => invoices.filter(invoice => invoice.memo.includes(invoicesSearchText))
) )
export { invoiceSelectors } export { invoiceSelectors }

10
app/reducers/ipc.js

@ -6,19 +6,16 @@ import { receiveCryptocurrency } from './ticker'
import { receivePeers, connectSuccess, disconnectSuccess, connectFailure } from './peers' import { receivePeers, connectSuccess, disconnectSuccess, connectFailure } from './peers'
import { import {
receiveChannels, receiveChannels,
channelSuccessful, channelSuccessful,
pushchannelupdated, pushchannelupdated,
pushchannelend, pushchannelend,
pushchannelerror, pushchannelerror,
pushchannelstatus, pushchannelstatus,
closeChannelSuccessful, closeChannelSuccessful,
pushclosechannelupdated, pushclosechannelupdated,
pushclosechannelend, pushclosechannelend,
pushclosechannelerror, pushclosechannelerror,
pushclosechannelstatus, pushclosechannelstatus,
channelGraphData, channelGraphData,
channelGraphStatus channelGraphStatus
} from './channels' } from './channels'
@ -26,12 +23,7 @@ import { lightningPaymentUri } from './payform'
import { receivePayments, paymentSuccessful, paymentFailed } from './payment' import { receivePayments, paymentSuccessful, paymentFailed } from './payment'
import { receiveInvoices, createdInvoice, receiveFormInvoice, invoiceUpdate, invoiceFailed } from './invoice' import { receiveInvoices, createdInvoice, receiveFormInvoice, invoiceUpdate, invoiceFailed } from './invoice'
import { receiveBalance } from './balance' import { receiveBalance } from './balance'
import { import { receiveTransactions, transactionSuccessful, transactionError, newTransaction } from './transaction'
receiveTransactions,
transactionSuccessful,
transactionError,
newTransaction
} from './transaction'
import { receiveDescribeNetwork, receiveQueryRoutes, receiveInvoiceAndQueryRoutes } from './network' import { receiveDescribeNetwork, receiveQueryRoutes, receiveInvoiceAndQueryRoutes } from './network'

21
app/reducers/lnd.js

@ -26,7 +26,7 @@ export const GRPC_CONNECTED = 'GRPC_CONNECTED'
export const lndSyncing = () => dispatch => dispatch({ type: START_SYNCING }) export const lndSyncing = () => dispatch => dispatch({ type: START_SYNCING })
// Receive IPC event for LND stoping sync // Receive IPC event for LND stoping sync
export const lndSynced = () => (dispatch) => { export const lndSynced = () => dispatch => {
// Fetch data now that we know LND is synced // Fetch data now that we know LND is synced
dispatch(fetchTicker()) dispatch(fetchTicker())
dispatch(fetchBalance()) dispatch(fetchBalance())
@ -36,7 +36,7 @@ export const lndSynced = () => (dispatch) => {
// HTML 5 desktop notification for the new transaction // HTML 5 desktop notification for the new transaction
const notifTitle = 'Lightning Node Synced' const notifTitle = 'Lightning Node Synced'
const notifBody = 'Visa who? You\'re your own payment processor now!' const notifBody = "Visa who? You're your own payment processor now!"
showNotification(notifTitle, notifBody) showNotification(notifTitle, notifBody)
} }
@ -46,7 +46,7 @@ export const grpcDisconnected = () => dispatch => dispatch({ type: GRPC_DISCONNE
export const grpcConnected = () => dispatch => dispatch({ type: GRPC_CONNECTED }) export const grpcConnected = () => dispatch => dispatch({ type: GRPC_CONNECTED })
// Receive IPC event for LND streaming a line // Receive IPC event for LND streaming a line
export const lndStdout = (event, line) => (dispatch) => { export const lndStdout = (event, line) => dispatch => {
let height let height
let trimmed let trimmed
@ -63,7 +63,6 @@ export const lndStdout = (event, line) => (dispatch) => {
dispatch({ type: RECEIVE_LINE, lndBlockHeight: height }) dispatch({ type: RECEIVE_LINE, lndBlockHeight: height })
} }
export function getBlockHeight() { export function getBlockHeight() {
return { return {
type: GET_BLOCK_HEIGHT type: GET_BLOCK_HEIGHT
@ -78,7 +77,7 @@ export function receiveBlockHeight(blockHeight) {
} }
// Fetch current block height // Fetch current block height
export const fetchBlockHeight = () => async (dispatch) => { export const fetchBlockHeight = () => async dispatch => {
dispatch(getBlockHeight()) dispatch(getBlockHeight())
const blockData = await requestBlockHeight() const blockData = await requestBlockHeight()
dispatch(receiveBlockHeight(blockData.blocks[0].height)) dispatch(receiveBlockHeight(blockData.blocks[0].height))
@ -119,17 +118,15 @@ const lndSelectors = {}
const blockHeightSelector = state => state.lnd.blockHeight const blockHeightSelector = state => state.lnd.blockHeight
const lndBlockHeightSelector = state => state.lnd.lndBlockHeight const lndBlockHeightSelector = state => state.lnd.lndBlockHeight
lndSelectors.syncPercentage = createSelector( lndSelectors.syncPercentage = createSelector(blockHeightSelector, lndBlockHeightSelector, (blockHeight, lndBlockHeight) => {
blockHeightSelector,
lndBlockHeightSelector,
(blockHeight, lndBlockHeight) => {
const percentage = Math.floor((lndBlockHeight / blockHeight) * 100) const percentage = Math.floor((lndBlockHeight / blockHeight) * 100)
if (percentage === Infinity) { return '' } if (percentage === Infinity) {
return ''
}
return percentage return percentage
} })
)
export { lndSelectors } export { lndSelectors }

62
app/reducers/network.js

@ -128,16 +128,15 @@ export function clearSelectedChannels() {
} }
// Send IPC event for describeNetwork // Send IPC event for describeNetwork
export const fetchDescribeNetwork = () => (dispatch) => { export const fetchDescribeNetwork = () => dispatch => {
dispatch(getDescribeNetwork()) dispatch(getDescribeNetwork())
ipcRenderer.send('lnd', { msg: 'describeNetwork' }) ipcRenderer.send('lnd', { msg: 'describeNetwork' })
} }
// Receive IPC event for describeNetwork // Receive IPC event for describeNetwork
export const receiveDescribeNetwork = (event, { nodes, edges }) => dispatch => export const receiveDescribeNetwork = (event, { nodes, edges }) => dispatch => dispatch({ type: RECEIVE_DESCRIBE_NETWORK, nodes, edges })
dispatch({ type: RECEIVE_DESCRIBE_NETWORK, nodes, edges })
export const queryRoutes = (pubkey, amount) => (dispatch) => { export const queryRoutes = (pubkey, amount) => dispatch => {
dispatch(getQueryRoutes(pubkey)) dispatch(getQueryRoutes(pubkey))
ipcRenderer.send('lnd', { msg: 'queryRoutes', data: { pubkey, amount } }) ipcRenderer.send('lnd', { msg: 'queryRoutes', data: { pubkey, amount } })
} }
@ -145,13 +144,12 @@ export const queryRoutes = (pubkey, amount) => (dispatch) => {
export const receiveQueryRoutes = (event, { routes }) => dispatch => dispatch({ type: RECEIVE_QUERY_ROUTES, routes }) export const receiveQueryRoutes = (event, { routes }) => dispatch => dispatch({ type: RECEIVE_QUERY_ROUTES, routes })
// take a payreq and query routes for it // take a payreq and query routes for it
export const fetchInvoiceAndQueryRoutes = payreq => (dispatch) => { export const fetchInvoiceAndQueryRoutes = payreq => dispatch => {
dispatch(getInvoiceAndQueryRoutes()) dispatch(getInvoiceAndQueryRoutes())
ipcRenderer.send('lnd', { msg: 'getInvoiceAndQueryRoutes', data: { payreq } }) ipcRenderer.send('lnd', { msg: 'getInvoiceAndQueryRoutes', data: { payreq } })
} }
export const receiveInvoiceAndQueryRoutes = (event, { routes }) => dispatch => export const receiveInvoiceAndQueryRoutes = (event, { routes }) => dispatch => dispatch({ type: RECEIVE_INFO_AND_QUERY_ROUTES, routes })
dispatch({ type: RECEIVE_INFO_AND_QUERY_ROUTES, routes })
// ------------------------------------ // ------------------------------------
// Action Handlers // Action Handlers
@ -159,17 +157,18 @@ export const receiveInvoiceAndQueryRoutes = (event, { routes }) => dispatch =>
const ACTION_HANDLERS = { const ACTION_HANDLERS = {
[GET_DESCRIBE_NETWORK]: state => ({ ...state, networkLoading: true }), [GET_DESCRIBE_NETWORK]: state => ({ ...state, networkLoading: true }),
[RECEIVE_DESCRIBE_NETWORK]: (state, { nodes, edges }) => ({ [RECEIVE_DESCRIBE_NETWORK]: (state, { nodes, edges }) => ({
...state, networkLoading: false, nodes, edges ...state,
networkLoading: false,
nodes,
edges
}), }),
[GET_QUERY_ROUTES]: (state, { pubkey }) => ({ ...state, networkLoading: true, selectedNode: { pubkey, routes: [], currentRoute: {} } }), [GET_QUERY_ROUTES]: (state, { pubkey }) => ({ ...state, networkLoading: true, selectedNode: { pubkey, routes: [], currentRoute: {} } }),
[RECEIVE_QUERY_ROUTES]: (state, { routes }) => ( [RECEIVE_QUERY_ROUTES]: (state, { routes }) => ({
{
...state, ...state,
networkLoading: false, networkLoading: false,
selectedNode: { pubkey: state.selectedNode.pubkey, routes, currentRoute: routes[0] } selectedNode: { pubkey: state.selectedNode.pubkey, routes, currentRoute: routes[0] }
} }),
),
[SET_CURRENT_ROUTE]: (state, { route }) => ({ ...state, currentRoute: route }), [SET_CURRENT_ROUTE]: (state, { route }) => ({ ...state, currentRoute: route }),
@ -198,7 +197,8 @@ const ACTION_HANDLERS = {
} }
return { return {
...state, selectedPeers ...state,
selectedPeers
} }
}, },
[CLEAR_SELECTED_PEERS]: state => ({ ...state, selectedPeers: [] }), [CLEAR_SELECTED_PEERS]: state => ({ ...state, selectedPeers: [] }),
@ -215,7 +215,8 @@ const ACTION_HANDLERS = {
} }
return { return {
...state, selectedChannels ...state,
selectedChannels
} }
}, },
[CLEAR_SELECTED_CHANNELS]: state => ({ ...state, selectedChannels: [] }) [CLEAR_SELECTED_CHANNELS]: state => ({ ...state, selectedChannels: [] })
@ -239,20 +240,14 @@ const currentRouteSelector = state => state.network.currentRoute
// } // }
// ) // )
networkSelectors.selectedPeerPubkeys = createSelector( networkSelectors.selectedPeerPubkeys = createSelector(selectedPeersSelector, peers => peers.map(peer => peer.pub_key))
selectedPeersSelector,
peers => peers.map(peer => peer.pub_key)
)
networkSelectors.selectedChannelIds = createSelector( networkSelectors.selectedChannelIds = createSelector(selectedChannelsSelector, channels => channels.map(channel => channel.chan_id))
selectedChannelsSelector,
channels => channels.map(channel => channel.chan_id)
)
networkSelectors.payReqIsLn = createSelector( networkSelectors.payReqIsLn = createSelector(payReqSelector, input => {
payReqSelector, if (!input.startsWith('ln')) {
(input) => { return false
if (!input.startsWith('ln')) { return false } }
try { try {
bech32.decode(input) bech32.decode(input)
@ -260,17 +255,15 @@ networkSelectors.payReqIsLn = createSelector(
} catch (e) { } catch (e) {
return false return false
} }
} })
)
networkSelectors.currentRouteChanIds = createSelector( networkSelectors.currentRouteChanIds = createSelector(currentRouteSelector, route => {
currentRouteSelector, if (!route.hops || !route.hops.length) {
(route) => { return []
if (!route.hops || !route.hops.length) { return [] } }
return route.hops.map(hop => hop.chan_id) return route.hops.map(hop => hop.chan_id)
} })
)
export { networkSelectors } export { networkSelectors }
@ -295,7 +288,6 @@ const initialState = {
selectedChannels: [] selectedChannels: []
} }
// ------------------------------------ // ------------------------------------
// Reducer // Reducer
// ------------------------------------ // ------------------------------------

18
app/reducers/onboarding.js

@ -155,23 +155,23 @@ export function startLnd(options) {
} }
} }
export const submitNewWallet = (wallet_password, cipher_seed_mnemonic, aezeed_passphrase) => (dispatch) => { export const submitNewWallet = (wallet_password, cipher_seed_mnemonic, aezeed_passphrase) => dispatch => {
// once the user submits the data needed to start LND we will alert the app that it should start LND // once the user submits the data needed to start LND we will alert the app that it should start LND
ipcRenderer.send('walletUnlocker', { msg: 'initWallet', data: { wallet_password, cipher_seed_mnemonic, aezeed_passphrase } }) ipcRenderer.send('walletUnlocker', { msg: 'initWallet', data: { wallet_password, cipher_seed_mnemonic, aezeed_passphrase } })
dispatch({ type: CREATING_NEW_WALLET }) dispatch({ type: CREATING_NEW_WALLET })
} }
export const startOnboarding = () => (dispatch) => { export const startOnboarding = () => dispatch => {
dispatch({ type: ONBOARDING_STARTED }) dispatch({ type: ONBOARDING_STARTED })
} }
// Listener from after the LND walletUnlocker has started // Listener from after the LND walletUnlocker has started
export const walletUnlockerStarted = () => (dispatch) => { export const walletUnlockerStarted = () => dispatch => {
dispatch({ type: LND_STARTED }) dispatch({ type: LND_STARTED })
ipcRenderer.send('walletUnlocker', { msg: 'genSeed' }) ipcRenderer.send('walletUnlocker', { msg: 'genSeed' })
} }
export const createWallet = () => (dispatch) => { export const createWallet = () => dispatch => {
ipcRenderer.send('walletUnlocker', { msg: 'genSeed' }) ipcRenderer.send('walletUnlocker', { msg: 'genSeed' })
dispatch({ type: CHANGE_STEP, step: 4 }) dispatch({ type: CHANGE_STEP, step: 4 })
} }
@ -179,31 +179,31 @@ export const createWallet = () => (dispatch) => {
export const successfullyCreatedWallet = () => dispatch => dispatch({ type: ONBOARDING_FINISHED }) export const successfullyCreatedWallet = () => dispatch => dispatch({ type: ONBOARDING_FINISHED })
// Listener for when LND creates and sends us a generated seed // Listener for when LND creates and sends us a generated seed
export const receiveSeed = (event, { cipher_seed_mnemonic }) => (dispatch) => { export const receiveSeed = (event, { cipher_seed_mnemonic }) => dispatch => {
dispatch({ type: CHANGE_STEP, step: 4 }) dispatch({ type: CHANGE_STEP, step: 4 })
// there was no seed and we just generated a new one, send user to the login component // there was no seed and we just generated a new one, send user to the login component
dispatch({ type: SET_SEED, seed: cipher_seed_mnemonic }) dispatch({ type: SET_SEED, seed: cipher_seed_mnemonic })
} }
// Listener for when LND throws an error on seed creation // Listener for when LND throws an error on seed creation
export const receiveSeedError = () => (dispatch) => { export const receiveSeedError = () => dispatch => {
dispatch({ type: SET_HAS_SEED, hasSeed: true }) dispatch({ type: SET_HAS_SEED, hasSeed: true })
// there is already a seed, send user to the login component // there is already a seed, send user to the login component
dispatch({ type: CHANGE_STEP, step: 3 }) dispatch({ type: CHANGE_STEP, step: 3 })
} }
// Unlock an existing wallet with a wallet password // Unlock an existing wallet with a wallet password
export const unlockWallet = wallet_password => (dispatch) => { export const unlockWallet = wallet_password => dispatch => {
ipcRenderer.send('walletUnlocker', { msg: 'unlockWallet', data: { wallet_password } }) ipcRenderer.send('walletUnlocker', { msg: 'unlockWallet', data: { wallet_password } })
dispatch({ type: UNLOCKING_WALLET }) dispatch({ type: UNLOCKING_WALLET })
} }
export const walletUnlocked = () => (dispatch) => { export const walletUnlocked = () => dispatch => {
dispatch({ type: WALLET_UNLOCKED }) dispatch({ type: WALLET_UNLOCKED })
dispatch({ type: ONBOARDING_FINISHED }) dispatch({ type: ONBOARDING_FINISHED })
} }
export const unlockWalletError = () => (dispatch) => { export const unlockWalletError = () => dispatch => {
dispatch({ type: SET_UNLOCK_WALLET_ERROR }) dispatch({ type: SET_UNLOCK_WALLET_ERROR })
} }

55
app/reducers/payform.js

@ -5,6 +5,7 @@ import isEmpty from 'lodash/isEmpty'
import { setFormType } from './form' import { setFormType } from './form'
import { tickerSelectors } from './ticker' import { tickerSelectors } from './ticker'
import { infoSelectors } from './info'
import { btc, bech32 } from '../utils' import { btc, bech32 } from '../utils'
// Initial State // Initial State
@ -78,7 +79,7 @@ export function updatePayErrors(errorsObject) {
} }
} }
export const lightningPaymentUri = (event, { payreq }) => (dispatch) => { export const lightningPaymentUri = (event, { payreq }) => dispatch => {
// Open pay form // Open pay form
dispatch(setFormType('PAY_FORM')) dispatch(setFormType('PAY_FORM'))
// Set payreq // Set payreq
@ -102,7 +103,7 @@ const ACTION_HANDLERS = {
[UPDATE_PAY_ERRORS]: (state, { errorsObject }) => ({ ...state, showErrors: Object.assign(state.showErrors, errorsObject) }), [UPDATE_PAY_ERRORS]: (state, { errorsObject }) => ({ ...state, showErrors: Object.assign(state.showErrors, errorsObject) }),
[RESET_FORM]: () => (initialState) [RESET_FORM]: () => initialState
} }
// ------------------------------------ // ------------------------------------
@ -122,22 +123,19 @@ const sendingPaymentSelector = state => state.payment.sendingPayment
// ticker // ticker
const currencySelector = state => state.ticker.currency const currencySelector = state => state.ticker.currency
payFormSelectors.isOnchain = createSelector( payFormSelectors.isOnchain = createSelector(payInputSelector, infoSelectors.networkSelector, (input, network) => {
payInputSelector,
(input) => {
try { try {
bitcoin.address.toOutputScript(input, bitcoin.networks.testnet) bitcoin.address.toOutputScript(input, network.bitcoinJsNetwork)
return true return true
} catch (e) { } catch (e) {
return false return false
} }
} })
)
payFormSelectors.isLn = createSelector( payFormSelectors.isLn = createSelector(payInputSelector, input => {
payInputSelector, if (!input.startsWith('ln')) {
(input) => { return false
if (!input.startsWith('ln')) { return false } }
try { try {
bech32.decode(input) bech32.decode(input)
@ -145,8 +143,7 @@ payFormSelectors.isLn = createSelector(
} catch (e) { } catch (e) {
return false return false
} }
} })
)
payFormSelectors.currentAmount = createSelector( payFormSelectors.currentAmount = createSelector(
payFormSelectors.isLn, payFormSelectors.isLn,
@ -157,9 +154,9 @@ payFormSelectors.currentAmount = createSelector(
if (isLn) { if (isLn) {
switch (currency) { switch (currency) {
case 'btc': case 'btc':
return btc.satoshisToBtc((invoice.num_satoshis || 0)) return btc.satoshisToBtc(invoice.num_satoshis || 0)
case 'bits': case 'bits':
return btc.satoshisToBits((invoice.num_satoshis || 0)) return btc.satoshisToBits(invoice.num_satoshis || 0)
case 'sats': case 'sats':
return invoice.num_satoshis return invoice.num_satoshis
default: default:
@ -178,19 +175,19 @@ payFormSelectors.usdAmount = createSelector(
currencySelector, currencySelector,
tickerSelectors.currentTicker, tickerSelectors.currentTicker,
(isLn, amount, invoice, currency, ticker) => { (isLn, amount, invoice, currency, ticker) => {
if (!ticker || !ticker.price_usd) { return false } if (!ticker || !ticker.price_usd) {
return false
}
if (isLn) { if (isLn) {
return btc.satoshisToUsd((invoice.num_satoshis || 0), ticker.price_usd) return btc.satoshisToUsd(invoice.num_satoshis || 0, ticker.price_usd)
} }
return btc.convert(currency, 'usd', amount, ticker.price_usd) return btc.convert(currency, 'usd', amount, ticker.price_usd)
} }
) )
payFormSelectors.payInputMin = createSelector( payFormSelectors.payInputMin = createSelector(currencySelector, currency => {
currencySelector,
(currency) => {
switch (currency) { switch (currency) {
case 'btc': case 'btc':
return '0.00000001' return '0.00000001'
@ -201,8 +198,7 @@ payFormSelectors.payInputMin = createSelector(
default: default:
return '0' return '0'
} }
} })
)
payFormSelectors.inputCaption = createSelector( payFormSelectors.inputCaption = createSelector(
payFormSelectors.isOnchain, payFormSelectors.isOnchain,
@ -210,7 +206,9 @@ payFormSelectors.inputCaption = createSelector(
payFormSelectors.currentAmount, payFormSelectors.currentAmount,
currencySelector, currencySelector,
(isOnchain, isLn, amount, currency) => { (isOnchain, isLn, amount, currency) => {
if (!isOnchain && !isLn) { return '' } if (!isOnchain && !isLn) {
return ''
}
if (isOnchain) { if (isOnchain) {
return `You're about to send ${amount} ${currency.toUpperCase()} on-chain which should take around 10 minutes` return `You're about to send ${amount} ${currency.toUpperCase()} on-chain which should take around 10 minutes`
@ -230,11 +228,7 @@ payFormSelectors.showPayLoadingScreen = createSelector(
(sendingTransaction, sendingPayment) => sendingTransaction || sendingPayment (sendingTransaction, sendingPayment) => sendingTransaction || sendingPayment
) )
payFormSelectors.payFormIsValid = createSelector( payFormSelectors.payFormIsValid = createSelector(payFormSelectors.isOnchain, payFormSelectors.isLn, payAmountSelector, (isOnchain, isLn, amount) => {
payFormSelectors.isOnchain,
payFormSelectors.isLn,
payAmountSelector,
(isOnchain, isLn, amount) => {
const errors = {} const errors = {}
if (!isLn && amount <= 0) { if (!isLn && amount <= 0) {
@ -251,8 +245,7 @@ payFormSelectors.payFormIsValid = createSelector(
payInputIsValid: isEmpty(errors.payInput), payInputIsValid: isEmpty(errors.payInput),
isValid: isEmpty(errors) isValid: isEmpty(errors)
} }
} })
)
export { payFormSelectors } export { payFormSelectors }

14
app/reducers/payment.js

@ -63,7 +63,7 @@ export function hideSuccessScreen() {
} }
// Send IPC event for payments // Send IPC event for payments
export const fetchPayments = () => (dispatch) => { export const fetchPayments = () => dispatch => {
dispatch(getPayments()) dispatch(getPayments())
ipcRenderer.send('lnd', { msg: 'payments' }) ipcRenderer.send('lnd', { msg: 'payments' })
} }
@ -73,7 +73,7 @@ export const receivePayments = (event, { payments }) => dispatch => dispatch({ t
// Receive IPC event for successful payment // Receive IPC event for successful payment
// TODO: Add payment to state, not a total re-fetch // TODO: Add payment to state, not a total re-fetch
export const paymentSuccessful = () => (dispatch) => { export const paymentSuccessful = () => dispatch => {
// Dispatch successful payment to stop loading screen // Dispatch successful payment to stop loading screen
dispatch(paymentSuccessfull()) dispatch(paymentSuccessfull())
@ -90,12 +90,12 @@ export const paymentSuccessful = () => (dispatch) => {
dispatch(fetchBalance()) dispatch(fetchBalance())
} }
export const paymentFailed = (event, { error }) => (dispatch) => { export const paymentFailed = (event, { error }) => dispatch => {
dispatch({ type: PAYMENT_FAILED }) dispatch({ type: PAYMENT_FAILED })
dispatch(setError(error)) dispatch(setError(error))
} }
export const payInvoice = paymentRequest => (dispatch) => { export const payInvoice = paymentRequest => dispatch => {
dispatch(sendPayment()) dispatch(sendPayment())
ipcRenderer.send('lnd', { msg: 'sendPayment', data: { paymentRequest } }) ipcRenderer.send('lnd', { msg: 'sendPayment', data: { paymentRequest } })
@ -112,7 +112,6 @@ export const payInvoice = paymentRequest => (dispatch) => {
// }, 10000) // }, 10000)
} }
// ------------------------------------ // ------------------------------------
// Action Handlers // Action Handlers
// ------------------------------------ // ------------------------------------
@ -132,10 +131,7 @@ const ACTION_HANDLERS = {
const paymentSelectors = {} const paymentSelectors = {}
const modalPaymentSelector = state => state.payment.payment const modalPaymentSelector = state => state.payment.payment
paymentSelectors.paymentModalOpen = createSelector( paymentSelectors.paymentModalOpen = createSelector(modalPaymentSelector, payment => !!payment)
modalPaymentSelector,
payment => (!!payment)
)
export { paymentSelectors } export { paymentSelectors }

41
app/reducers/peers.js

@ -70,7 +70,7 @@ export function updateSearchQuery(searchQuery) {
} }
// Send IPC event for peers // Send IPC event for peers
export const fetchPeers = () => async (dispatch) => { export const fetchPeers = () => async dispatch => {
dispatch(getPeers()) dispatch(getPeers())
ipcRenderer.send('lnd', { msg: 'peers' }) ipcRenderer.send('lnd', { msg: 'peers' })
} }
@ -79,7 +79,7 @@ export const fetchPeers = () => async (dispatch) => {
export const receivePeers = (event, { peers }) => dispatch => dispatch({ type: RECEIVE_PEERS, peers }) export const receivePeers = (event, { peers }) => dispatch => dispatch({ type: RECEIVE_PEERS, peers })
// Send IPC event for connecting to a peer // Send IPC event for connecting to a peer
export const connectRequest = ({ pubkey, host }) => (dispatch) => { export const connectRequest = ({ pubkey, host }) => dispatch => {
dispatch(connectPeer()) dispatch(connectPeer())
ipcRenderer.send('lnd', { msg: 'connectPeer', data: { pubkey, host } }) ipcRenderer.send('lnd', { msg: 'connectPeer', data: { pubkey, host } })
} }
@ -88,13 +88,13 @@ export const connectRequest = ({ pubkey, host }) => (dispatch) => {
export const connectSuccess = (event, peer) => dispatch => dispatch({ type: CONNECT_SUCCESS, peer }) export const connectSuccess = (event, peer) => dispatch => dispatch({ type: CONNECT_SUCCESS, peer })
// Send IPC receive for unsuccessfully connecting to a peer // Send IPC receive for unsuccessfully connecting to a peer
export const connectFailure = (event, { error }) => (dispatch) => { export const connectFailure = (event, { error }) => dispatch => {
dispatch({ type: CONNECT_FAILURE }) dispatch({ type: CONNECT_FAILURE })
dispatch(setError(error)) dispatch(setError(error))
} }
// Send IPC send for disconnecting from a peer // Send IPC send for disconnecting from a peer
export const disconnectRequest = ({ pubkey }) => (dispatch) => { export const disconnectRequest = ({ pubkey }) => dispatch => {
dispatch(disconnectPeer()) dispatch(disconnectPeer())
ipcRenderer.send('lnd', { msg: 'disconnectPeer', data: { pubkey } }) ipcRenderer.send('lnd', { msg: 'disconnectPeer', data: { pubkey } })
} }
@ -107,19 +107,21 @@ export const disconnectSuccess = (event, { pubkey }) => dispatch => dispatch({ t
// ------------------------------------ // ------------------------------------
const ACTION_HANDLERS = { const ACTION_HANDLERS = {
[DISCONNECT_PEER]: state => ({ ...state, disconnecting: true }), [DISCONNECT_PEER]: state => ({ ...state, disconnecting: true }),
[DISCONNECT_SUCCESS]: (state, { pubkey }) => ( [DISCONNECT_SUCCESS]: (state, { pubkey }) => ({
{ ...state,
...state, disconnecting: false, peer: null, peers: state.peers.filter(peer => peer.pub_key !== pubkey) disconnecting: false,
} peer: null,
), peers: state.peers.filter(peer => peer.pub_key !== pubkey)
}),
[DISCONNECT_FAILURE]: state => ({ ...state, disconnecting: false }), [DISCONNECT_FAILURE]: state => ({ ...state, disconnecting: false }),
[CONNECT_PEER]: state => ({ ...state, connecting: true }), [CONNECT_PEER]: state => ({ ...state, connecting: true }),
[CONNECT_SUCCESS]: (state, { peer }) => ( [CONNECT_SUCCESS]: (state, { peer }) => ({
{ ...state,
...state, connecting: false, peerForm: { pubkey: '', host: '', isOpen: false }, peers: [...state.peers, peer] connecting: false,
} peerForm: { pubkey: '', host: '', isOpen: false },
), peers: [...state.peers, peer]
}),
[CONNECT_FAILURE]: state => ({ ...state, connecting: false }), [CONNECT_FAILURE]: state => ({ ...state, connecting: false }),
[SET_PEER_FORM]: (state, { form }) => ({ ...state, peerForm: Object.assign({}, state.peerForm, form) }), [SET_PEER_FORM]: (state, { form }) => ({ ...state, peerForm: Object.assign({}, state.peerForm, form) }),
@ -137,15 +139,10 @@ const peerSelector = state => state.peers.peer
const peersSelector = state => state.peers.peers const peersSelector = state => state.peers.peers
const peersSearchQuerySelector = state => state.peers.searchQuery const peersSearchQuerySelector = state => state.peers.searchQuery
peersSelectors.peerModalOpen = createSelector( peersSelectors.peerModalOpen = createSelector(peerSelector, peer => !!peer)
peerSelector,
peer => (!!peer)
)
peersSelectors.filteredPeers = createSelector( peersSelectors.filteredPeers = createSelector(peersSelector, peersSearchQuerySelector, (peers, query) =>
peersSelector, peers.filter(peer => peer.pub_key.includes(query) || peer.address.includes(query))
peersSearchQuerySelector,
(peers, query) => peers.filter(peer => peer.pub_key.includes(query) || peer.address.includes(query))
) )
export { peersSelectors } export { peersSelectors }

6
app/reducers/requestform.js

@ -57,7 +57,7 @@ const ACTION_HANDLERS = {
[SET_REQUEST_MEMO]: (state, { memo }) => ({ ...state, memo }), [SET_REQUEST_MEMO]: (state, { memo }) => ({ ...state, memo }),
[SET_REQUEST_CURRENCY_FILTERS]: (state, { showCurrencyFilters }) => ({ ...state, showCurrencyFilters }), [SET_REQUEST_CURRENCY_FILTERS]: (state, { showCurrencyFilters }) => ({ ...state, showCurrencyFilters }),
[RESET_FORM]: () => (initialState) [RESET_FORM]: () => initialState
} }
const requestFormSelectors = {} const requestFormSelectors = {}
@ -70,7 +70,9 @@ requestFormSelectors.usdAmount = createSelector(
tickerSelectors.currentTicker, tickerSelectors.currentTicker,
(amount, currency, ticker) => { (amount, currency, ticker) => {
if (!ticker || !ticker.price_usd) { return false } if (!ticker || !ticker.price_usd) {
return false
}
return btc.convert(currency, 'usd', amount, ticker.price_usd) return btc.convert(currency, 'usd', amount, ticker.price_usd)
} }

42
app/reducers/ticker.js

@ -1,5 +1,7 @@
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import { requestTickers } from '../api' import { requestTickers } from '../api'
import { infoSelectors } from './info'
// ------------------------------------ // ------------------------------------
// Constants // Constants
// ------------------------------------ // ------------------------------------
@ -45,7 +47,7 @@ export function recieveTickers({ btcTicker, ltcTicker }) {
} }
} }
export const fetchTicker = () => async (dispatch) => { export const fetchTicker = () => async dispatch => {
dispatch(getTickers()) dispatch(getTickers())
const tickers = await requestTickers(['bitcoin', 'litecoin']) const tickers = await requestTickers(['bitcoin', 'litecoin'])
dispatch(recieveTickers(tickers)) dispatch(recieveTickers(tickers))
@ -54,12 +56,11 @@ export const fetchTicker = () => async (dispatch) => {
} }
// Receive IPC event for receiveCryptocurrency // Receive IPC event for receiveCryptocurrency
export const receiveCryptocurrency = (event, currency) => (dispatch) => { export const receiveCryptocurrency = (event, currency) => dispatch => {
dispatch({ type: SET_CURRENCY, currency: cryptoTickers[currency] }) dispatch({ type: SET_CURRENCY, currency: cryptoTickers[currency] })
dispatch({ type: SET_CRYPTO, crypto: cryptoTickers[currency] }) dispatch({ type: SET_CRYPTO, crypto: cryptoTickers[currency] })
} }
// ------------------------------------ // ------------------------------------
// Action Handlers // Action Handlers
// ------------------------------------ // ------------------------------------
@ -67,11 +68,12 @@ const ACTION_HANDLERS = {
[SET_CURRENCY]: (state, { currency }) => ({ ...state, fromCurrency: state.currency, currency }), [SET_CURRENCY]: (state, { currency }) => ({ ...state, fromCurrency: state.currency, currency }),
[SET_CRYPTO]: (state, { crypto }) => ({ ...state, crypto }), [SET_CRYPTO]: (state, { crypto }) => ({ ...state, crypto }),
[GET_TICKERS]: state => ({ ...state, tickerLoading: true }), [GET_TICKERS]: state => ({ ...state, tickerLoading: true }),
[RECIEVE_TICKERS]: (state, { btcTicker, ltcTicker }) => ( [RECIEVE_TICKERS]: (state, { btcTicker, ltcTicker }) => ({
{ ...state,
...state, tickerLoading: false, btcTicker, ltcTicker tickerLoading: false,
} btcTicker,
) ltcTicker
})
} }
// Selectors // Selectors
@ -89,21 +91,21 @@ tickerSelectors.currentTicker = createSelector(
(crypto, btcTicker, ltcTicker) => (crypto === 'btc' ? btcTicker : ltcTicker) (crypto, btcTicker, ltcTicker) => (crypto === 'btc' ? btcTicker : ltcTicker)
) )
tickerSelectors.currentCurrencyFilters = createSelector( tickerSelectors.currentCurrencyFilters = createSelector(currencySelector, currencyFiltersSelector, (currency, filters) =>
currencySelector, filters.filter(f => f.key !== currency)
currencyFiltersSelector,
(currency, filters) => filters.filter(f => f.key !== currency)
) )
tickerSelectors.currencyName = createSelector( tickerSelectors.currencyName = createSelector(currencySelector, infoSelectors.networkSelector, (currency, network) => {
currencySelector, let unit = currency
(currency) => { if (currency === 'btc') {
if (currency === 'btc') { return 'BTC' } unit = 'BTC'
if (currency === 'sats') { return 'satoshis' }
return currency
} }
) if (currency === 'sats') {
unit = 'satoshis'
}
return `${network.unitPrefix}${unit}`
})
export { tickerSelectors } export { tickerSelectors }

27
app/reducers/transaction.js

@ -52,7 +52,7 @@ export function hideSuccessTransactionScreen() {
} }
// Send IPC event for payments // Send IPC event for payments
export const fetchTransactions = () => (dispatch) => { export const fetchTransactions = () => dispatch => {
dispatch(getTransactions()) dispatch(getTransactions())
ipcRenderer.send('lnd', { msg: 'transactions' }) ipcRenderer.send('lnd', { msg: 'transactions' })
} }
@ -60,9 +60,7 @@ export const fetchTransactions = () => (dispatch) => {
// Receive IPC event for payments // Receive IPC event for payments
export const receiveTransactions = (event, { transactions }) => dispatch => dispatch({ type: RECEIVE_TRANSACTIONS, transactions }) export const receiveTransactions = (event, { transactions }) => dispatch => dispatch({ type: RECEIVE_TRANSACTIONS, transactions })
export const sendCoins = ({ export const sendCoins = ({ value, addr, currency }) => dispatch => {
value, addr, currency
}) => (dispatch) => {
// backend needs amount in satoshis no matter what currency we are using // backend needs amount in satoshis no matter what currency we are using
const amount = btc.convert(currency, 'sats', value) const amount = btc.convert(currency, 'sats', value)
@ -78,7 +76,7 @@ export const sendCoins = ({
// Receive IPC event for successful payment // Receive IPC event for successful payment
// TODO: Add payment to state, not a total re-fetch // TODO: Add payment to state, not a total re-fetch
export const transactionSuccessful = (event, { txid }) => (dispatch) => { export const transactionSuccessful = (event, { txid }) => dispatch => {
// Get the new list of transactions (TODO dont do an entire new fetch) // Get the new list of transactions (TODO dont do an entire new fetch)
dispatch(fetchTransactions()) dispatch(fetchTransactions())
// Show successful payment state // Show successful payment state
@ -93,13 +91,13 @@ export const transactionSuccessful = (event, { txid }) => (dispatch) => {
dispatch(resetPayForm()) dispatch(resetPayForm())
} }
export const transactionError = (event, { error }) => (dispatch) => { export const transactionError = (event, { error }) => dispatch => {
dispatch({ type: TRANSACTION_FAILED }) dispatch({ type: TRANSACTION_FAILED })
dispatch(setError(error)) dispatch(setError(error))
} }
// Listener for when a new transaction is pushed from the subscriber // Listener for when a new transaction is pushed from the subscriber
export const newTransaction = (event, { transaction }) => (dispatch) => { export const newTransaction = (event, { transaction }) => dispatch => {
// Fetch new balance // Fetch new balance
dispatch(fetchBalance()) dispatch(fetchBalance())
@ -107,7 +105,10 @@ export const newTransaction = (event, { transaction }) => (dispatch) => {
// HTML 5 desktop notification for the new transaction // HTML 5 desktop notification for the new transaction
const notifTitle = transaction.amount > 0 ? 'On-chain Transaction Received!' : 'On-chain Transaction Sent!' const notifTitle = transaction.amount > 0 ? 'On-chain Transaction Received!' : 'On-chain Transaction Sent!'
const notifBody = transaction.amount > 0 ? 'Lucky you, you just received a new on-chain transaction. I\'m jealous.' : 'Hate to see \'em go but love to watch \'em leave. Your on-chain transaction successfully sent.' // eslint-disable-line max-len const notifBody =
transaction.amount > 0
? "Lucky you, you just received a new on-chain transaction. I'm jealous."
: "Hate to see 'em go but love to watch 'em leave. Your on-chain transaction successfully sent." // eslint-disable-line max-len
showNotification(notifTitle, notifBody) showNotification(notifTitle, notifBody)
@ -115,7 +116,6 @@ export const newTransaction = (event, { transaction }) => (dispatch) => {
dispatch(newAddress('p2pkh')) dispatch(newAddress('p2pkh'))
} }
// ------------------------------------ // ------------------------------------
// Action Handlers // Action Handlers
// ------------------------------------ // ------------------------------------
@ -125,14 +125,15 @@ const ACTION_HANDLERS = {
[RECEIVE_TRANSACTIONS]: (state, { transactions }) => ({ ...state, transactionLoading: false, transactions }), [RECEIVE_TRANSACTIONS]: (state, { transactions }) => ({ ...state, transactionLoading: false, transactions }),
[TRANSACTION_SUCCESSFULL]: state => ({ ...state, sendingTransaction: false }), [TRANSACTION_SUCCESSFULL]: state => ({ ...state, sendingTransaction: false }),
[TRANSACTION_FAILED]: state => ({ ...state, sendingTransaction: false }), [TRANSACTION_FAILED]: state => ({ ...state, sendingTransaction: false }),
[ADD_TRANSACTION]: (state, { transaction }) => ( [ADD_TRANSACTION]: (state, { transaction }) => {
// add the transaction only if we are not already aware of it // add the transaction only if we are not already aware of it
state.transactions.find(tx => (tx.tx_hash === transaction.tx_hash)) ? state : { return state.transactions.find(tx => tx.tx_hash === transaction.tx_hash)
? state
: {
...state, ...state,
transactions: [transaction, ...state.transactions] transactions: [transaction, ...state.transactions]
} }
), },
[SHOW_SUCCESS_TRANSACTION_SCREEN]: (state, { txid }) => ({ ...state, successTransactionScreen: { show: true, txid } }), [SHOW_SUCCESS_TRANSACTION_SCREEN]: (state, { txid }) => ({ ...state, successTransactionScreen: { show: true, txid } }),
[HIDE_SUCCESS_TRANSACTION_SCREEN]: state => ({ ...state, successTransactionScreen: { show: false, txid: '' } }) [HIDE_SUCCESS_TRANSACTION_SCREEN]: state => ({ ...state, successTransactionScreen: { show: false, txid: '' } })
} }

2
app/routes.js

@ -7,7 +7,7 @@ import Activity from './routes/activity'
const routes = () => ( const routes = () => (
<App> <App>
<Switch> <Switch>
<Route path='/' component={Activity} /> <Route path="/" component={Activity} />
</Switch> </Switch>
</App> </App>
) )

70
app/routes/activity/components/Activity.js

@ -19,9 +19,7 @@ class Activity extends Component {
} }
componentWillMount() { componentWillMount() {
const { const { fetchPayments, fetchInvoices, fetchTransactions, fetchBalance } = this.props
fetchPayments, fetchInvoices, fetchTransactions, fetchBalance
} = this.props
fetchBalance() fetchBalance()
fetchPayments() fetchPayments()
@ -30,13 +28,7 @@ class Activity extends Component {
} }
renderActivity(activity) { renderActivity(activity) {
const { const { ticker, currentTicker, showActivityModal, network, currencyName } = this.props
ticker,
currentTicker,
showActivityModal,
network,
currencyName
} = this.props
if (Object.prototype.hasOwnProperty.call(activity, 'block_hash')) { if (Object.prototype.hasOwnProperty.call(activity, 'block_hash')) {
// activity is an on-chain tx // activity is an on-chain tx
@ -52,13 +44,7 @@ class Activity extends Component {
} else if (Object.prototype.hasOwnProperty.call(activity, 'payment_request')) { } else if (Object.prototype.hasOwnProperty.call(activity, 'payment_request')) {
// activity is an LN invoice // activity is an LN invoice
return ( return (
<Invoice <Invoice invoice={activity} ticker={ticker} currentTicker={currentTicker} showActivityModal={showActivityModal} currencyName={currencyName} />
invoice={activity}
ticker={ticker}
currentTicker={currentTicker}
showActivityModal={showActivityModal}
currencyName={currencyName}
/>
) )
} }
// activity is an LN payment // activity is an LN payment
@ -77,13 +63,7 @@ class Activity extends Component {
render() { render() {
const { const {
balance, balance,
activity: { activity: { filters, filter, filterPulldown, searchActive, searchText },
filters,
filter,
filterPulldown,
searchActive,
searchText
},
changeFilter, changeFilter,
currentActivity, currentActivity,
@ -93,62 +73,56 @@ class Activity extends Component {
walletProps walletProps
} = this.props } = this.props
if (!balance.channelBalance || !balance.walletBalance) { return <LoadingBolt /> } if (!balance.channelBalance || !balance.walletBalance) {
return <LoadingBolt />
}
return ( return (
<div> <div>
<Wallet {...walletProps} /> <Wallet {...walletProps} />
<div className={styles.activities}> <div className={styles.activities}>
{ {searchActive ? (
searchActive ?
<header className={`${styles.header} ${styles.search}`}> <header className={`${styles.header} ${styles.search}`}>
<section> <section>
<input <input placeholder="Search" value={searchText} onChange={event => updateSearchText(event.target.value)} />
placeholder='Search'
value={searchText}
onChange={event => updateSearchText(event.target.value)}
/>
</section> </section>
<section onClick={() => { updateSearchActive(false); updateSearchText('') }}> <section
onClick={() => {
updateSearchActive(false)
updateSearchText('')
}}
>
<span className={styles.xIcon}> <span className={styles.xIcon}>
<Isvg src={xIcon} /> <Isvg src={xIcon} />
</span> </span>
</section> </section>
</header> </header>
: ) : (
<header className={styles.header}> <header className={styles.header}>
<section> <section>
<ul className={styles.filters}> <ul className={styles.filters}>
{ {filters.map(f => (
filters.map(f => (
<li key={f.key} className={f.key === filter.key && styles.activeFilter} onClick={() => changeFilter(f)}> <li key={f.key} className={f.key === filter.key && styles.activeFilter} onClick={() => changeFilter(f)}>
<span>{f.name}</span> <span>{f.name}</span>
<div className={f.key === filter.key && styles.activeBorder} /> <div className={f.key === filter.key && styles.activeBorder} />
</li> </li>
)) ))}
}
</ul> </ul>
</section> </section>
<section onClick={() => updateSearchActive(true)}> <section onClick={() => updateSearchActive(true)}>
<Isvg src={searchIcon} /> <Isvg src={searchIcon} />
</section> </section>
</header> </header>
} )}
<ul className={`${styles.activityContainer} ${filterPulldown && styles.pulldown}`}> <ul className={`${styles.activityContainer} ${filterPulldown && styles.pulldown}`}>
{ {currentActivity.map((activityBlock, index) => (
currentActivity.map((activityBlock, index) => (
<li className={styles.activity} key={index}> <li className={styles.activity} key={index}>
<h2>{activityBlock.title}</h2> <h2>{activityBlock.title}</h2>
<ul> <ul>{activityBlock.activity.map((activity, i) => <li key={i}>{this.renderActivity(activity.el)}</li>)}</ul>
{
activityBlock.activity.map((activity, i) => <li key={i}>{this.renderActivity(activity.el)}</li>)
}
</ul>
</li> </li>
)) ))}
}
</ul> </ul>
</div> </div>
</div> </div>

28
app/routes/activity/components/components/Invoice/Invoice.js

@ -9,42 +9,30 @@ import Value from 'components/Value'
import checkmarkIcon from 'icons/check_circle.svg' import checkmarkIcon from 'icons/check_circle.svg'
import styles from '../Activity.scss' import styles from '../Activity.scss'
const Invoice = ({ const Invoice = ({ invoice, ticker, currentTicker, showActivityModal, currencyName }) => (
invoice, ticker, currentTicker, showActivityModal, currencyName
}) => (
<div className={`${styles.container} ${!invoice.settled && styles.unpaid}`} onClick={() => showActivityModal('INVOICE', { invoice })}> <div className={`${styles.container} ${!invoice.settled && styles.unpaid}`} onClick={() => showActivityModal('INVOICE', { invoice })}>
{ {!invoice.settled && (
!invoice.settled && (
<div className={styles.pendingIcon}> <div className={styles.pendingIcon}>
<Isvg src={checkmarkIcon} /> <Isvg src={checkmarkIcon} />
</div> </div>
) )}
}
<div className={styles.data}> <div className={styles.data}>
<div className={styles.title}> <div className={styles.title}>
<h3> <h3>{invoice.settled ? 'Received payment' : 'Requested payment'}</h3>
{ invoice.settled ? 'Received payment' : 'Requested payment' }
</h3>
</div> </div>
<div className={styles.subtitle}> <div className={styles.subtitle}>
<Moment format='h:mm a'>{invoice.settled ? invoice.settled_date * 1000 : invoice.creation_date * 1000}</Moment> <Moment format="h:mm a">{invoice.settled ? invoice.settled_date * 1000 : invoice.creation_date * 1000}</Moment>
</div> </div>
</div> </div>
<div className={`${styles.amount} ${invoice.settled ? styles.positive : styles.negative}`}> <div className={`${styles.amount} ${invoice.settled ? styles.positive : styles.negative}`}>
<span className='hint--top' data-hint='Invoice amount'> <span className="hint--top" data-hint="Invoice amount">
<i className={styles.plus}>+</i> <i className={styles.plus}>+</i>
<Value <Value value={invoice.value} currency={ticker.currency} currentTicker={currentTicker} />
value={invoice.value}
currency={ticker.currency}
currentTicker={currentTicker}
/>
<i> {currencyName}</i> <i> {currencyName}</i>
</span> </span>
<span> <span>
<span> <span>${btc.convert('sats', 'usd', invoice.value, currentTicker.price_usd)}</span>
${btc.convert('sats', 'usd', invoice.value, currentTicker.price_usd)}
</span>
</span> </span>
</div> </div>
</div> </div>

26
app/routes/activity/components/components/Payment/Payment.js

@ -8,13 +8,13 @@ import { btc } from 'utils'
import Value from 'components/Value' import Value from 'components/Value'
import styles from '../Activity.scss' import styles from '../Activity.scss'
const Payment = ({ const Payment = ({ payment, ticker, currentTicker, showActivityModal, nodes, currencyName }) => {
payment, ticker, currentTicker, showActivityModal, nodes, currencyName const displayNodeName = pubkey => {
}) => {
const displayNodeName = (pubkey) => {
const node = find(nodes, n => pubkey === n.pub_key) const node = find(nodes, n => pubkey === n.pub_key)
if (node && node.alias.length) { return node.alias } if (node && node.alias.length) {
return node.alias
}
return pubkey.substring(0, 10) return pubkey.substring(0, 10)
} }
@ -23,25 +23,19 @@ const Payment = ({
<div className={styles.container} onClick={() => showActivityModal('PAYMENT', { payment })}> <div className={styles.container} onClick={() => showActivityModal('PAYMENT', { payment })}>
<div className={styles.data}> <div className={styles.data}>
<div className={styles.title}> <div className={styles.title}>
<h3> <h3>{displayNodeName(payment.path[payment.path.length - 1])}</h3>
{displayNodeName(payment.path[payment.path.length - 1])}
</h3>
</div> </div>
<div className={styles.subtitle}> <div className={styles.subtitle}>
<Moment format='h:mm a'>{payment.creation_date * 1000}</Moment> <Moment format="h:mm a">{payment.creation_date * 1000}</Moment>
</div> </div>
</div> </div>
<div className={styles.amount}> <div className={styles.amount}>
<span className='hint--top' data-hint='Payment amount'> <span className="hint--top" data-hint="Payment amount">
<i className={styles.minus}>-</i> <i className={styles.minus}>-</i>
<Value <Value value={payment.value} currency={ticker.currency} currentTicker={currentTicker} />
value={payment.value}
currency={ticker.currency}
currentTicker={currentTicker}
/>
<i> {currencyName}</i> <i> {currencyName}</i>
</span> </span>
<span className='hint--bottom' data-hint='Payment fee'> <span className="hint--bottom" data-hint="Payment fee">
${btc.convert('sats', 'usd', payment.value, currentTicker.price_usd)} ${btc.convert('sats', 'usd', payment.value, currentTicker.price_usd)}
</span> </span>
</div> </div>

20
app/routes/activity/components/components/Transaction/Transaction.js

@ -7,31 +7,23 @@ import { btc } from 'utils'
import Value from 'components/Value' import Value from 'components/Value'
import styles from '../Activity.scss' import styles from '../Activity.scss'
const Transaction = ({ const Transaction = ({ transaction, ticker, currentTicker, showActivityModal, currencyName }) => (
transaction, ticker, currentTicker, showActivityModal, currencyName
}) => (
<div className={styles.container} onClick={() => showActivityModal('TRANSACTION', { transaction })}> <div className={styles.container} onClick={() => showActivityModal('TRANSACTION', { transaction })}>
<div className={styles.data}> <div className={styles.data}>
<div className={styles.title}> <div className={styles.title}>
<h3> <h3>{transaction.amount > 0 ? 'Received' : 'Sent'}</h3>
{ transaction.amount > 0 ? 'Received' : 'Sent' }
</h3>
</div> </div>
<div className={styles.subtitle}> <div className={styles.subtitle}>
<Moment format='h:mm a'>{transaction.time_stamp * 1000}</Moment> <Moment format="h:mm a">{transaction.time_stamp * 1000}</Moment>
</div> </div>
</div> </div>
<div className={`${styles.amount} ${transaction.amount > 0 ? styles.positive : styles.negative}`}> <div className={`${styles.amount} ${transaction.amount > 0 ? styles.positive : styles.negative}`}>
<span className='hint--top' data-hint='Transaction amount'> <span className="hint--top" data-hint="Transaction amount">
<i className={transaction.amount > 0 ? styles.plus : styles.minus}>{transaction.amount > 0 ? '+' : '-'}</i> <i className={transaction.amount > 0 ? styles.plus : styles.minus}>{transaction.amount > 0 ? '+' : '-'}</i>
<Value <Value value={transaction.amount} currency={ticker.currency} currentTicker={currentTicker} />
value={transaction.amount}
currency={ticker.currency}
currentTicker={currentTicker}
/>
<i> {currencyName}</i> <i> {currencyName}</i>
</span> </span>
<span className='hint--bottom' data-hint='Transaction fee'> <span className="hint--bottom" data-hint="Transaction fee">
${btc.convert('sats', 'usd', transaction.amount, currentTicker.price_usd)} ${btc.convert('sats', 'usd', transaction.amount, currentTicker.price_usd)}
</span> </span>
</div> </div>

38
app/routes/activity/containers/ActivityContainer.js

@ -1,16 +1,8 @@
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { setCurrency, tickerSelectors } from 'reducers/ticker' import { setCurrency, tickerSelectors } from 'reducers/ticker'
import { fetchBalance } from 'reducers/balance' import { fetchBalance } from 'reducers/balance'
import { import { fetchInvoices, setInvoice, invoiceSelectors } from 'reducers/invoice'
fetchInvoices, import { setPayment, fetchPayments, paymentSelectors } from 'reducers/payment'
setInvoice,
invoiceSelectors
} from 'reducers/invoice'
import {
setPayment,
fetchPayments,
paymentSelectors
} from 'reducers/payment'
import { fetchTransactions } from 'reducers/transaction' import { fetchTransactions } from 'reducers/transaction'
import { import {
showActivityModal, showActivityModal,
@ -81,8 +73,12 @@ const mapStateToProps = state => ({
showPayLoadingScreen: payFormSelectors.showPayLoadingScreen(state) showPayLoadingScreen: payFormSelectors.showPayLoadingScreen(state)
}) })
const mergeProps = (stateProps, dispatchProps, ownProps) => { const mergeProps = (stateProps, dispatchProps, ownProps) => ({
const walletProps = { ...stateProps,
...dispatchProps,
...ownProps,
walletProps: {
balance: stateProps.balance, balance: stateProps.balance,
address: stateProps.address.address, address: stateProps.address.address,
info: stateProps.info, info: stateProps.info,
@ -93,7 +89,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
successTransactionScreen: stateProps.transaction.successTransactionScreen, successTransactionScreen: stateProps.transaction.successTransactionScreen,
currentCurrencyFilters: stateProps.currentCurrencyFilters, currentCurrencyFilters: stateProps.currentCurrencyFilters,
currencyName: stateProps.currencyName, currencyName: stateProps.currencyName,
isTestnet: stateProps.info.data.testnet, network: stateProps.info.network,
setCurrency: dispatchProps.setCurrency, setCurrency: dispatchProps.setCurrency,
setWalletCurrencyFilters: dispatchProps.setWalletCurrencyFilters, setWalletCurrencyFilters: dispatchProps.setWalletCurrencyFilters,
@ -102,14 +98,10 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
openPayForm: () => dispatchProps.setFormType('PAY_FORM'), openPayForm: () => dispatchProps.setFormType('PAY_FORM'),
openRequestForm: () => dispatchProps.setFormType('REQUEST_FORM') openRequestForm: () => dispatchProps.setFormType('REQUEST_FORM')
} }
})
return { export default connect(
...stateProps, mapStateToProps,
...dispatchProps, mapDispatchToProps,
...ownProps, mergeProps
)(Activity)
walletProps
}
}
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Activity)

25
app/routes/app/components/App.js

@ -18,15 +18,7 @@ import styles from './App.scss'
class App extends Component { class App extends Component {
componentWillMount() { componentWillMount() {
const { const { fetchTicker, fetchInfo, newAddress, fetchChannels, fetchSuggestedNodes, fetchBalance, fetchDescribeNetwork } = this.props
fetchTicker,
fetchInfo,
newAddress,
fetchChannels,
fetchSuggestedNodes,
fetchBalance,
fetchDescribeNetwork
} = this.props
// fetch price ticker // fetch price ticker
fetchTicker() fetchTicker()
@ -65,7 +57,9 @@ class App extends Component {
children children
} = this.props } = this.props
if (!currentTicker) { return <LoadingBolt /> } if (!currentTicker) {
return <LoadingBolt />
}
return ( return (
<div> <div>
@ -79,16 +73,9 @@ class App extends Component {
<ReceiveModal {...receiveModalProps} /> <ReceiveModal {...receiveModalProps} />
<ActivityModal {...activityModalProps} /> <ActivityModal {...activityModalProps} />
<div className={styles.content}> <div className={styles.content}>{children}</div>
{children}
</div>
{ {contactsFormProps.contactsform.isOpen ? <AddChannel {...contactsFormProps} /> : <Network {...networkTabProps} />}
contactsFormProps.contactsform.isOpen ?
<AddChannel {...contactsFormProps} />
:
<Network {...networkTabProps} />
}
<Form formType={form.formType} formProps={formProps} closeForm={closeForm} /> <Form formType={form.formType} formProps={formProps} closeForm={closeForm} />
</div> </div>

53
app/routes/app/containers/AppContainer.js

@ -40,23 +40,17 @@ import {
import { import {
openContactsForm, openContactsForm,
closeContactsForm, closeContactsForm,
setChannelFormType, setChannelFormType,
openManualForm, openManualForm,
closeManualForm, closeManualForm,
openSubmitChannelForm, openSubmitChannelForm,
closeSubmitChannelForm, closeSubmitChannelForm,
updateContactFormSearchQuery, updateContactFormSearchQuery,
updateManualFormSearchQuery, updateManualFormSearchQuery,
updateContactCapacity, updateContactCapacity,
setNode, setNode,
contactFormSelectors, contactFormSelectors,
updateManualFormErrors, updateManualFormErrors,
setContactsCurrencyFilters setContactsCurrencyFilters
} from 'reducers/contactsform' } from 'reducers/contactsform'
@ -271,21 +265,26 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
setCurrency: dispatchProps.setCurrency, setCurrency: dispatchProps.setCurrency,
setRequestCurrencyFilters: dispatchProps.setRequestCurrencyFilters, setRequestCurrencyFilters: dispatchProps.setRequestCurrencyFilters,
onRequestSubmit: () => ( onRequestSubmit: () =>
dispatchProps.createInvoice( dispatchProps.createInvoice(
stateProps.requestform.amount, stateProps.requestform.amount,
stateProps.requestform.memo, stateProps.requestform.memo,
stateProps.ticker.currency, stateProps.ticker.currency,
stateProps.currentTicker.price_usd stateProps.currentTicker.price_usd
) )
)
} }
const formProps = (formType) => { const formProps = formType => {
if (!formType) { return {} } if (!formType) {
return {}
}
if (formType === 'PAY_FORM') { return payFormProps } if (formType === 'PAY_FORM') {
if (formType === 'REQUEST_FORM') { return requestFormProps } return payFormProps
}
if (formType === 'REQUEST_FORM') {
return requestFormProps
}
return {} return {}
} }
@ -299,7 +298,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
nodes: stateProps.network.nodes, nodes: stateProps.network.nodes,
nonActiveFilters: stateProps.nonActiveFilters, nonActiveFilters: stateProps.nonActiveFilters,
ticker: stateProps.ticker, ticker: stateProps.ticker,
isTestnet: stateProps.info.data.testnet, network: stateProps.info.network,
fetchChannels: dispatchProps.fetchChannels, fetchChannels: dispatchProps.fetchChannels,
openContactsForm: dispatchProps.openContactsForm, openContactsForm: dispatchProps.openContactsForm,
@ -356,7 +355,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
modalProps: stateProps.activity.modal.modalProps, modalProps: stateProps.activity.modal.modalProps,
ticker: stateProps.ticker, ticker: stateProps.ticker,
currentTicker: stateProps.currentTicker, currentTicker: stateProps.currentTicker,
isTestnet: stateProps.info.data.testnet, network: stateProps.info.network,
hideActivityModal: dispatchProps.hideActivityModal, hideActivityModal: dispatchProps.hideActivityModal,
@ -367,7 +366,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
setActivityModalCurrencyFilters: dispatchProps.setActivityModalCurrencyFilters, setActivityModalCurrencyFilters: dispatchProps.setActivityModalCurrencyFilters,
setCurrencyFilters: dispatchProps.setCurrencyFilters, setCurrencyFilters: dispatchProps.setCurrencyFilters,
onCurrencyFilterClick: (currency) => { onCurrencyFilterClick: currency => {
dispatchProps.setCurrency(currency) dispatchProps.setCurrency(currency)
dispatchProps.setActivityModalCurrencyFilters(false) dispatchProps.setActivityModalCurrencyFilters(false)
} }
@ -376,6 +375,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
const receiveModalProps = { const receiveModalProps = {
isOpen: stateProps.address.walletModal, isOpen: stateProps.address.walletModal,
network: stateProps.info.network,
pubkey: stateProps.info.data.identity_pubkey, pubkey: stateProps.info.data.identity_pubkey,
address: stateProps.address.address, address: stateProps.address.address,
alias: stateProps.info.data.alias, alias: stateProps.info.data.alias,
@ -403,7 +403,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
setContactsCurrencyFilters: dispatchProps.setContactsCurrencyFilters, setContactsCurrencyFilters: dispatchProps.setContactsCurrencyFilters,
setCurrencyFilters: dispatchProps.setCurrencyFilters, setCurrencyFilters: dispatchProps.setCurrencyFilters,
onCurrencyFilterClick: (currency) => { onCurrencyFilterClick: currency => {
dispatchProps.updateContactCapacity(btc.convert(stateProps.ticker.currency, currency, stateProps.contactsform.contactCapacity)) dispatchProps.updateContactCapacity(btc.convert(stateProps.ticker.currency, currency, stateProps.contactsform.contactCapacity))
dispatchProps.setCurrency(currency) dispatchProps.setCurrency(currency)
dispatchProps.setContactsCurrencyFilters(false) dispatchProps.setContactsCurrencyFilters(false)
@ -424,9 +424,13 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
showErrors: stateProps.contactsform.showErrors showErrors: stateProps.contactsform.showErrors
} }
const calcChannelFormProps = (formType) => { const calcChannelFormProps = formType => {
if (formType === 'MANUAL_FORM') { return connectManuallyProps } if (formType === 'MANUAL_FORM') {
if (formType === 'SUBMIT_CHANNEL_FORM') { return submitChannelFormProps } return connectManuallyProps
}
if (formType === 'SUBMIT_CHANNEL_FORM') {
return submitChannelFormProps
}
return {} return {}
} }
@ -437,7 +441,6 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
closeForm: () => dispatchProps.setChannelFormType(null) closeForm: () => dispatchProps.setChannelFormType(null)
} }
return { return {
...stateProps, ...stateProps,
...dispatchProps, ...dispatchProps,
@ -463,9 +466,13 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
formProps: formProps(stateProps.form.formType), formProps: formProps(stateProps.form.formType),
// action to close form // action to close form
closeForm: () => dispatchProps.setFormType(null) closeForm: () => dispatchProps.setFormType(null)
} }
} }
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(App)) export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(App)
)

5
app/store/configureStore.dev.js

@ -8,7 +8,7 @@ import ipc from '../reducers/ipc'
const history = createHashHistory() const history = createHashHistory()
const configureStore = (initialState) => { const configureStore = initialState => {
// Redux Configuration // Redux Configuration
const middleware = [] const middleware = []
const enhancers = [] const enhancers = []
@ -49,8 +49,7 @@ const configureStore = (initialState) => {
const store = createStore(rootReducer, initialState, enhancer) const store = createStore(rootReducer, initialState, enhancer)
if (module.hot) { if (module.hot) {
module.hot.accept('../reducers', () => module.hot.accept('../reducers', () => store.replaceReducer(require('../reducers'))) // eslint-disable-line global-require
store.replaceReducer(require('../reducers'))) // eslint-disable-line global-require
} }
return store return store

17
app/utils/blockExplorer.js

@ -1,23 +1,14 @@
import { shell } from 'electron' import { shell } from 'electron'
const testnetUrl = 'https://testnet.smartbit.com.au' const showTransaction = (network, txid) => shell.openExternal(`${network.explorerUrl}/tx/${txid}`)
const mainnetUrl = 'https://smartbit.com.au'
const showTransaction = (isTestnet, txid) => const showBlock = (network, blockHash) => shell.openExternal(`${network.explorerUrl}/block/${blockHash}`)
(isTestnet ? shell.openExternal(`${testnetUrl}/tx/${txid}`) : shell.openExternal(`${mainnetUrl}/tx/${txid}`))
const showBlock = (isTestnet, blockHash) => const showChannelClosing = channel => showTransaction(channel.closing_txid)
(isTestnet ? shell.openExternal(`${testnetUrl}/block/${blockHash}`) : shell.openExternal(`${mainnetUrl}/block/${blockHash}`))
const showChannelClosing = channel => const showChannelPoint = channel => showTransaction(channel.channel.channel_point.split(':')[0])
showTransaction(channel.closing_txid)
const showChannelPoint = channel =>
showTransaction(channel.channel.channel_point.split(':')[0])
export default { export default {
testnetUrl,
mainnetUrl,
showTransaction, showTransaction,
showBlock, showBlock,
showChannelClosing, showChannelClosing,

80
app/utils/log.js

@ -0,0 +1,80 @@
import debugLogger from 'debug-logger'
// Enable colours for object inspection.
debugLogger.inspectOptions = {
colors: true
}
// Enable all zap logs if DEBUG has not been explicitly set.
if (!process.env.DEBUG) {
debugLogger.debug.enable('zap:*')
}
// Method to configure a logger instance with a specific namespace suffix.
const logConfig = name => ({
levels: {
trace: {
prefix: '[TRC] ',
namespaceSuffix: `:${name}`
},
debug: {
prefix: '[DBG] ',
namespaceSuffix: `:${name}`
},
log: {
prefix: '[LOG] ',
namespaceSuffix: `:${name}`
},
info: {
prefix: '[INF] ',
namespaceSuffix: `:${name}`
},
warn: {
prefix: '[WRN] ',
namespaceSuffix: `:${name}`
},
error: {
prefix: '[ERR] ',
namespaceSuffix: `:${name}`
},
critical: {
color: debugLogger.colors.magenta,
prefix: '[CRT] ',
namespaceSuffix: `:${name}`,
level: 6,
fd: 2
}
}
})
// Create 2 logs for use in the app.
export const mainLog = debugLogger.config(logConfig('main'))('zap')
export const lndLog = debugLogger.config(logConfig('lnd '))('zap')
let lndLogLevel = null // stored most recent log level for continuity
export const lndLogGetLevel = msg => {
// Define a mapping between log level prefixes and log level names.
const levelMap = {
TRC: 'trace',
DBG: 'debug',
INF: 'info',
WRN: 'warn',
ERR: 'error',
CRT: 'critical'
}
// Parse the log line to determine its level.
let level
Object.entries(levelMap).forEach(([key, value]) => {
if (msg.includes(`[${key}]`)) {
level = value
}
})
if (level) {
lndLogLevel = level
return level
}
// We set the default level to trace.
// The only log lines that don't include a level prefix are a part of trace entries
return lndLogLevel || 'trace'
}

4
app/utils/usd.js

@ -3,7 +3,9 @@ export function formatUsd(usd) {
} }
export function usdToBtc(usd, rate) { export function usdToBtc(usd, rate) {
if (usd === undefined || usd === null || usd === '') return null if (usd === undefined || usd === null || usd === '') {
return null
}
return (usd / rate).toFixed(8) return (usd / rate).toFixed(8)
} }

6
app/yarn.lock

@ -171,9 +171,9 @@ glob@^7.0.5:
once "^1.3.0" once "^1.3.0"
path-is-absolute "^1.0.0" path-is-absolute "^1.0.0"
grpc@1.12.1: grpc@^1.12.3:
version "1.12.1" version "1.12.3"
resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.12.1.tgz#bf2ef184695836582d7b0f04a0120032460fdac7" resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.12.3.tgz#b38bf05f26477d42f8285794c0b1f8b8c0b6dec3"
dependencies: dependencies:
lodash "^4.17.5" lodash "^4.17.5"
nan "^2.0.0" nan "^2.0.0"

4
appveyor.yml

@ -1,9 +1,10 @@
os: unstable os: unstable
environment: environment:
ELECTRON_DISABLE_SECURITY_WARNINGS: true
matrix: matrix:
- nodejs_version: 10
- nodejs_version: 8 - nodejs_version: 8
- nodejs_version: 7
cache: cache:
- "%LOCALAPPDATA%/Yarn" - "%LOCALAPPDATA%/Yarn"
@ -25,7 +26,6 @@ install:
- ps: Install-Product node $env:nodejs_version - ps: Install-Product node $env:nodejs_version
- set CI=true - set CI=true
- yarn - yarn
- cd app && yarn
test_script: test_script:
- node --version - node --version

3
internals/scripts/CheckNodeEnv.js

@ -7,7 +7,6 @@ export default function CheckNodeEnv(expectedEnv: string) {
} }
if (process.env.NODE_ENV !== expectedEnv) { if (process.env.NODE_ENV !== expectedEnv) {
console.log(chalk.whiteBright.bgRed.bold(`"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`)) throw new Error(chalk.whiteBright.bgRed.bold(`"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`))
process.exit(2)
} }
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save