Browse Source

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

renovate/lint-staged-8.x
Kristiyan Lukanov 6 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. 52
      .eslintrc
  4. 6
      .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. 89
      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. 56
      app/components/Contacts/AddChannel.js
  23. 5
      app/components/Contacts/ChannelForm.js
  24. 13
      app/components/Contacts/ConnectManually.js
  25. 48
      app/components/Contacts/ContactModal.js
  26. 95
      app/components/Contacts/ContactsForm.js
  27. 178
      app/components/Contacts/Network.js
  28. 46
      app/components/Contacts/SubmitChannelForm.js
  29. 20
      app/components/Contacts/SubmitChannelForm.scss
  30. 37
      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. 2
      app/components/Onboarding/FormContainer.js
  38. 9
      app/components/Onboarding/InitWallet.js
  39. 23
      app/components/Onboarding/Login.js
  40. 8
      app/components/Onboarding/NewAezeedPassword.js
  41. 8
      app/components/Onboarding/NewWalletPassword.js
  42. 22
      app/components/Onboarding/NewWalletSeed.js
  43. 54
      app/components/Onboarding/Onboarding.js
  44. 37
      app/components/Onboarding/ReEnterSeed.js
  45. 13
      app/components/Onboarding/RecoverForm.js
  46. 22
      app/components/Onboarding/Signup.js
  47. 6
      app/components/Onboarding/Syncing.js
  48. 13
      app/components/Value/Value.js
  49. 40
      app/components/Wallet/ReceiveModal.js
  50. 78
      app/components/Wallet/Wallet.js
  51. 6
      app/containers/Root.js
  52. 12
      app/lnd/index.js
  53. 9
      app/lnd/lib/lightning.js
  54. 9
      app/lnd/lib/walletUnlocker.js
  55. 16
      app/lnd/methods/channelController.js
  56. 91
      app/lnd/methods/index.js
  57. 18
      app/lnd/methods/invoicesController.js
  58. 24
      app/lnd/methods/networkController.js
  59. 27
      app/lnd/methods/paymentsController.js
  60. 14
      app/lnd/methods/peersController.js
  61. 60
      app/lnd/methods/walletController.js
  62. 6
      app/lnd/subscribe/index.js
  63. 8
      app/lnd/subscribe/invoices.js
  64. 12
      app/lnd/subscribe/transactions.js
  65. 15
      app/lnd/walletUnlockerMethods/index.js
  66. 103
      app/main.dev.js
  67. 228
      app/menu.js
  68. 5
      app/package.json
  69. 67
      app/reducers/activity.js
  70. 2
      app/reducers/address.js
  71. 15
      app/reducers/balance.js
  72. 151
      app/reducers/channels.js
  73. 74
      app/reducers/contactsform.js
  74. 2
      app/reducers/error.js
  75. 43
      app/reducers/info.js
  76. 43
      app/reducers/invoice.js
  77. 10
      app/reducers/ipc.js
  78. 25
      app/reducers/lnd.js
  79. 84
      app/reducers/network.js
  80. 18
      app/reducers/onboarding.js
  81. 115
      app/reducers/payform.js
  82. 14
      app/reducers/payment.js
  83. 41
      app/reducers/peers.js
  84. 6
      app/reducers/requestform.js
  85. 42
      app/reducers/ticker.js
  86. 33
      app/reducers/transaction.js
  87. 2
      app/routes.js
  88. 120
      app/routes/activity/components/Activity.js
  89. 34
      app/routes/activity/components/components/Invoice/Invoice.js
  90. 26
      app/routes/activity/components/components/Payment/Payment.js
  91. 22
      app/routes/activity/components/components/Transaction/Transaction.js
  92. 38
      app/routes/activity/containers/ActivityContainer.js
  93. 25
      app/routes/app/components/App.js
  94. 53
      app/routes/app/containers/AppContainer.js
  95. 11
      app/store/configureStore.dev.js
  96. 17
      app/utils/blockExplorer.js
  97. 80
      app/utils/log.js
  98. 4
      app/utils/usd.js
  99. 6
      app/yarn.lock
  100. 4
      appveyor.yml

2
.babelrc

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

3
.commitlintrc

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

52
.eslintrc

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

6
.gitignore

@ -52,4 +52,8 @@ main.js.map
npm-debug.log.*
# 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:
global:
- DEBUG=electron-builder
- ELECTRON_DISABLE_SECURITY_WARNINGS=true
matrix:
- TEST=lint-ci
- TEST=test-ci
@ -31,7 +32,6 @@ addons:
install:
- 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"
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
***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
@ -21,12 +27,11 @@ git clone https://github.com/LN-Zap/zap-desktop.git
### Installing Dependencies
Install all the dependencies with yarn + install grpc:
Install all the dependencies with yarn:
```bash
cd zap-desktop
yarn
npm run install-grpc
```
## 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
***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.
@ -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.
The `lnd` binary can be found at `$GOPATH/bin`.
### Full Bitcoin Node
Follow the instructions on the [lnd installation](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md) page.
### lnd Location
Zap expects `lnd` to be in one of these directories depending on your OS:
@ -81,6 +84,7 @@ chmod +x lnd
## Running Zap
### Testing
To test that everything has been installed correctly:
```bash
@ -96,8 +100,12 @@ npm run dev
```
### Linting
To check linting:
```bash
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>
(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?")
# Contributing
## Overview
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.
Thanks for being willing to contribute!
## Pull Requests
The `master` branch will be used for all pull requests for the time being. This may change as the repo and contributors grow.
## Table of Contents
### 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
```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
```bash
git commit -m "feature(list-onchain-txs): create hard code mock of onchain-txs list"
git branch feat/close-channel-ui
```
## eslint
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
### Pull Requests
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.
[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
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,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

89
README.md

@ -1,27 +1,46 @@
<h1 align='center'>
<img src='https://imgur.com/svn8Jrw.jpg' alt='screenshot' />
<br />
<center>
<a href='https://zap.jackmallers.com'>Zap</a>
</center>
</h1>
# Zap
<p align='center'>
<a href='https://zap.jackmallers.com'>
<img src='https://imgur.com/svn8Jrw.jpg' alt='screenshot' />
</a>
</p>
> Lightning wallet focused on user experience and ease of use ⚡️
[![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.
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).
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
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.
@ -72,22 +91,48 @@ Once you have the .AppImage file extracted, you can either **double click** the
```
## 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 help contribute to the project, please see the [contributing guide](https://github.com/LN-Zap/zap-desktop/blob/master/CONTRIBUTING.md).
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.
## 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.
### Example User Stories
`User wants to connect to a peer`
## Maintainers
- [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) {
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] })))
}
@ -26,7 +27,7 @@ export function requestBlockHeight() {
}
export function requestSuggestedNodes() {
const BASE_URL = 'http://zap.jackmallers.com/suggested-peers'
const BASE_URL = 'https://zap.jackmallers.com/suggested-peers'
return axios({
method: 'get',
url: BASE_URL

2
app/app.global.scss

@ -8,7 +8,7 @@
font-family: 'Roboto';
font-style: normal;
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 {

8
app/app.html

@ -2,9 +2,15 @@
<html>
<head>
<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>
<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=Noto+Sans:400,700|Roboto:300' rel='stylesheet' type='text/css'>
<script>

16
app/components/Activity/ActivityModal.js

@ -14,7 +14,7 @@ const ActivityModal = ({
modalProps,
ticker,
currentTicker,
isTestnet,
network,
hideActivityModal,
toggleCurrencyProps
@ -25,7 +25,9 @@ const ActivityModal = ({
INVOICE: InvoiceModal
}
if (!modalType) { return null }
if (!modalType) {
return null
}
const SpecificModal = MODAL_COMPONENTS[modalType]
return (
@ -35,13 +37,7 @@ const ActivityModal = ({
<Isvg src={x} />
</span>
</div>
<SpecificModal
{...modalProps}
isTestnet={isTestnet}
ticker={ticker}
currentTicker={currentTicker}
toggleCurrencyProps={toggleCurrencyProps}
/>
<SpecificModal {...modalProps} network={network} ticker={ticker} currentTicker={currentTicker} toggleCurrencyProps={toggleCurrencyProps} />
</div>
)
}
@ -51,7 +47,7 @@ ActivityModal.propTypes = {
currentTicker: PropTypes.object.isRequired,
toggleCurrencyProps: PropTypes.object.isRequired,
isTestnet: PropTypes.bool.isRequired,
network: PropTypes.object.isRequired,
modalType: PropTypes.string,
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 now = new Date().getTime()
const distance = (this.props.countDownDate * 1000) - now
const countDownSeconds = this.props.countDownDate * 1000
const distance = countDownSeconds - now
if (distance <= 0) {
this.setState({ expired: true })
@ -56,32 +57,22 @@ class Countdown extends React.Component {
}
render() {
const {
days,
hours,
minutes,
seconds,
expired
} = this.state
const { days, hours, minutes, seconds, expired } = this.state
if (expired) { return <span className={`${styles.container} ${styles.expired}`}>Expired</span> }
if (!days && !hours && !minutes && !seconds) { return <span className={styles.container} /> }
if (expired) {
return <span className={`${styles.container} ${styles.expired}`}>Expired</span>
}
if (!days && !hours && !minutes && !seconds) {
return <span className={styles.container} />
}
return (
<span className={styles.container}>
<i className={styles.caption}>Expires in</i>
<i>
{days > 0 && `${days}:`}
</i>
<i>
{hours > 0 && `${hours}:`}
</i>
<i>
{minutes > 0 && `${minutes}:`}
</i>
<i>
{seconds >= 0 && `${seconds}`}
</i>
<i>{days > 0 && `${days}:`}</i>
<i>{hours > 0 && `${hours}:`}</i>
<i>{minutes > 0 && `${minutes}:`}</i>
<i>{seconds >= 0 && `${seconds}`}</i>
</span>
)
}

38
app/components/Activity/InvoiceModal.js

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

25
app/components/Activity/PaymentModal.js

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

44
app/components/Activity/TransactionModal.js

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

56
app/components/Contacts/AddChannel.js

@ -21,7 +21,7 @@ const AddChannel = ({
showManualForm,
openManualForm
}) => {
const renderRightSide = (node) => {
const renderRightSide = node => {
if (loadingChannelPubkeys.includes(node.pub_key)) {
return (
<span className={styles.inactive}>
@ -57,11 +57,7 @@ const AddChannel = ({
}
if (!node.addresses.length) {
return (
<span className={`${styles.private} ${styles.inactive}`}>
Private
</span>
)
return <span className={`${styles.private} ${styles.inactive}`}>Private</span>
}
return (
@ -79,7 +75,7 @@ const AddChannel = ({
)
}
const searchUpdated = (search) => {
const searchUpdated = search => {
updateContactFormSearchQuery(search)
if (search.includes('@') && search.split('@')[0].length === 66) {
@ -91,8 +87,8 @@ const AddChannel = ({
<div className={styles.container}>
<header className={styles.header}>
<input
type='text'
placeholder='Search the network...'
type="text"
placeholder="Search the network..."
className={styles.searchInput}
value={contactsform.searchQuery}
onChange={event => searchUpdated(event.target.value)}
@ -105,38 +101,36 @@ const AddChannel = ({
<section className={styles.nodes}>
<ul className={styles.networkResults}>
{
filteredNetworkNodes.map(node => (
{filteredNetworkNodes.map(node => (
<li key={node.pub_key}>
<section>
{
node.alias.length > 0 ?
<h2>
<span>{node.alias.trim()}</span>
<span>({node.pub_key.substr(0, 10)}...{node.pub_key.substr(node.pub_key.length - 10)})</span>
</h2>
:
<h2>
<span>{node.pub_key}</span>
</h2>
}
</section>
<section>
{renderRightSide(node)}
{node.alias.length > 0 ? (
<h2>
<span>{node.alias.trim()}</span>
<span>
({node.pub_key.substr(0, 10)}...{node.pub_key.substr(node.pub_key.length - 10)})
</span>
</h2>
) : (
<h2>
<span>{node.pub_key}</span>
</h2>
)}
</section>
<section>{renderRightSide(node)}</section>
</li>
))
}
))}
</ul>
</section>
{
showManualForm &&
{showManualForm && (
<section className={styles.manualForm}>
<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>
}
)}
</div>
)
}

5
app/components/Contacts/ChannelForm.js

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

13
app/components/Contacts/ConnectManually.js

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

48
app/components/Contacts/ContactModal.js

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

95
app/components/Contacts/ContactsForm.js

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

178
app/components/Contacts/Network.js

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

46
app/components/Contacts/SubmitChannelForm.js

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

20
app/components/Contacts/SubmitChannelForm.scss

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

37
app/components/Contacts/SuggestedNodes.js

@ -2,13 +2,8 @@ import React from 'react'
import PropTypes from 'prop-types'
import styles from './SuggestedNodes.scss'
const SuggestedNodes = ({
suggestedNodesLoading,
suggestedNodes,
setNode,
openSubmitChannelForm
}) => {
const nodeClicked = (n) => {
const SuggestedNodes = ({ suggestedNodesLoading, suggestedNodes, setNode, openSubmitChannelForm }) => {
const nodeClicked = n => {
// set the node public key for the submit form
setNode({ pub_key: n.pubkey, addresses: [{ addr: n.host }] })
// open the submit form
@ -26,24 +21,20 @@ const SuggestedNodes = ({
return (
<div className={styles.container}>
<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>
<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>
<ul className={styles.suggestedNodes}>
{
suggestedNodes.map(node => (
<li key={node.pubkey}>
<section>
<span>{node.nickname}</span>
<span>{`${node.pubkey.substring(0, 30)}...`}</span>
</section>
<section>
<span onClick={() => nodeClicked(node)}>Connect</span>
</section>
</li>
))
}
{suggestedNodes.map(node => (
<li key={node.pubkey}>
<section>
<span>{node.nickname}</span>
<span>{`${node.pubkey.substring(0, 30)}...`}</span>
</section>
<section>
<span onClick={() => nodeClicked(node)}>Connect</span>
</section>
</li>
))}
</ul>
</div>
)

7
app/components/CurrencyIcon/CurrencyIcon.js

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

5
app/components/Form/Form.js

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

106
app/components/Form/Pay.js

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

48
app/components/Form/Request.js

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

4
app/components/Onboarding/Alias.js

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

28
app/components/Onboarding/ConnectionDetails.js

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

2
app/components/Onboarding/FormContainer.js

@ -36,7 +36,7 @@ const FormContainer = ({
<footer className={styles.footer}>
<div className={styles.buttonsContainer}>
<section>
{back && (
{back && (
<div onClick={back} className={styles.backButton} >
<FaAngleLeft style={{ verticalAlign: 'top' }} /> Back
</div>

9
app/components/Onboarding/InitWallet.js

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

23
app/components/Onboarding/Login.js

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

8
app/components/Onboarding/NewAezeedPassword.js

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

8
app/components/Onboarding/NewWalletPassword.js

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

22
app/components/Onboarding/NewWalletSeed.js

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

54
app/components/Onboarding/Onboarding.js

@ -51,12 +51,12 @@ const Onboarding = ({
case 0.1:
return (
<FormContainer
title='How do you want to connect to the Lightning Network?'
description='
title="How do you want to connect to the Lightning Network?"
description="
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
use Zap to control a remote node if you desire (for advanced users).
'
"
back={null}
next={() => changeStep(connectionType === 'local' ? 1 : 0.2)}
>
@ -67,8 +67,8 @@ const Onboarding = ({
case 0.2:
return (
<FormContainer
title='Connection details'
description='Enter the connection details for your Lightning node.'
title="Connection details"
description="Enter the connection details for your Lightning node."
back={() => changeStep(0.1)}
next={() =>
startLnd({
@ -86,8 +86,8 @@ const Onboarding = ({
case 1:
return (
<FormContainer
title='What should we call you?'
description='Set your nickname to help others connect with you on the Lightning Network'
title="What should we call you?"
description="Set your nickname to help others connect with you on the Lightning Network"
back={() => changeStep(0.1)}
next={() => changeStep(2)}
>
@ -97,8 +97,8 @@ const Onboarding = ({
case 2:
return (
<FormContainer
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
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
back={() => changeStep(1)}
next={() => startLnd({ connectionType, alias, autopilot })}
>
@ -108,8 +108,8 @@ const Onboarding = ({
case 3:
return (
<FormContainer
title='Welcome back!'
description='Enter your wallet password or create a new wallet' // eslint-disable-line
title="Welcome back!"
description="Enter your wallet password or create a new wallet" // eslint-disable-line
back={null}
next={null}
>
@ -119,8 +119,8 @@ const Onboarding = ({
case 4:
return (
<FormContainer
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
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
back={null}
next={() => {
// dont allow the user to move on if the confirmation password doesnt match the original password
@ -137,8 +137,8 @@ const Onboarding = ({
case 5:
return (
<FormContainer
title={'Alright, let\'s get set up'}
description='Would you like to create a new wallet or import an existing one?' // eslint-disable-line
title={"Alright, let's get set up"}
description="Would you like to create a new wallet or import an existing one?" // eslint-disable-line
back={() => changeStep(4)}
next={() => (initWalletProps.signupProps.signupForm.create ? changeStep(6) : changeStep(5.1))}
>
@ -148,8 +148,8 @@ const Onboarding = ({
case 5.1:
return (
<FormContainer
title='Import your seed'
description={'Recovering a wallet, nice. You don\'t need anyone else, you got yourself :)'} // eslint-disable-line
title="Import your seed"
description={"Recovering a wallet, nice. You don't need anyone else, you got yourself :)"} // eslint-disable-line
back={() => changeStep(5)}
next={() => changeStep(5.2)}
>
@ -159,8 +159,8 @@ const Onboarding = ({
case 5.2:
return (
<FormContainer
title='Seed passphrase'
description={'Enter your cipherseed passphrase (or just submit if you don\'t have one)'} // eslint-disable-line
title="Seed passphrase"
description={"Enter your cipherseed passphrase (or just submit if you don't have one)"} // eslint-disable-line
back={() => changeStep(5)}
next={() => {
const recoverySeed = recoverFormProps.seedInput.map(input => input.word)
@ -174,8 +174,8 @@ const Onboarding = ({
case 6:
return (
<FormContainer
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
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
back={() => changeStep(5)}
next={() => changeStep(7)}
>
@ -185,8 +185,8 @@ const Onboarding = ({
case 7:
return (
<FormContainer
title='Re-enter your seed'
description='Yeah I know, might be annoying, but just to be safe!' // eslint-disable-line
title="Re-enter your seed"
description="Yeah I know, might be annoying, but just to be safe!" // eslint-disable-line
back={() => changeStep(6)}
next={() => {
// don't allow them to move on if they havent re-entered the seed correctly
@ -203,12 +203,14 @@ const Onboarding = ({
case 8:
return (
<FormContainer
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
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
back={() => changeStep(6)}
next={() => {
// 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)
}}

37
app/components/Onboarding/ReEnterSeed.js

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

22
app/components/Onboarding/Signup.js

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

6
app/components/Onboarding/Syncing.js

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

13
app/components/Value/Value.js

@ -3,18 +3,15 @@ import PropTypes from 'prop-types'
import { btc } from 'utils'
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 (
<i>{btc.convert('sats', currency, value, currentTicker.price_usd)}</i>
)
return <i>{btc.convert('sats', currency, value, currentTicker.price_usd)}</i>
}
Value.propTypes = {
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
currency: PropTypes.string.isRequired,
currentTicker: PropTypes.object.isRequired
}

40
app/components/Wallet/ReceiveModal.js

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

78
app/components/Wallet/Wallet.js

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

6
app/containers/Root.js

@ -219,4 +219,8 @@ Root.propTypes = {
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 methods from './methods'
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 lnd = lightning(lndConfig.lightningRpc, lndConfig.lightningHost)
const lndSubscribe = mainWindow => subscribe(mainWindow, lnd)
const lndMethods = (event, msg, data) => methods(lnd, event, msg, data)
const lndSubscribe = mainWindow => subscribe(mainWindow, lnd, mainLog)
const lndMethods = (event, msg, data) => methods(lnd, mainLog, event, msg, data)
callback(lndSubscribe, lndMethods)
}
const initWalletUnlocker = (callback) => {
const initWalletUnlocker = callback => {
const lndConfig = config.lnd()
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)
}

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:
// 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 || [
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES128-CBC-SHA256',
'ECDHE-ECDSA-CHACHA20-POLY1305'
].join(':')
process.env.GRPC_SSL_CIPHER_SUITES =
process.env.GRPC_SSL_CIPHER_SUITES ||
['ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-AES128-CBC-SHA256', 'ECDHE-ECDSA-CHACHA20-POLY1305'].join(':')
const lightning = (rpcpath, host) => {
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:
// 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 || [
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-ECDSA-AES128-CBC-SHA256',
'ECDHE-ECDSA-CHACHA20-POLY1305'
].join(':')
process.env.GRPC_SSL_CIPHER_SUITES =
process.env.GRPC_SSL_CIPHER_SUITES ||
['ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-AES128-CBC-SHA256', 'ECDHE-ECDSA-CHACHA20-POLY1305'].join(':')
const walletUnlocker = (rpcpath, host) => {
const lndConfig = config.lnd()

16
app/lnd/methods/channelController.js

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

91
app/lnd/methods/index.js

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

18
app/lnd/methods/invoicesController.js

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

24
app/lnd/methods/networkController.js

@ -6,14 +6,15 @@
export function getInfo(lnd) {
return new Promise((resolve, reject) => {
lnd.getInfo({}, (err, data) => {
if (err) { reject(err) }
if (err) {
reject(err)
}
resolve(data)
})
})
}
/**
* Returns general information concerning the lightning node
* @param {[type]} lnd [description]
@ -23,14 +24,15 @@ export function getInfo(lnd) {
export function getNodeInfo(lnd, { pubkey }) {
return new Promise((resolve, reject) => {
lnd.getNodeInfo({ pub_key: pubkey }, (err, data) => {
if (err) { reject(err) }
if (err) {
reject(err)
}
resolve(data)
})
})
}
/**
* Returns a description of the latest graph state from the point of view of the node
* @param {[type]} lnd [description]
@ -39,14 +41,15 @@ export function getNodeInfo(lnd, { pubkey }) {
export function describeGraph(lnd) {
return new Promise((resolve, reject) => {
lnd.describeGraph({}, (err, data) => {
if (err) { reject(err) }
if (err) {
reject(err)
}
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
* @param {[type]} lnd [description]
@ -57,14 +60,15 @@ export function describeGraph(lnd) {
export function queryRoutes(lnd, { pubkey, amount }) {
return new Promise((resolve, reject) => {
lnd.queryRoutes({ pub_key: pubkey, amt: amount }, (err, data) => {
if (err) { reject(err) }
if (err) {
reject(err)
}
resolve(data)
})
})
}
/**
* Returns some basic stats about the known channel graph from the point of view of the node
* @param {[type]} lnd [description]
@ -73,7 +77,9 @@ export function queryRoutes(lnd, { pubkey, amount }) {
export function getNetworkInfo(lnd) {
return new Promise((resolve, reject) => {
lnd.getNetworkInfo({}, (err, data) => {
if (err) { reject(err) }
if (err) {
reject(err)
}
resolve(data)
})

27
app/lnd/methods/paymentsController.js

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

14
app/lnd/methods/peersController.js

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

60
app/lnd/methods/walletController.js

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

6
app/lnd/subscribe/index.js

@ -2,8 +2,8 @@ import subscribeToTransactions from './transactions'
import subscribeToInvoices from './invoices'
import subscribeToChannelGraph from './channelgraph'
export default (mainWindow, lnd) => {
subscribeToTransactions(mainWindow, lnd)
subscribeToInvoices(mainWindow, lnd)
export default (mainWindow, lnd, log) => {
subscribeToTransactions(mainWindow, lnd, log)
subscribeToInvoices(mainWindow, lnd, log)
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({})
call.on('data', invoice => mainWindow.send('invoiceUpdate', { invoice }))
call.on('end', () => console.log('end'))
call.on('error', error => console.log('error: ', error))
call.on('status', status => console.log('status: ', status))
call.on('end', () => log.info('end'))
call.on('error', error => log.error(error))
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({})
call.on('data', (transaction) => {
console.log('TRANSACTION: ', transaction)
call.on('data', transaction => {
lnd.log.info('TRANSACTION:', transaction)
mainWindow.send('newTransaction', { transaction })
})
call.on('end', () => console.log('end'))
call.on('error', error => console.log('error: ', error))
call.on('status', status => console.log('TRANSACTION STATUS: ', status))
call.on('end', () => log.info('end'))
call.on('error', error => log.error('error: ', error))
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'
export default function (walletUnlocker, event, msg, data) {
export default function(walletUnlocker, log, event, msg, data) {
switch (msg) {
case 'genSeed':
walletController.genSeed(walletUnlocker)
walletController
.genSeed(walletUnlocker)
.then(genSeedData => event.sender.send('receiveSeed', genSeedData))
.catch(error => event.sender.send('receiveSeedError', error))
break
case 'unlockWallet':
walletController.unlockWallet(walletUnlocker, data)
walletController
.unlockWallet(walletUnlocker, data)
.then(() => event.sender.send('walletUnlocked'))
.catch(() => event.sender.send('unlockWalletError'))
break
case 'initWallet':
walletController.initWallet(walletUnlocker, data)
walletController
.initWallet(walletUnlocker, data)
.then(() => event.sender.send('successfullyCreatedWallet'))
.catch(error => console.log('initWallet error: ', error))
.catch(error => log.error('initWallet:', error))
break
default:
}

103
app/main.dev.js

@ -13,12 +13,14 @@
import { app, BrowserWindow, ipcMain, dialog } from 'electron'
import path from 'path'
import fs from 'fs'
import split2 from 'split2'
import { spawn } from 'child_process'
import { lookup } from 'ps-node'
import Store from 'electron-store'
import MenuBuilder from './menu'
import lnd from './lnd'
import config from './lnd/config'
import { mainLog, lndLog, lndLogGetLevel } from './utils/log'
let mainWindow = null
@ -43,7 +45,7 @@ const installExtensions = async () => {
const forceDownload = !!process.env.UPGRADE_EXTENSIONS
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
@ -67,7 +69,7 @@ const sendLndSyncing = () => {
clearInterval(sendLndSyncingInterval)
if (mainWindow) {
console.log('SENDING SYNCING')
mainLog.info('SENDING SYNCING')
startedSync = true
mainWindow.webContents.send('lndSyncing')
}
@ -81,7 +83,7 @@ const sendStartOnboarding = () => {
clearInterval(sendStartOnboardingInterval)
if (mainWindow) {
console.log('STARTING ONBOARDING')
mainLog.info('STARTING ONBOARDING')
mainWindow.webContents.send('startOnboarding')
}
}
@ -118,7 +120,7 @@ const startGrpc = () => {
// Create and subscribe the grpc object
const startWalletUnlocker = () => {
lnd.initWalletUnlocker((walletUnlockerMethods) => {
lnd.initWalletUnlocker(walletUnlockerMethods => {
// Listen for all gRPC restful methods
ipcMain.on('walletUnlocker', (event, { msg, data }) => {
walletUnlockerMethods(event, msg, data)
@ -135,7 +137,7 @@ const sendLndSynced = () => {
clearInterval(sendLndSyncedInterval)
if (mainWindow) {
console.log('SENDING SYNCED')
mainLog.info('SENDING SYNCED')
mainWindow.webContents.send('lndSynced')
}
}
@ -145,7 +147,12 @@ const sendLndSynced = () => {
// Starts the LND node
const startLnd = (alias, autopilot) => {
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 = [
'--bitcoin.active',
@ -159,35 +166,38 @@ const startLnd = (alias, autopilot) => {
]
const neutrino = spawn(lndConfig.lndPath, neutrinoArgs)
.on('error', (error) => {
console.log(`lnd error: ${error}`)
.on('error', error => {
lndLog.error(`lnd error: ${error}`)
dialog.showMessageBox({
type: 'error',
message: `lnd error: ${error}`
})
})
.on('close', (code) => {
console.log(`lnd shutting down ${code}`)
.on('close', code => {
lndLog.info(`lnd shutting down ${code}`)
app.quit()
})
// Listen for when neutrino prints out data
neutrino.stdout.on('data', (data) => {
// Data stored in variable line, log line to the console
const line = data.toString('utf8')
// Listen for when neutrino prints odata to stderr.
neutrino.stderr.pipe(split2()).on('data', line => {
if (process.env.NODE_ENV === 'development') {
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') {
console.log(line)
lndLog[lndLogGetLevel(line)](line)
}
// If the gRPC proxy has started we can start ours
if (line.includes('gRPC proxy started')) {
const certInterval = setInterval(() => {
console.log('lndConfig', lndConfig)
if (fs.existsSync(lndConfig.cert)) {
clearInterval(certInterval)
console.log('CERT EXISTS, STARTING WALLET UNLOCKER')
mainLog.info('CERT EXISTS, STARTING WALLET UNLOCKER')
startWalletUnlocker()
if (mainWindow) {
@ -198,7 +208,7 @@ const startLnd = (alias, autopilot) => {
}
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()
startGrpc()
}
@ -212,7 +222,7 @@ const startLnd = (alias, autopilot) => {
// When LND is all caught up to the blockchain
if (line.includes('Chain backend is fully synced')) {
// 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
sendLndSynced()
@ -274,14 +284,30 @@ app.on('ready', async () => {
sendGrpcDisconnected()
// Let the application know onboarding has started.
sendStartOnboarding()
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.
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
// once the onboarding has enough information, start or connect to LND.
ipcMain.on('startLnd', (event, options = {}) => {
console.log('STARTING LND', options)
const store = new Store({ name: 'connection' })
store.store = {
type: options.connectionType,
@ -292,29 +318,20 @@ app.on('ready', async () => {
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') {
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)
}
mainLog.info('SAVED LND CONFIG TO:', store.path)
// No LND process was found.
if (!results.length) {
startLnd(options.alias, options.autopilot)
} else {
// An LND process was found, no need to start our own.
console.log('LND ALREADY RUNNING')
startGrpc()
mainWindow.webContents.send('successfullyCreatedWallet')
}
})
if (options.connectionType === 'local') {
startLnd(options.alias, options.autopilot)
} else {
console.log('USING CUSTOM LND')
mainLog.info('CONNECTING TO CUSTOM LND INSTANCE')
startGrpc()
mainWindow.webContents.send('successfullyCreatedWallet')
}

228
app/menu.js

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

5
app/package.json

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

67
app/reducers/activity.js

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

2
app/reducers/address.js

@ -37,7 +37,7 @@ export function closeWalletModal() {
}
// Send IPC event for getinfo
export const newAddress = type => async (dispatch) => {
export const newAddress = type => async dispatch => {
dispatch(getAddress())
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
export const fetchBalance = () => async (dispatch) => {
export const fetchBalance = () => async dispatch => {
dispatch(getBalance())
ipcRenderer.send('lnd', { msg: '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 })
}
@ -30,11 +30,12 @@ export const receiveBalance = (event, { walletBalance, channelBalance }) => (dis
// ------------------------------------
const ACTION_HANDLERS = {
[GET_BALANCE]: state => ({ ...state, balanceLoading: true }),
[RECEIVE_BALANCE]: (state, { walletBalance, channelBalance }) => (
{
...state, balanceLoading: false, walletBalance, channelBalance
}
)
[RECEIVE_BALANCE]: (state, { walletBalance, channelBalance }) => ({
...state,
balanceLoading: false,
walletBalance,
channelBalance
})
}
// ------------------------------------

151
app/reducers/channels.js

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

74
app/reducers/contactsform.js

@ -195,9 +195,7 @@ const manualSearchQuerySelector = state => state.contactsform.manualSearchQuery
const contactCapacitySelector = state => state.contactsform.contactCapacity
const currencySelector = state => state.ticker.currency
const contactable = node => (
node.addresses.length > 0
)
const contactable = node => node.addresses.length > 0
// comparator to sort the contacts list with contactable contacts first
const contactableFirst = (a, b) => {
@ -209,70 +207,70 @@ const contactableFirst = (a, b) => {
return 0
}
contactFormSelectors.filteredNetworkNodes = createSelector(
networkNodesSelector,
searchQuerySelector,
(nodes, searchQuery) => {
// 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)
// if (!searchQuery.length) { return nodes.sort(contactableFirst).slice(0, 20) }
contactFormSelectors.filteredNetworkNodes = createSelector(networkNodesSelector, searchQuerySelector, (nodes, searchQuery) => {
// 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)
// if (!searchQuery.length) { return nodes.sort(contactableFirst).slice(0, 20) }
// return an empty array if there is no search query
if (!searchQuery.length) { return [] }
// return an empty array if there is no search query
if (!searchQuery.length) {
return []
}
// 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
const query = searchQuery.includes('@') ? searchQuery.split('@')[0] : searchQuery
// 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
const query = searchQuery.includes('@') ? searchQuery.split('@')[0] : searchQuery
// list of the nodes
const list = filter(nodes, node => node.alias.includes(query) || node.pub_key.includes(query)).sort(contactableFirst)
// list of the nodes
const list = filter(nodes, node => node.alias.includes(query) || node.pub_key.includes(query)).sort(contactableFirst)
// 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
return list.slice(0, 20)
}
)
// 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
return list.slice(0, 20)
})
contactFormSelectors.showManualForm = createSelector(
searchQuerySelector,
contactFormSelectors.filteredNetworkNodes,
(searchQuery, filteredNetworkNodes) => {
if (!searchQuery.length) { return false }
if (!searchQuery.length) {
return false
}
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
}
)
contactFormSelectors.manualFormIsValid = createSelector(
manualSearchQuerySelector,
(input) => {
const errors = {}
if (!input.length || !input.includes('@')) {
errors.manualInput = 'Invalid format'
}
return {
errors,
isValid: isEmpty(errors)
}
contactFormSelectors.manualFormIsValid = createSelector(manualSearchQuerySelector, input => {
const errors = {}
if (!input.length || !input.includes('@')) {
errors.manualInput = 'Invalid format'
}
)
return {
errors,
isValid: isEmpty(errors)
}
})
contactFormSelectors.contactFormUsdAmount = createSelector(
contactCapacitySelector,
currencySelector,
tickerSelectors.currentTicker,
(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)
}
)
export { contactFormSelectors }
// ------------------------------------

2
app/reducers/error.js

@ -32,7 +32,7 @@ export function clearError() {
// ------------------------------------
const ACTION_HANDLERS = {
[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 { blockExplorer } from 'utils'
// ------------------------------------
// Constants
@ -26,16 +26,30 @@ export function setWalletCurrencyFilters(showWalletCurrencyFilters) {
}
// Send IPC event for getinfo
export const fetchInfo = () => async (dispatch) => {
export const fetchInfo = () => async dispatch => {
dispatch(getInfo())
ipcRenderer.send('lnd', { msg: 'info' })
}
// Receive IPC event for info
export const receiveInfo = (event, data) => (dispatch) => {
export const receiveInfo = (event, data) => dispatch => {
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
// export const infoFailed = (event, data) => dispatch => {}
@ -44,7 +58,12 @@ export const receiveInfo = (event, data) => (dispatch) => {
// ------------------------------------
const ACTION_HANDLERS = {
[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 })
}
@ -53,23 +72,15 @@ const ACTION_HANDLERS = {
// ------------------------------------
const initialState = {
infoLoading: false,
network: {},
data: {},
showWalletCurrencyFilters: false
}
// Selectors
const infoSelectors = {}
const testnetSelector = state => state.info.data.testnet
infoSelectors.isTestnet = createSelector(
testnetSelector,
isTestnet => (!!isTestnet)
)
infoSelectors.explorerLinkBase = createSelector(
infoSelectors.isTestnet,
isTestnet => (isTestnet ? blockExplorer.testnetUrl : blockExplorer.mainnetUrl)
)
infoSelectors.testnetSelector = state => state.info.data.testnet
infoSelectors.networkSelector = state => state.info.network
export { infoSelectors }

43
app/reducers/invoice.js

@ -75,19 +75,19 @@ export function sendInvoice() {
}
// Send IPC event for a specific invoice
export const fetchInvoice = payreq => (dispatch) => {
export const fetchInvoice = payreq => dispatch => {
dispatch(getInvoice())
ipcRenderer.send('lnd', { msg: 'invoice', data: { payreq } })
}
// Receive IPC event for form invoice
export const receiveFormInvoice = (event, invoice) => (dispatch) => {
export const receiveFormInvoice = (event, invoice) => dispatch => {
dispatch(setPayInvoice(invoice))
dispatch({ type: RECEIVE_FORM_INVOICE })
}
// Send IPC event for invoices
export const fetchInvoices = () => (dispatch) => {
export const fetchInvoices = () => dispatch => {
dispatch(getInvoices())
ipcRenderer.send('lnd', { msg: 'invoices' })
}
@ -96,7 +96,7 @@ export const fetchInvoices = () => (dispatch) => {
export const receiveInvoices = (event, { invoices }) => dispatch => dispatch({ type: RECEIVE_INVOICES, invoices })
// 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
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
export const createdInvoice = (event, invoice) => (dispatch) => {
export const createdInvoice = (event, invoice) => dispatch => {
// Close the form modal once the payment was succesful
dispatch(setFormType(null))
@ -122,20 +122,20 @@ export const createdInvoice = (event, invoice) => (dispatch) => {
dispatch(showActivityModal('INVOICE', { invoice }))
}
export const invoiceFailed = (event, { error }) => (dispatch) => {
export const invoiceFailed = (event, { error }) => dispatch => {
dispatch({ type: INVOICE_FAILED })
dispatch(setError(error))
}
// 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 })
// Fetch new balance
dispatch(fetchBalance())
// 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'
showNotification(notifTitle, notifBody)
@ -156,14 +156,14 @@ const ACTION_HANDLERS = {
[RECEIVE_INVOICES]: (state, { invoices }) => ({ ...state, invoiceLoading: false, invoices }),
[SEND_INVOICE]: state => ({ ...state, invoiceLoading: true }),
[INVOICE_SUCCESSFUL]: (state, { invoice }) => (
{ ...state, invoiceLoading: false, invoices: [invoice, ...state.invoices] }
),
[INVOICE_SUCCESSFUL]: (state, { invoice }) => ({ ...state, invoiceLoading: false, invoices: [invoice, ...state.invoices] }),
[INVOICE_FAILED]: state => ({ ...state, invoiceLoading: false, data: null }),
[UPDATE_INVOICE]: (state, action) => {
const updatedInvoices = state.invoices.map((invoice) => {
if (invoice.r_hash.toString('hex') !== action.invoice.r_hash.toString('hex')) { return invoice }
const updatedInvoices = state.invoices.map(invoice => {
if (invoice.r_hash.toString('hex') !== action.invoice.r_hash.toString('hex')) {
return invoice
}
return {
...invoice,
@ -180,21 +180,14 @@ const invoiceSelector = state => state.invoice.invoice
const invoicesSelector = state => state.invoice.invoices
const invoicesSearchTextSelector = state => state.invoice.invoicesSearchText
invoiceSelectors.invoiceModalOpen = createSelector(
invoiceSelector,
invoice => (!!invoice)
)
invoiceSelectors.invoiceModalOpen = createSelector(invoiceSelector, invoice => !!invoice)
invoiceSelectors.invoices = createSelector(
invoicesSelector,
invoicesSearchTextSelector,
(invoices, invoicesSearchText) => invoices.filter(invoice => invoice.memo.includes(invoicesSearchText))
invoiceSelectors.invoices = createSelector(invoicesSelector, invoicesSearchTextSelector, (invoices, invoicesSearchText) =>
invoices.filter(invoice => invoice.memo.includes(invoicesSearchText))
)
invoiceSelectors.invoices = createSelector(
invoicesSelector,
invoicesSearchTextSelector,
(invoices, invoicesSearchText) => invoices.filter(invoice => invoice.memo.includes(invoicesSearchText))
invoiceSelectors.invoices = createSelector(invoicesSelector, invoicesSearchTextSelector, (invoices, invoicesSearchText) =>
invoices.filter(invoice => invoice.memo.includes(invoicesSearchText))
)
export { invoiceSelectors }

10
app/reducers/ipc.js

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

25
app/reducers/lnd.js

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

84
app/reducers/network.js

@ -128,16 +128,15 @@ export function clearSelectedChannels() {
}
// Send IPC event for describeNetwork
export const fetchDescribeNetwork = () => (dispatch) => {
export const fetchDescribeNetwork = () => dispatch => {
dispatch(getDescribeNetwork())
ipcRenderer.send('lnd', { msg: 'describeNetwork' })
}
// Receive IPC event for describeNetwork
export const receiveDescribeNetwork = (event, { nodes, edges }) => dispatch =>
dispatch({ type: RECEIVE_DESCRIBE_NETWORK, nodes, edges })
export const receiveDescribeNetwork = (event, { nodes, edges }) => dispatch => dispatch({ type: RECEIVE_DESCRIBE_NETWORK, nodes, edges })
export const queryRoutes = (pubkey, amount) => (dispatch) => {
export const queryRoutes = (pubkey, amount) => dispatch => {
dispatch(getQueryRoutes(pubkey))
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 })
// take a payreq and query routes for it
export const fetchInvoiceAndQueryRoutes = payreq => (dispatch) => {
export const fetchInvoiceAndQueryRoutes = payreq => dispatch => {
dispatch(getInvoiceAndQueryRoutes())
ipcRenderer.send('lnd', { msg: 'getInvoiceAndQueryRoutes', data: { payreq } })
}
export const receiveInvoiceAndQueryRoutes = (event, { routes }) => dispatch =>
dispatch({ type: RECEIVE_INFO_AND_QUERY_ROUTES, routes })
export const receiveInvoiceAndQueryRoutes = (event, { routes }) => dispatch => dispatch({ type: RECEIVE_INFO_AND_QUERY_ROUTES, routes })
// ------------------------------------
// Action Handlers
@ -159,17 +157,18 @@ export const receiveInvoiceAndQueryRoutes = (event, { routes }) => dispatch =>
const ACTION_HANDLERS = {
[GET_DESCRIBE_NETWORK]: state => ({ ...state, networkLoading: true }),
[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: {} } }),
[RECEIVE_QUERY_ROUTES]: (state, { routes }) => (
{
...state,
networkLoading: false,
selectedNode: { pubkey: state.selectedNode.pubkey, routes, currentRoute: routes[0] }
}
),
[RECEIVE_QUERY_ROUTES]: (state, { routes }) => ({
...state,
networkLoading: false,
selectedNode: { pubkey: state.selectedNode.pubkey, routes, currentRoute: routes[0] }
}),
[SET_CURRENT_ROUTE]: (state, { route }) => ({ ...state, currentRoute: route }),
@ -198,7 +197,8 @@ const ACTION_HANDLERS = {
}
return {
...state, selectedPeers
...state,
selectedPeers
}
},
[CLEAR_SELECTED_PEERS]: state => ({ ...state, selectedPeers: [] }),
@ -215,7 +215,8 @@ const ACTION_HANDLERS = {
}
return {
...state, selectedChannels
...state,
selectedChannels
}
},
[CLEAR_SELECTED_CHANNELS]: state => ({ ...state, selectedChannels: [] })
@ -239,38 +240,30 @@ const currentRouteSelector = state => state.network.currentRoute
// }
// )
networkSelectors.selectedPeerPubkeys = createSelector(
selectedPeersSelector,
peers => peers.map(peer => peer.pub_key)
)
networkSelectors.selectedChannelIds = createSelector(
selectedChannelsSelector,
channels => channels.map(channel => channel.chan_id)
)
networkSelectors.payReqIsLn = createSelector(
payReqSelector,
(input) => {
if (!input.startsWith('ln')) { return false }
try {
bech32.decode(input)
return true
} catch (e) {
return false
}
networkSelectors.selectedPeerPubkeys = createSelector(selectedPeersSelector, peers => peers.map(peer => peer.pub_key))
networkSelectors.selectedChannelIds = createSelector(selectedChannelsSelector, channels => channels.map(channel => channel.chan_id))
networkSelectors.payReqIsLn = createSelector(payReqSelector, input => {
if (!input.startsWith('ln')) {
return false
}
)
networkSelectors.currentRouteChanIds = createSelector(
currentRouteSelector,
(route) => {
if (!route.hops || !route.hops.length) { return [] }
try {
bech32.decode(input)
return true
} catch (e) {
return false
}
})
return route.hops.map(hop => hop.chan_id)
networkSelectors.currentRouteChanIds = createSelector(currentRouteSelector, route => {
if (!route.hops || !route.hops.length) {
return []
}
)
return route.hops.map(hop => hop.chan_id)
})
export { networkSelectors }
@ -295,7 +288,6 @@ const initialState = {
selectedChannels: []
}
// ------------------------------------
// 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
ipcRenderer.send('walletUnlocker', { msg: 'initWallet', data: { wallet_password, cipher_seed_mnemonic, aezeed_passphrase } })
dispatch({ type: CREATING_NEW_WALLET })
}
export const startOnboarding = () => (dispatch) => {
export const startOnboarding = () => dispatch => {
dispatch({ type: ONBOARDING_STARTED })
}
// Listener from after the LND walletUnlocker has started
export const walletUnlockerStarted = () => (dispatch) => {
export const walletUnlockerStarted = () => dispatch => {
dispatch({ type: LND_STARTED })
ipcRenderer.send('walletUnlocker', { msg: 'genSeed' })
}
export const createWallet = () => (dispatch) => {
export const createWallet = () => dispatch => {
ipcRenderer.send('walletUnlocker', { msg: 'genSeed' })
dispatch({ type: CHANGE_STEP, step: 4 })
}
@ -179,31 +179,31 @@ export const createWallet = () => (dispatch) => {
export const successfullyCreatedWallet = () => dispatch => dispatch({ type: ONBOARDING_FINISHED })
// 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 })
// 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 })
}
// 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 })
// there is already a seed, send user to the login component
dispatch({ type: CHANGE_STEP, step: 3 })
}
// 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 } })
dispatch({ type: UNLOCKING_WALLET })
}
export const walletUnlocked = () => (dispatch) => {
export const walletUnlocked = () => dispatch => {
dispatch({ type: WALLET_UNLOCKED })
dispatch({ type: ONBOARDING_FINISHED })
}
export const unlockWalletError = () => (dispatch) => {
export const unlockWalletError = () => dispatch => {
dispatch({ type: SET_UNLOCK_WALLET_ERROR })
}

115
app/reducers/payform.js

@ -5,6 +5,7 @@ import isEmpty from 'lodash/isEmpty'
import { setFormType } from './form'
import { tickerSelectors } from './ticker'
import { infoSelectors } from './info'
import { btc, bech32 } from '../utils'
// 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
dispatch(setFormType('PAY_FORM'))
// Set payreq
@ -102,7 +103,7 @@ const ACTION_HANDLERS = {
[UPDATE_PAY_ERRORS]: (state, { errorsObject }) => ({ ...state, showErrors: Object.assign(state.showErrors, errorsObject) }),
[RESET_FORM]: () => (initialState)
[RESET_FORM]: () => initialState
}
// ------------------------------------
@ -122,31 +123,27 @@ const sendingPaymentSelector = state => state.payment.sendingPayment
// ticker
const currencySelector = state => state.ticker.currency
payFormSelectors.isOnchain = createSelector(
payInputSelector,
(input) => {
try {
bitcoin.address.toOutputScript(input, bitcoin.networks.testnet)
return true
} catch (e) {
return false
}
payFormSelectors.isOnchain = createSelector(payInputSelector, infoSelectors.networkSelector, (input, network) => {
try {
bitcoin.address.toOutputScript(input, network.bitcoinJsNetwork)
return true
} catch (e) {
return false
}
)
})
payFormSelectors.isLn = createSelector(
payInputSelector,
(input) => {
if (!input.startsWith('ln')) { return false }
payFormSelectors.isLn = createSelector(payInputSelector, input => {
if (!input.startsWith('ln')) {
return false
}
try {
bech32.decode(input)
return true
} catch (e) {
return false
}
try {
bech32.decode(input)
return true
} catch (e) {
return false
}
)
})
payFormSelectors.currentAmount = createSelector(
payFormSelectors.isLn,
@ -157,9 +154,9 @@ payFormSelectors.currentAmount = createSelector(
if (isLn) {
switch (currency) {
case 'btc':
return btc.satoshisToBtc((invoice.num_satoshis || 0))
return btc.satoshisToBtc(invoice.num_satoshis || 0)
case 'bits':
return btc.satoshisToBits((invoice.num_satoshis || 0))
return btc.satoshisToBits(invoice.num_satoshis || 0)
case 'sats':
return invoice.num_satoshis
default:
@ -178,31 +175,30 @@ payFormSelectors.usdAmount = createSelector(
currencySelector,
tickerSelectors.currentTicker,
(isLn, amount, invoice, currency, ticker) => {
if (!ticker || !ticker.price_usd) { return false }
if (!ticker || !ticker.price_usd) {
return false
}
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)
}
)
payFormSelectors.payInputMin = createSelector(
currencySelector,
(currency) => {
switch (currency) {
case 'btc':
return '0.00000001'
case 'bits':
return '0.01'
case 'sats':
return '1'
default:
return '0'
}
payFormSelectors.payInputMin = createSelector(currencySelector, currency => {
switch (currency) {
case 'btc':
return '0.00000001'
case 'bits':
return '0.01'
case 'sats':
return '1'
default:
return '0'
}
)
})
payFormSelectors.inputCaption = createSelector(
payFormSelectors.isOnchain,
@ -210,7 +206,9 @@ payFormSelectors.inputCaption = createSelector(
payFormSelectors.currentAmount,
currencySelector,
(isOnchain, isLn, amount, currency) => {
if (!isOnchain && !isLn) { return '' }
if (!isOnchain && !isLn) {
return ''
}
if (isOnchain) {
return `You're about to send ${amount} ${currency.toUpperCase()} on-chain which should take around 10 minutes`
@ -230,29 +228,24 @@ payFormSelectors.showPayLoadingScreen = createSelector(
(sendingTransaction, sendingPayment) => sendingTransaction || sendingPayment
)
payFormSelectors.payFormIsValid = createSelector(
payFormSelectors.isOnchain,
payFormSelectors.isLn,
payAmountSelector,
(isOnchain, isLn, amount) => {
const errors = {}
payFormSelectors.payFormIsValid = createSelector(payFormSelectors.isOnchain, payFormSelectors.isLn, payAmountSelector, (isOnchain, isLn, amount) => {
const errors = {}
if (!isLn && amount <= 0) {
errors.amount = 'Amount must be more than 0'
}
if (!isLn && amount <= 0) {
errors.amount = 'Amount must be more than 0'
}
if (!isOnchain && !isLn) {
errors.payInput = 'Must be a valid BTC address or Lightning Network request'
}
if (!isOnchain && !isLn) {
errors.payInput = 'Must be a valid BTC address or Lightning Network request'
}
return {
errors,
amountIsValid: isEmpty(errors.amount),
payInputIsValid: isEmpty(errors.payInput),
isValid: isEmpty(errors)
}
return {
errors,
amountIsValid: isEmpty(errors.amount),
payInputIsValid: isEmpty(errors.payInput),
isValid: isEmpty(errors)
}
)
})
export { payFormSelectors }

14
app/reducers/payment.js

@ -63,7 +63,7 @@ export function hideSuccessScreen() {
}
// Send IPC event for payments
export const fetchPayments = () => (dispatch) => {
export const fetchPayments = () => dispatch => {
dispatch(getPayments())
ipcRenderer.send('lnd', { msg: 'payments' })
}
@ -73,7 +73,7 @@ export const receivePayments = (event, { payments }) => dispatch => dispatch({ t
// Receive IPC event for successful payment
// 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(paymentSuccessfull())
@ -90,12 +90,12 @@ export const paymentSuccessful = () => (dispatch) => {
dispatch(fetchBalance())
}
export const paymentFailed = (event, { error }) => (dispatch) => {
export const paymentFailed = (event, { error }) => dispatch => {
dispatch({ type: PAYMENT_FAILED })
dispatch(setError(error))
}
export const payInvoice = paymentRequest => (dispatch) => {
export const payInvoice = paymentRequest => dispatch => {
dispatch(sendPayment())
ipcRenderer.send('lnd', { msg: 'sendPayment', data: { paymentRequest } })
@ -112,7 +112,6 @@ export const payInvoice = paymentRequest => (dispatch) => {
// }, 10000)
}
// ------------------------------------
// Action Handlers
// ------------------------------------
@ -132,10 +131,7 @@ const ACTION_HANDLERS = {
const paymentSelectors = {}
const modalPaymentSelector = state => state.payment.payment
paymentSelectors.paymentModalOpen = createSelector(
modalPaymentSelector,
payment => (!!payment)
)
paymentSelectors.paymentModalOpen = createSelector(modalPaymentSelector, payment => !!payment)
export { paymentSelectors }

41
app/reducers/peers.js

@ -70,7 +70,7 @@ export function updateSearchQuery(searchQuery) {
}
// Send IPC event for peers
export const fetchPeers = () => async (dispatch) => {
export const fetchPeers = () => async dispatch => {
dispatch(getPeers())
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 })
// Send IPC event for connecting to a peer
export const connectRequest = ({ pubkey, host }) => (dispatch) => {
export const connectRequest = ({ pubkey, host }) => dispatch => {
dispatch(connectPeer())
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 })
// 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(setError(error))
}
// Send IPC send for disconnecting from a peer
export const disconnectRequest = ({ pubkey }) => (dispatch) => {
export const disconnectRequest = ({ pubkey }) => dispatch => {
dispatch(disconnectPeer())
ipcRenderer.send('lnd', { msg: 'disconnectPeer', data: { pubkey } })
}
@ -107,19 +107,21 @@ export const disconnectSuccess = (event, { pubkey }) => dispatch => dispatch({ t
// ------------------------------------
const ACTION_HANDLERS = {
[DISCONNECT_PEER]: state => ({ ...state, disconnecting: true }),
[DISCONNECT_SUCCESS]: (state, { pubkey }) => (
{
...state, disconnecting: false, peer: null, peers: state.peers.filter(peer => peer.pub_key !== pubkey)
}
),
[DISCONNECT_SUCCESS]: (state, { pubkey }) => ({
...state,
disconnecting: false,
peer: null,
peers: state.peers.filter(peer => peer.pub_key !== pubkey)
}),
[DISCONNECT_FAILURE]: state => ({ ...state, disconnecting: false }),
[CONNECT_PEER]: state => ({ ...state, connecting: true }),
[CONNECT_SUCCESS]: (state, { peer }) => (
{
...state, connecting: false, peerForm: { pubkey: '', host: '', isOpen: false }, peers: [...state.peers, peer]
}
),
[CONNECT_SUCCESS]: (state, { peer }) => ({
...state,
connecting: false,
peerForm: { pubkey: '', host: '', isOpen: false },
peers: [...state.peers, peer]
}),
[CONNECT_FAILURE]: state => ({ ...state, connecting: false }),
[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 peersSearchQuerySelector = state => state.peers.searchQuery
peersSelectors.peerModalOpen = createSelector(
peerSelector,
peer => (!!peer)
)
peersSelectors.peerModalOpen = createSelector(peerSelector, peer => !!peer)
peersSelectors.filteredPeers = createSelector(
peersSelector,
peersSearchQuerySelector,
(peers, query) => peers.filter(peer => peer.pub_key.includes(query) || peer.address.includes(query))
peersSelectors.filteredPeers = createSelector(peersSelector, peersSearchQuerySelector, (peers, query) =>
peers.filter(peer => peer.pub_key.includes(query) || peer.address.includes(query))
)
export { peersSelectors }

6
app/reducers/requestform.js

@ -57,7 +57,7 @@ const ACTION_HANDLERS = {
[SET_REQUEST_MEMO]: (state, { memo }) => ({ ...state, memo }),
[SET_REQUEST_CURRENCY_FILTERS]: (state, { showCurrencyFilters }) => ({ ...state, showCurrencyFilters }),
[RESET_FORM]: () => (initialState)
[RESET_FORM]: () => initialState
}
const requestFormSelectors = {}
@ -70,7 +70,9 @@ requestFormSelectors.usdAmount = createSelector(
tickerSelectors.currentTicker,
(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)
}

42
app/reducers/ticker.js

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

33
app/reducers/transaction.js

@ -52,7 +52,7 @@ export function hideSuccessTransactionScreen() {
}
// Send IPC event for payments
export const fetchTransactions = () => (dispatch) => {
export const fetchTransactions = () => dispatch => {
dispatch(getTransactions())
ipcRenderer.send('lnd', { msg: 'transactions' })
}
@ -60,9 +60,7 @@ export const fetchTransactions = () => (dispatch) => {
// Receive IPC event for payments
export const receiveTransactions = (event, { transactions }) => dispatch => dispatch({ type: RECEIVE_TRANSACTIONS, transactions })
export const sendCoins = ({
value, addr, currency
}) => (dispatch) => {
export const sendCoins = ({ value, addr, currency }) => dispatch => {
// backend needs amount in satoshis no matter what currency we are using
const amount = btc.convert(currency, 'sats', value)
@ -78,7 +76,7 @@ export const sendCoins = ({
// Receive IPC event for successful payment
// 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)
dispatch(fetchTransactions())
// Show successful payment state
@ -93,13 +91,13 @@ export const transactionSuccessful = (event, { txid }) => (dispatch) => {
dispatch(resetPayForm())
}
export const transactionError = (event, { error }) => (dispatch) => {
export const transactionError = (event, { error }) => dispatch => {
dispatch({ type: TRANSACTION_FAILED })
dispatch(setError(error))
}
// 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
dispatch(fetchBalance())
@ -107,7 +105,10 @@ export const newTransaction = (event, { transaction }) => (dispatch) => {
// HTML 5 desktop notification for the new transaction
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)
@ -115,7 +116,6 @@ export const newTransaction = (event, { transaction }) => (dispatch) => {
dispatch(newAddress('p2pkh'))
}
// ------------------------------------
// Action Handlers
// ------------------------------------
@ -125,14 +125,15 @@ const ACTION_HANDLERS = {
[RECEIVE_TRANSACTIONS]: (state, { transactions }) => ({ ...state, transactionLoading: false, transactions }),
[TRANSACTION_SUCCESSFULL]: 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
state.transactions.find(tx => (tx.tx_hash === transaction.tx_hash)) ? state : {
...state,
transactions: [transaction, ...state.transactions]
}
),
return state.transactions.find(tx => tx.tx_hash === transaction.tx_hash)
? state
: {
...state,
transactions: [transaction, ...state.transactions]
}
},
[SHOW_SUCCESS_TRANSACTION_SCREEN]: (state, { txid }) => ({ ...state, successTransactionScreen: { show: true, 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 = () => (
<App>
<Switch>
<Route path='/' component={Activity} />
<Route path="/" component={Activity} />
</Switch>
</App>
)

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

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

34
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 styles from '../Activity.scss'
const Invoice = ({
invoice, ticker, currentTicker, showActivityModal, currencyName
}) => (
const Invoice = ({ invoice, ticker, currentTicker, showActivityModal, currencyName }) => (
<div className={`${styles.container} ${!invoice.settled && styles.unpaid}`} onClick={() => showActivityModal('INVOICE', { invoice })}>
{
!invoice.settled && (
<div className={styles.pendingIcon}>
<Isvg src={checkmarkIcon} />
</div>
)
}
{!invoice.settled && (
<div className={styles.pendingIcon}>
<Isvg src={checkmarkIcon} />
</div>
)}
<div className={styles.data}>
<div className={styles.title}>
<h3>
{ invoice.settled ? 'Received payment' : 'Requested payment' }
</h3>
<h3>{invoice.settled ? 'Received payment' : 'Requested payment'}</h3>
</div>
<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 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>
<Value
value={invoice.value}
currency={ticker.currency}
currentTicker={currentTicker}
/>
<Value value={invoice.value} currency={ticker.currency} currentTicker={currentTicker} />
<i> {currencyName}</i>
</span>
<span>
<span>
${btc.convert('sats', 'usd', invoice.value, currentTicker.price_usd)}
</span>
<span>${btc.convert('sats', 'usd', invoice.value, currentTicker.price_usd)}</span>
</span>
</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 styles from '../Activity.scss'
const Payment = ({
payment, ticker, currentTicker, showActivityModal, nodes, currencyName
}) => {
const displayNodeName = (pubkey) => {
const Payment = ({ payment, ticker, currentTicker, showActivityModal, nodes, currencyName }) => {
const displayNodeName = pubkey => {
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)
}
@ -23,25 +23,19 @@ const Payment = ({
<div className={styles.container} onClick={() => showActivityModal('PAYMENT', { payment })}>
<div className={styles.data}>
<div className={styles.title}>
<h3>
{displayNodeName(payment.path[payment.path.length - 1])}
</h3>
<h3>{displayNodeName(payment.path[payment.path.length - 1])}</h3>
</div>
<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 className={styles.amount}>
<span className='hint--top' data-hint='Payment amount'>
<span className="hint--top" data-hint="Payment amount">
<i className={styles.minus}>-</i>
<Value
value={payment.value}
currency={ticker.currency}
currentTicker={currentTicker}
/>
<Value value={payment.value} currency={ticker.currency} currentTicker={currentTicker} />
<i> {currencyName}</i>
</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)}
</span>
</div>

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

@ -7,31 +7,23 @@ import { btc } from 'utils'
import Value from 'components/Value'
import styles from '../Activity.scss'
const Transaction = ({
transaction, ticker, currentTicker, showActivityModal, currencyName
}) => (
const Transaction = ({ transaction, ticker, currentTicker, showActivityModal, currencyName }) => (
<div className={styles.container} onClick={() => showActivityModal('TRANSACTION', { transaction })}>
<div className={styles.data}>
<div className={styles.title}>
<h3>
{ transaction.amount > 0 ? 'Received' : 'Sent' }
</h3>
<h3>{transaction.amount > 0 ? 'Received' : 'Sent'}</h3>
</div>
<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 className={`${styles.amount} ${transaction.amount > 0 ? styles.positive : styles.negative}`}>
<span className='hint--top' data-hint='Transaction amount'>
<i className={transaction.amount > 0 ? styles.plus : styles.minus}>{ transaction.amount > 0 ? '+' : '-' }</i>
<Value
value={transaction.amount}
currency={ticker.currency}
currentTicker={currentTicker}
/>
<span className="hint--top" data-hint="Transaction amount">
<i className={transaction.amount > 0 ? styles.plus : styles.minus}>{transaction.amount > 0 ? '+' : '-'}</i>
<Value value={transaction.amount} currency={ticker.currency} currentTicker={currentTicker} />
<i> {currencyName}</i>
</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)}
</span>
</div>

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

@ -1,16 +1,8 @@
import { connect } from 'react-redux'
import { setCurrency, tickerSelectors } from 'reducers/ticker'
import { fetchBalance } from 'reducers/balance'
import {
fetchInvoices,
setInvoice,
invoiceSelectors
} from 'reducers/invoice'
import {
setPayment,
fetchPayments,
paymentSelectors
} from 'reducers/payment'
import { fetchInvoices, setInvoice, invoiceSelectors } from 'reducers/invoice'
import { setPayment, fetchPayments, paymentSelectors } from 'reducers/payment'
import { fetchTransactions } from 'reducers/transaction'
import {
showActivityModal,
@ -81,8 +73,12 @@ const mapStateToProps = state => ({
showPayLoadingScreen: payFormSelectors.showPayLoadingScreen(state)
})
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const walletProps = {
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...stateProps,
...dispatchProps,
...ownProps,
walletProps: {
balance: stateProps.balance,
address: stateProps.address.address,
info: stateProps.info,
@ -93,7 +89,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
successTransactionScreen: stateProps.transaction.successTransactionScreen,
currentCurrencyFilters: stateProps.currentCurrencyFilters,
currencyName: stateProps.currencyName,
isTestnet: stateProps.info.data.testnet,
network: stateProps.info.network,
setCurrency: dispatchProps.setCurrency,
setWalletCurrencyFilters: dispatchProps.setWalletCurrencyFilters,
@ -102,14 +98,10 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
openPayForm: () => dispatchProps.setFormType('PAY_FORM'),
openRequestForm: () => dispatchProps.setFormType('REQUEST_FORM')
}
})
return {
...stateProps,
...dispatchProps,
...ownProps,
walletProps
}
}
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Activity)
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 {
componentWillMount() {
const {
fetchTicker,
fetchInfo,
newAddress,
fetchChannels,
fetchSuggestedNodes,
fetchBalance,
fetchDescribeNetwork
} = this.props
const { fetchTicker, fetchInfo, newAddress, fetchChannels, fetchSuggestedNodes, fetchBalance, fetchDescribeNetwork } = this.props
// fetch price ticker
fetchTicker()
@ -65,7 +57,9 @@ class App extends Component {
children
} = this.props
if (!currentTicker) { return <LoadingBolt /> }
if (!currentTicker) {
return <LoadingBolt />
}
return (
<div>
@ -79,16 +73,9 @@ class App extends Component {
<ReceiveModal {...receiveModalProps} />
<ActivityModal {...activityModalProps} />
<div className={styles.content}>
{children}
</div>
<div className={styles.content}>{children}</div>
{
contactsFormProps.contactsform.isOpen ?
<AddChannel {...contactsFormProps} />
:
<Network {...networkTabProps} />
}
{contactsFormProps.contactsform.isOpen ? <AddChannel {...contactsFormProps} /> : <Network {...networkTabProps} />}
<Form formType={form.formType} formProps={formProps} closeForm={closeForm} />
</div>

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

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

11
app/store/configureStore.dev.js

@ -8,7 +8,7 @@ import ipc from '../reducers/ipc'
const history = createHashHistory()
const configureStore = (initialState) => {
const configureStore = initialState => {
// Redux Configuration
const middleware = []
const enhancers = []
@ -35,9 +35,9 @@ const configureStore = (initialState) => {
/* eslint-disable no-underscore-dangle */
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Options: http://zalmoxisus.github.io/redux-devtools-extension/API/Arguments.html
actionCreators
})
// Options: http://zalmoxisus.github.io/redux-devtools-extension/API/Arguments.html
actionCreators
})
: compose
/* eslint-enable no-underscore-dangle */
@ -49,8 +49,7 @@ const configureStore = (initialState) => {
const store = createStore(rootReducer, initialState, enhancer)
if (module.hot) {
module.hot.accept('../reducers', () =>
store.replaceReducer(require('../reducers'))) // eslint-disable-line global-require
module.hot.accept('../reducers', () => store.replaceReducer(require('../reducers'))) // eslint-disable-line global-require
}
return store

17
app/utils/blockExplorer.js

@ -1,23 +1,14 @@
import { shell } from 'electron'
const testnetUrl = 'https://testnet.smartbit.com.au'
const mainnetUrl = 'https://smartbit.com.au'
const showTransaction = (network, txid) => shell.openExternal(`${network.explorerUrl}/tx/${txid}`)
const showTransaction = (isTestnet, txid) =>
(isTestnet ? shell.openExternal(`${testnetUrl}/tx/${txid}`) : shell.openExternal(`${mainnetUrl}/tx/${txid}`))
const showBlock = (network, blockHash) => shell.openExternal(`${network.explorerUrl}/block/${blockHash}`)
const showBlock = (isTestnet, blockHash) =>
(isTestnet ? shell.openExternal(`${testnetUrl}/block/${blockHash}`) : shell.openExternal(`${mainnetUrl}/block/${blockHash}`))
const showChannelClosing = channel => showTransaction(channel.closing_txid)
const showChannelClosing = channel =>
showTransaction(channel.closing_txid)
const showChannelPoint = channel =>
showTransaction(channel.channel.channel_point.split(':')[0])
const showChannelPoint = channel => showTransaction(channel.channel.channel_point.split(':')[0])
export default {
testnetUrl,
mainnetUrl,
showTransaction,
showBlock,
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) {
if (usd === undefined || usd === null || usd === '') return null
if (usd === undefined || usd === null || usd === '') {
return null
}
return (usd / rate).toFixed(8)
}

6
app/yarn.lock

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

4
appveyor.yml

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

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

Loading…
Cancel
Save