Browse Source

Merge remote-tracking branch 'upstream/master' into feature/README-update

renovate/lint-staged-8.x
helgabutters 7 years ago
parent
commit
f795b3f456
  1. 8
      README.md
  2. 7
      app/app.global.scss
  3. BIN
      app/app.icns
  4. 38
      app/components/Contacts/ClosingContact.js
  5. 156
      app/components/Contacts/Contact.scss
  6. 2
      app/components/Contacts/ContactModal.js
  7. 55
      app/components/Contacts/ContactsForm.js
  8. 21
      app/components/Contacts/ContactsForm.scss
  9. 35
      app/components/Contacts/LoadingContact.js
  10. 188
      app/components/Contacts/Network.js
  11. 188
      app/components/Contacts/Network.scss
  12. 34
      app/components/Contacts/OfflineContact.js
  13. 34
      app/components/Contacts/OnlineContact.js
  14. 38
      app/components/Contacts/PendingContact.js
  15. 101
      app/components/LndSyncing/LndSyncing.js
  16. 242
      app/components/LndSyncing/LndSyncing.scss
  17. 3
      app/components/LndSyncing/index.js
  18. 2
      app/components/LoadingBolt/LoadingBolt.scss
  19. 54
      app/components/Nav/Nav.js
  20. 187
      app/components/Nav/Nav.scss
  21. 3
      app/components/Nav/index.js
  22. 274
      app/components/Network/CanvasNetworkGraph.js
  23. 156
      app/components/Network/CanvasNetworkGraph.scss
  24. 45
      app/components/Network/ChannelsList.js
  25. 78
      app/components/Network/ChannelsList.scss
  26. 25
      app/components/Network/PeersList.js
  27. 46
      app/components/Network/PeersList.scss
  28. 63
      app/components/Network/TransactionForm.js
  29. 125
      app/components/Network/TransactionForm.scss
  30. 23
      app/components/Onboarding/Alias.js
  31. 15
      app/components/Onboarding/Alias.scss
  32. 55
      app/components/Onboarding/FormContainer.js
  33. 67
      app/components/Onboarding/FormContainer.scss
  34. 49
      app/components/Onboarding/Onboarding.js
  35. 0
      app/components/Onboarding/Onboarding.scss
  36. 45
      app/components/Onboarding/Syncing.js
  37. 50
      app/components/Onboarding/Syncing.scss
  38. 3
      app/components/Onboarding/index.js
  39. 2
      app/components/Wallet/ReceiveModal.js
  40. 50
      app/components/Wallet/Wallet.js
  41. 90
      app/components/Wallet/Wallet.scss
  42. 70
      app/containers/Root.js
  43. 16
      app/icons/bitcoin.svg
  44. 1
      app/icons/check_circle.svg
  45. 1
      app/icons/clock.svg
  46. 17
      app/icons/contacts.svg
  47. 1
      app/icons/help.svg
  48. 18
      app/icons/help_2.svg
  49. 14
      app/icons/network.svg
  50. 27
      app/icons/qrcode.svg
  51. 1
      app/icons/search.svg
  52. 15
      app/icons/wallet_2.svg
  53. BIN
      app/icons/zap.icns
  54. 16
      app/icons/zap_logo.svg
  55. 204
      app/lnd/config/rpc.proto
  56. 57
      app/lnd/lib/rpc.proto
  57. 65
      app/main.dev.js
  58. 66
      app/menu.js
  59. 10
      app/package.json
  60. 104
      app/reducers/activity.js
  61. 12
      app/reducers/channels.js
  62. 50
      app/reducers/contactsform.js
  63. 3
      app/reducers/index.js
  64. 6
      app/reducers/ipc.js
  65. 8
      app/reducers/lnd.js
  66. 69
      app/reducers/onboarding.js
  67. 4
      app/routes.js
  68. 55
      app/routes/activity/components/Activity.js
  69. 114
      app/routes/activity/components/Activity.scss
  70. 79
      app/routes/activity/components/components/Activity.scss
  71. 38
      app/routes/activity/components/components/Invoice/Invoice.js
  72. 2
      app/routes/activity/components/components/Modal/Modal.js
  73. 16
      app/routes/activity/components/components/Payment/Payment.js
  74. 16
      app/routes/activity/components/components/Transaction/Transaction.js
  75. 23
      app/routes/activity/containers/ActivityContainer.js
  76. 48
      app/routes/app/components/App.js
  77. 10
      app/routes/app/components/App.scss
  78. 135
      app/routes/app/containers/AppContainer.js
  79. 195
      app/routes/contacts/components/Contacts.js
  80. 172
      app/routes/contacts/components/Contacts.scss
  81. 109
      app/routes/contacts/containers/ContactsContainer.js
  82. 3
      app/routes/contacts/index.js
  83. 168
      app/routes/network/components/Network.js
  84. 99
      app/routes/network/components/Network.scss
  85. 62
      app/routes/network/containers/NetworkContainer.js
  86. 3
      app/routes/network/index.js
  87. 57
      app/rpc.proto
  88. 3
      app/utils/btc.js
  89. 8
      app/variables.scss
  90. 32
      app/yarn.lock
  91. 12
      package.json
  92. BIN
      resources/icon.icns
  93. BIN
      resources/icon.ico
  94. BIN
      resources/icon.png
  95. 31
      test/components/Nav.spec.js
  96. 72
      test/reducers/__snapshots__/channels.spec.js.snap
  97. 296
      yarn.lock

8
README.md

@ -11,7 +11,10 @@ Zap is a free Lightning Network wallet focused on user experience and ease of us
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).
<<<<<<< HEAD
We have an active [slack](https://join.slack.com/t/zaphq/shared_invite/enQtMjkyNTAxNDA3MjE2LWE3NGZjZGE5ZmI1NGQ5YTk3MGQzMTdmNDAwYjNhZTJkMWU0ZWZlNzA0MjJiNDBjMzcxYjcyMDMxNWY3OGNhYWQ) channel where you can join the discussion on development, design and product.
=======
>>>>>>> upstream/master
## Installing
@ -67,9 +70,14 @@ zap-desktop
Once you have the .AppImage file extracted, you can either **double click** the file or by running in the cli:
<<<<<<< HEAD
```bash
./file.AppImage
```
=======
## Todos:
Join us on [slack](https://join.slack.com/t/zaphq/shared_invite/enQtMzA4OTgxNTQ4NzUzLTQwYjkzZGM0ZWMwYmYyZTE2Y2E1YjM5NTIwOTU0M2I1Zjc2YWY1NTc4NjdhZWQxNTM1YzEzOGM2YTVlNWIwODc) before tackling a todo to avoid duplicate work.
>>>>>>> upstream/master
## 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.

7
app/app.global.scss

@ -24,6 +24,13 @@ body {
font-family: 'Roboto';
}
// disable the pinball scrollers for windows
*, input[type=text], input[type=number] {
&::-webkit-inner-spin-button, &::-webkit-outer-spin-button {
-webkit-appearance: none;
}
}
.ReactModal__Overlay {
transition: opacity 500ms ease-in-out;
opacity: 0;

BIN
app/app.icns

Binary file not shown.

38
app/components/Contacts/ClosingContact.js

@ -1,38 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FaCircle } from 'react-icons/lib/fa'
import { btc, blockExplorer } from 'utils'
import styles from './Contact.scss'
const ClosingContact = ({ channel }) => (
<li className={styles.friend}>
<section className={styles.info}>
<p className={styles.closing}>
<FaCircle style={{ verticalAlign: 'top' }} />
<span>
Removing
<i onClick={() => blockExplorer.showChannelClosing(channel)}>
(Details)
</i>
</span>
</p>
<h2>{channel.channel.remote_node_pub}</h2>
</section>
<section className={styles.limits}>
<div>
<h4>Can Pay</h4>
<p>{btc.satoshisToBtc(channel.channel.local_balance)}BTC</p>
</div>
<div>
<h4>Can Receive</h4>
<p>{btc.satoshisToBtc(channel.channel.remote_balance)}BTC</p>
</div>
</section>
</li>
)
ClosingContact.propTypes = {
channel: PropTypes.object.isRequired
}
export default ClosingContact

156
app/components/Contacts/Contact.scss

@ -1,156 +0,0 @@
@import '../../variables.scss';
.friend {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 30px 60px 60px 60px;
cursor: pointer;
transition: all 0.25s;
&.loading {
.info {
opacity: 0.2;
}
}
&:hover {
background: $lightgrey;
}
.limits {
display: flex;
flex-direction: row;
justify-content: space-between;
div {
margin: 0 10px;
h4 {
font-size: 12px;
margin-bottom: 20px;
}
}
}
.info {
p {
margin-bottom: 20px;
&.online {
color: $green;
svg {
color: $green;
}
}
&.pending {
color: $orange;
svg {
color: $orange;
}
i {
margin-left: 5px;
color: $darkestgrey;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
&.closing {
color: $red;
svg {
color: $red;
}
i {
margin-left: 5px;
color: $darkestgrey;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
svg, span {
display: inline-block;
vertical-align: top;
}
svg {
margin-right: 5px;
width: 12px;
height: 12px;
color: $darkestgrey;
}
span {
font-size: 12px;
}
}
h2 {
color: $black;
font-size: 14px;
font-weight: bold;
letter-spacing: 1.3px;
span {
color: $darkestgrey;
margin-left: 5px;
}
}
}
}
@-webkit-keyframes animation-rotate {
100% {
-webkit-transform: rotate(360deg);
}
}
@-moz-keyframes animation-rotate {
100% {
-moz-transform: rotate(360deg);
}
}
@-o-keyframes animation-rotate {
100% {
-o-transform: rotate(360deg);
}
}
@keyframes animation-rotate {
100% {
transform: rotate(360deg);
}
}
.spinner {
border: 1px solid rgba(0, 0, 0, 0.1);
border-left-color: rgba(0, 0, 0, 0.4);
-webkit-border-radius: 999px;
-moz-border-radius: 999px;
border-radius: 999px;
}
.spinner {
margin: 0 auto;
height: 50px;
width: 50px;
-webkit-animation: animation-rotate 1000ms linear infinite;
-moz-animation: animation-rotate 1000ms linear infinite;
-o-animation: animation-rotate 1000ms linear infinite;
animation: animation-rotate 1000ms linear infinite;
}

2
app/components/Contacts/ContactModal.js

@ -26,7 +26,7 @@ const ContactModal = ({
},
content: {
top: 'auto',
left: '20%',
left: '0',
right: '0',
bottom: 'auto',
width: '40%',

55
app/components/Contacts/ContactsForm.js

@ -10,29 +10,30 @@ class ContactsForm extends React.Component {
super(props)
this.state = {
editing: false,
manualFormInput: ''
editing: false
}
}
render() {
const {
contactsform,
contactsform: { showErrors },
closeContactsForm,
updateContactFormSearchQuery,
updateManualFormSearchQuery,
updateContactCapacity,
openChannel,
updateManualFormErrors,
activeChannelPubkeys,
nonActiveChannelPubkeys,
pendingOpenChannelPubkeys,
filteredNetworkNodes,
loadingChannelPubkeys,
showManualForm
showManualForm,
manualFormIsValid
} = this.props
const { editing, manualFormInput } = this.state
const { editing } = this.state
const renderRightSide = (node) => {
if (loadingChannelPubkeys.includes(node.pub_key)) {
@ -94,15 +95,29 @@ class ContactsForm extends React.Component {
this.setState({ editing: true })
}
const manualFormSubmit = () => {
if (!manualFormInput.length) { return }
if (!manualFormInput.includes('@')) { return }
if (!manualFormIsValid.isValid) {
updateManualFormErrors(manualFormIsValid.errors)
updateManualFormSearchQuery('')
return
}
// clear any existing errors
const [pubkey, host] = manualFormInput && manualFormInput.split('@')
updateManualFormErrors({ manualInput: null })
const [pubkey, host] = contactsform.manualSearchQuery && contactsform.manualSearchQuery.split('@')
openChannel({ pubkey, host, local_amt: contactsform.contactCapacity })
this.setState({ manualFormInput: '' })
updateManualFormSearchQuery('')
}
const searchUpdated = (search) => {
updateContactFormSearchQuery(search)
if (search.includes('@') && search.split('@')[0].length === 66) {
updateManualFormSearchQuery(search)
}
}
return (
@ -132,7 +147,7 @@ class ContactsForm extends React.Component {
placeholder='Find contact by alias or pubkey'
className={styles.searchInput}
value={contactsform.searchQuery}
onChange={event => updateContactFormSearchQuery(event.target.value)}
onChange={event => searchUpdated(event.target.value)}
/>
</div>
@ -170,8 +185,8 @@ class ContactsForm extends React.Component {
<input
type='text'
placeholder='pubkey@host'
value={manualFormInput}
onChange={event => this.setState({ manualFormInput: event.target.value })}
value={contactsform.manualSearchQuery}
onChange={event => updateManualFormSearchQuery(event.target.value)}
/>
<div className={styles.submit} onClick={manualFormSubmit}>Submit</div>
@ -184,6 +199,12 @@ class ContactsForm extends React.Component {
</div>
}
</section>
<section className={`${styles.errorMessage} ${showErrors.manualInput && styles.active}`}>
{showErrors.manualInput &&
<span>{manualFormIsValid && manualFormIsValid.errors.manualInput}</span>
}
</section>
</div>
}
@ -220,14 +241,18 @@ class ContactsForm extends React.Component {
}
}
ContactsForm.propTypes = {
contactsform: PropTypes.object.isRequired,
closeContactsForm: PropTypes.func.isRequired,
updateContactFormSearchQuery: PropTypes.func.isRequired,
updateManualFormSearchQuery: PropTypes.func.isRequired,
manualFormIsValid: PropTypes.shape({
errors: PropTypes.object,
isValid: PropTypes.bool
}).isRequired,
updateContactCapacity: PropTypes.func.isRequired,
updateManualFormErrors: PropTypes.func.isRequired,
openChannel: PropTypes.func.isRequired,
activeChannelPubkeys: PropTypes.array.isRequired,
nonActiveChannelPubkeys: PropTypes.array.isRequired,
pendingOpenChannelPubkeys: PropTypes.array.isRequired,

21
app/components/Contacts/ContactsForm.scss

@ -6,7 +6,7 @@
margin: 50px auto;
position: absolute;
top: auto;
left: 20%;
left: 0;
right: 0;
bottom: auto;
background: $white;
@ -49,7 +49,7 @@
.networkResults {
overflow-y: scroll;
height: 300px;
height: 250px;
margin-top: 30px;
padding: 20px 0;
@ -156,11 +156,23 @@
.manualFormSpinner {
position: absolute;
right: 0;
top: 50%;
top: 40%;
padding: 0 10px;
}
}
.errorMessage {
margin: 10px 0;
min-height: 20px;
color: $red;
opacity: 0;
transition: all 0.25s ease;
&.active {
opacity: 1;
}
}
.footer {
padding: 10px 15px;
border-top: 1px solid $darkgrey;
@ -243,5 +255,4 @@
-moz-animation: animation-rotate 1000ms linear infinite;
-o-animation: animation-rotate 1000ms linear infinite;
animation: animation-rotate 1000ms linear infinite;
}
}

35
app/components/Contacts/LoadingContact.js

@ -1,35 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FaCircle } from 'react-icons/lib/fa'
import styles from './Contact.scss'
const LoadingContact = ({ pubkey, isClosing }) => (
<li className={`${styles.friend} ${styles.loading}`}>
<section className={styles.info}>
<p>
<FaCircle style={{ verticalAlign: 'top' }} />
<span>
{
isClosing ?
'Closing'
:
'Loading'
}
</span>
</p>
<h2>{pubkey}</h2>
</section>
<section className={styles.limits}>
<div className={styles.loading}>
<div className={styles.spinner} />
</div>
</section>
</li>
)
LoadingContact.propTypes = {
pubkey: PropTypes.string.isRequired,
isClosing: PropTypes.bool.isRequired
}
export default LoadingContact

188
app/components/Contacts/Network.js

@ -0,0 +1,188 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import find from 'lodash/find'
import Isvg from 'react-inlinesvg'
import { FaAngleDown, FaCircle, FaRepeat } from 'react-icons/lib/fa'
import { btc } from 'utils'
import plus from 'icons/plus.svg'
import search from 'icons/search.svg'
import styles from './Network.scss'
class Network extends Component {
constructor(props) {
super(props)
this.state = {
refreshing: false
}
}
render() {
const {
channels: {
searchQuery,
filterPulldown,
filter
// loadingChannelPubkeys,
// closingChannelIds
},
currentChannels,
balance,
currentTicker,
nodes,
fetchChannels,
openContactsForm,
nonActiveFilters,
toggleFilterPulldown,
changeFilter,
updateChannelSearchQuery,
openContactModal
} = this.props
const refreshClicked = () => {
// turn the spinner on
this.setState({ refreshing: true })
// store event in icon so we dont get an error when react clears it
const icon = this.repeat.childNodes
// fetch channels
fetchChannels()
// wait for the svg to appear as child
const svgTimeout = setTimeout(() => {
if (icon[0].tagName === 'svg') {
// spin icon for 1 sec
icon[0].style.animation = 'spin 1000ms linear 1'
clearTimeout(svgTimeout)
}
}, 1)
// clear animation after the second so we can reuse it
const refreshTimeout = setTimeout(() => {
icon[0].style.animation = ''
this.setState({ refreshing: false })
clearTimeout(refreshTimeout)
}, 1000)
}
const displayNodeName = (channel) => {
const node = find(nodes, n => channel.remote_pubkey === n.pub_key)
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) => {
if (Object.prototype.hasOwnProperty.call(channel, 'confirmation_height')) { return 'pending' }
if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) { return 'closing' }
if (!channel.active) { return 'offline' }
return 'online'
}
const usdAmount = btc.satoshisToUsd(balance.channelBalance, currentTicker.price_usd)
return (
<div className={styles.network}>
<header className={styles.header}>
<section>
<h2>My Network</h2>
<span className={styles.channelAmount}>
{btc.satoshisToBtc(balance.channelBalance)}BTC ${usdAmount ? usdAmount.toLocaleString() : ''}
</span>
</section>
<section className={`${styles.addChannel} hint--bottom-left`} onClick={openContactsForm} data-hint='Open a channel'>
<Isvg src={plus} />
</section>
</header>
<div className={styles.channels}>
<header className={styles.listHeader}>
<section>
<h2 onClick={toggleFilterPulldown} className={styles.filterTitle}>
{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>
))
}
</ul>
</section>
<section className={styles.refreshContainer}>
<span className={styles.refresh} onClick={refreshClicked} ref={(ref) => { this.repeat = ref }}>
{
this.state.refreshing ?
<FaRepeat />
:
'Refresh'
}
</span>
</section>
</header>
<ul className={filterPulldown && styles.fade}>
{
currentChannels.length > 0 && currentChannels.map((channelObj, index) => {
const channel = Object.prototype.hasOwnProperty.call(channelObj, 'channel') ? channelObj.channel : channelObj
return (
<li key={index} className={styles.channel} onClick={() => openContactModal(channelObj)}>
<span>{displayNodeName(channel)}</span>
<span className={`${styles[channelStatus(channelObj)]} hint--left`} data-hint={channelStatus(channelObj)}>
<FaCircle />
</span>
</li>
)
})
}
</ul>
</div>
<footer className={styles.search}>
<label htmlFor='search' className={`${styles.label} ${styles.input}`}>
<Isvg src={search} />
</label>
<input
id='search'
type='text'
className={`${styles.text} ${styles.input}`}
placeholder='search by alias or pubkey'
value={searchQuery}
onChange={event => updateChannelSearchQuery(event.target.value)}
/>
</footer>
</div>
)
}
}
Network.propTypes = {
currentChannels: PropTypes.array.isRequired,
nodes: PropTypes.array.isRequired,
nonActiveFilters: PropTypes.array.isRequired,
channels: PropTypes.object.isRequired,
balance: PropTypes.object.isRequired,
currentTicker: PropTypes.object.isRequired,
fetchChannels: PropTypes.func.isRequired,
openContactsForm: PropTypes.func.isRequired,
toggleFilterPulldown: PropTypes.func.isRequired,
changeFilter: PropTypes.func.isRequired,
updateChannelSearchQuery: PropTypes.func.isRequired,
openContactModal: PropTypes.func.isRequired
}
export default Network

188
app/components/Contacts/Network.scss

@ -0,0 +1,188 @@
@import '../../variables.scss';
.network {
position: relative;
width: 20%;
display: inline-block;
vertical-align: top;
height: 100vh;
background: #31343f;
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
background: #2D303B;
padding: 10px 20px;
color: $white;
h2 {
font-size: 14px;
font-weight: bold;
letter-spacing: 1.2px;
margin-bottom: 5px;
}
.channelAmount {
font-size: 10px;
opacity: 0.5;
}
.addChannel {
cursor: pointer;
transition: all 0.25s;
&:hover {
color: $darkestgrey;
}
}
}
.channels {
padding: 20px;
height: 100%;
overflow-y: scroll;
.listHeader {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: baseline;
h2, h2 span {
color: $white;
cursor: pointer;
transition: color 0.25s;
&:hover {
color: $darkestgrey;
}
}
h2, .filters li {
font-size: 12px;
}
.filters {
display: none;
&.active {
display: block;
position: absolute;
bottom: -100px;
z-index: 10;
li {
margin: 10px 0;
cursor: pointer;
color: $white;
&:hover {
color: $darkestgrey;
}
}
}
}
span {
color: $white;
opacity: 1;
font-size: 10px;
cursor: pointer;
transition: all 0.25s;
&:hover {
opacity: 0.5;
}
}
}
ul {
margin-top: 20px;
height: 100%;
overflow-y: scroll;
}
.fade {
opacity: 0.1;
}
.channel {
display: flex;
flex-direction: row;
justify-content: space-between;
color: $white;
padding: 10px 0;
margin: 10px 0;
cursor: pointer;
span:nth-child(1) {
font-size: 12px;
}
.online {
color: $green;
}
.pending {
color: $orange;
}
.offline {
color: $darkestgrey;
}
.closing {
color: $red;
}
svg {
width: 5px;
height: 5px;
}
}
}
.search {
position: absolute;
bottom: 20px;
width: calc(100% - 40px);
padding: 10px 20px;
border-top: 1px solid $darkestgrey;
background: #2D303B;
.input {
display: inline-block;
vertical-align: top;
height: 100%;
}
.label {
width: 5%;
line-height: 50px;
font-size: 25px;
text-align: center;
cursor: pointer;
color: $white;
opacity: 0.5;
svg {
width: 14px;
height:14px;
}
}
.text {
width: calc(95% - 20px);
background: transparent;
outline: 0;
padding: 0 10px;
border: 0;
border-radius: 0;
height: 50px;
font-size: 12px;
color: $white;
}
}

34
app/components/Contacts/OfflineContact.js

@ -1,34 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FaCircle } from 'react-icons/lib/fa'
import { btc } from 'utils'
import styles from './Contact.scss'
const OfflineContact = ({ channel, openContactModal }) => (
<li className={styles.friend} key={channel.chan_id} onClick={() => openContactModal(channel)}>
<section className={styles.info}>
<p>
<FaCircle style={{ verticalAlign: 'top' }} />
<span>Offline</span>
</p>
<h2>{channel.remote_pubkey}</h2>
</section>
<section className={styles.limits}>
<div>
<h4>Can Pay</h4>
<p>{btc.satoshisToBtc(channel.local_balance)}BTC</p>
</div>
<div>
<h4>Can Receive</h4>
<p>{btc.satoshisToBtc(channel.remote_balance)}BTC</p>
</div>
</section>
</li>
)
OfflineContact.propTypes = {
channel: PropTypes.object.isRequired,
openContactModal: PropTypes.func.isRequired
}
export default OfflineContact

34
app/components/Contacts/OnlineContact.js

@ -1,34 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FaCircle } from 'react-icons/lib/fa'
import { btc } from 'utils'
import styles from './Contact.scss'
const OnlineContact = ({ channel, openContactModal }) => (
<li className={styles.friend} key={channel.chan_id} onClick={() => openContactModal(channel)}>
<section className={styles.info}>
<p className={styles.online}>
<FaCircle style={{ verticalAlign: 'top' }} />
<span>Online</span>
</p>
<h2>{channel.remote_pubkey}</h2>
</section>
<section className={styles.limits}>
<div>
<h4>Can Pay</h4>
<p>{btc.satoshisToBtc(channel.local_balance)}BTC</p>
</div>
<div>
<h4>Can Receive</h4>
<p>{btc.satoshisToBtc(channel.remote_balance)}BTC</p>
</div>
</section>
</li>
)
OnlineContact.propTypes = {
channel: PropTypes.object.isRequired,
openContactModal: PropTypes.func.isRequired
}
export default OnlineContact

38
app/components/Contacts/PendingContact.js

@ -1,38 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { FaCircle } from 'react-icons/lib/fa'
import { btc, blockExplorer } from 'utils'
import styles from './Contact.scss'
const PendingContact = ({ channel }) => (
<li className={styles.friend} key={channel.chan_id}>
<section className={styles.info}>
<p className={styles.pending}>
<FaCircle style={{ verticalAlign: 'top' }} />
<span>
Pending
<i onClick={() => blockExplorer.showChannelPoint(channel)}>
(Details)
</i>
</span>
</p>
<h2>{channel.channel.remote_node_pub}</h2>
</section>
<section className={styles.limits}>
<div>
<h4>Can Pay</h4>
<p>{btc.satoshisToBtc(channel.channel.local_balance)}BTC</p>
</div>
<div>
<h4>Can Receive</h4>
<p>{btc.satoshisToBtc(channel.channel.remote_balance)}BTC</p>
</div>
</section>
</li>
)
PendingContact.propTypes = {
channel: PropTypes.object.isRequired
}
export default PendingContact

101
app/components/LndSyncing/LndSyncing.js

@ -1,101 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { FaCopy } from 'react-icons/lib/fa'
import styles from './LndSyncing.scss'
class LndSyncing extends Component {
constructor(props) {
super(props)
this.state = {
facts: [
{
title: 'The Lightning Network',
description: 'The Lightning Network is a second layer solution built on top of the Bitcoin block chain that attempts to increase Bitcoin\'s scalability and privacy' // eslint-disable-line max-len
},
{
title: 'Payment Channel',
description: 'A payment channel is a class of techniques designed to allow users to make multiple Bitcoin transactions without commiting all of the transactions to the Bitcoin block chain. You can think of payment channels like tubes of money' // eslint-disable-line max-len
},
{
title: 'HTLC',
description: 'Hashed TimeLock Contracts is a class of payments that use hashlocks and timelocks to require the receiver of a payment either acknowledge receiving the payment before a deadline or forfeit the ability to claim the payment. HTLCs are useful within the Lightning Network for routing payments across two or more payment channels' // eslint-disable-line max-len
},
{
title: 'Onion Routing',
description: 'Onion routing is a technique for anonymous communication over a computer network. In an onion network, messages are encapsulated in layers of encryption, analogous to layers of an onion.' // eslint-disable-line max-len
}
],
currentFact: 0
}
}
componentWillMount() {
const { fetchBlockHeight, newAddress } = this.props
fetchBlockHeight()
newAddress('np2wkh')
}
render() {
const { syncPercentage, address: { address } } = this.props
const { facts, currentFact } = this.state
const renderCurrentFact = facts[currentFact]
return (
<div className={styles.container}>
<header>
<section>
<h3>zap</h3>
</section>
<section className={styles.loading}>
<h4>{syncPercentage}%</h4>
<div className={styles.spinner} />
</section>
</header>
<div className={styles.facts}>
<div className={styles.fact}>
<h2>{renderCurrentFact.title}</h2>
<p>{renderCurrentFact.description}</p>
</div>
<ul className={styles.factButtons}>
{
facts.map((fact, index) => (
<li
className={`${styles.factButton} ${currentFact === index && styles.active}`}
key={index}
onClick={() => this.setState({ currentFact: index })}
/>
))
}
</ul>
</div>
<div className={styles.footer}>
<section>
<h2>Fund your Zap wallet</h2>
<p>Deposit to your wallet while your node is syncing so autopilot can start working magic for you</p>
</section>
<section>
<div className={styles.address}>
<span>{address}</span>
<span className='hint--left' data-hint='Copy Address'>
<FaCopy />
</span>
</div>
</section>
</div>
</div>
)
}
}
LndSyncing.propTypes = {
newAddress: PropTypes.func.isRequired,
fetchBlockHeight: PropTypes.func.isRequired,
syncPercentage: PropTypes.number.isRequired,
address: PropTypes.object.isRequired
}
export default LndSyncing

242
app/components/LndSyncing/LndSyncing.scss

@ -1,242 +0,0 @@
@import '../../variables.scss';
.container {
height: 100vh;
background: $secondary;
header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: top;
padding: 100px;
}
h3 {
font-size: 50px;
color: $white;
}
.loading {
text-align: center;
position: relative;
.spinner, h1 {
display: inline-block;
vertical-align: top;
}
h4 {
position: absolute;
min-width: 100px;
top: calc(50% - 5px);
left: calc(50% - 48px);
color: $main;
font-size: 10px;
}
h1 {
color: $main;
line-height: 50px;
margin-left: 20px;
}
}
.facts {
color: $white;
.fact {
transition: all 0.25s;
width: 50%;
margin: 0 auto;
text-align: center;
line-height: 1.5;
letter-spacing: 1.1px;
height: 250px;
h2 {
font-size: 50px;
margin-bottom: 10px;
}
p {
margin-bottom: 20px;
}
}
}
.factButtons {
text-align: center;
}
.factButton {
cursor: pointer;
display: inline-block;
width: 15px;
height: 15px;
background: $white;
opacity: 0.5;
border-radius: 50%;
margin: 0 5px;
&:hover {
opacity: 0.75;
}
&.active {
opacity: 1;
}
}
}
.aliasForm {
width: 50%;
margin: 0 auto;
h1 {
text-align: center;
font-size: 32px;
color: $white;
}
p {
color: $darkgrey;
text-align: center;
margin-top: 20px;
font-weight: 100;
}
.inputContainer {
text-align: center;
margin-top: 50px;
}
.input {
padding: 20px;
font-size: 18px;
color: $darkestgrey;
background: lighten($black, 15%);
border: none;
outline: 0;
-webkit-appearance: none;
font-weight: 200;
width: calc(100% - 20px);
}
.submit {
background: $main;
color: $white;
font-size: 18px;
cursor: pointer;
width: 10%;
margin: 50px auto 0 auto;
padding: 20px 60px;
opacity: 0.5;
}
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
background: darken($secondary, 5%);
white-space: nowrap;
section {
display: inline-block;
vertical-align: top;
width: 50%;
white-space: normal;
padding: 30px 0;
h2 {
color: $white;
font-size: 22px;
letter-spacing: 1.2px;
font-weight: bold;
margin-bottom: 20px;
padding: 0 30px;
}
p {
color: $white;
padding: 0 30px;
line-height: 1.5;
}
}
.address {
display: flex;
flex-direction: row;
font-family: 'Roboto';
font-size: 14px;
font-weight: 200;
background: lighten($black, 15%);
color: $darkestgrey;
width: 75%;
margin: 0 auto;
span {
padding: 20px;
}
span:nth-child(1) {
flex: 9;
overflow-x: scroll;
font-size: 18px;
}
span:nth-child(2) {
background: $darkestgrey;
color: $white;
cursor: pointer;
transition: all 0.25s;
&:hover {
background: $darkestgrey;
}
}
}
}
.spinner {
border: 1px solid rgba(235, 184, 100, 0.1);
border-left-color: rgba(235, 184, 100, 0.4);
-webkit-border-radius: 999px;
-moz-border-radius: 999px;
border-radius: 999px;
}
.spinner {
margin: 0 auto;
height: 50px;
width: 50px;
-webkit-animation: animation-rotate 1000ms linear infinite;
-moz-animation: animation-rotate 1000ms linear infinite;
-o-animation: animation-rotate 1000ms linear infinite;
animation: animation-rotate 1000ms linear infinite;
}
@-webkit-keyframes animation-rotate {
100% {
-webkit-transform: rotate(360deg);
}
}
@-moz-keyframes animation-rotate {
100% {
-moz-transform: rotate(360deg);
}
}
@-o-keyframes animation-rotate {
100% {
-o-transform: rotate(360deg);
}
}
@keyframes animation-rotate {
100% {
transform: rotate(360deg);
}
}

3
app/components/LndSyncing/index.js

@ -1,3 +0,0 @@
import LndSyncing from './LndSyncing'
export default LndSyncing

2
app/components/LoadingBolt/LoadingBolt.scss

@ -7,7 +7,7 @@
left: 0;
width: 100%;
height: 100%;
background: $white;
background: $spaceblue;
text-align: center;
}

54
app/components/Nav/Nav.js

@ -1,54 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { NavLink } from 'react-router-dom'
import Isvg from 'react-inlinesvg'
import walletIcon from 'icons/wallet.svg'
import peersIcon from 'icons/peers.svg'
import networkIcon from 'icons/globe.svg'
import styles from './Nav.scss'
const Nav = ({ openPayForm, openRequestForm }) => (
<nav className={styles.nav}>
<header className={styles.header}>
<h1>zap</h1>
<span>beta</span>
</header>
<ul className={styles.links}>
<NavLink exact to='/' activeClassName={styles.active} className={styles.link}>
<li>
<Isvg styles={{ verticalAlign: 'middle' }} src={walletIcon} />
<span>Wallet</span>
</li>
</NavLink>
<NavLink exact to='/contacts' activeClassName={styles.active} className={styles.link}>
<li>
<Isvg styles={{ verticalAlign: 'middle' }} src={peersIcon} />
<span>Contacts</span>
</li>
</NavLink>
<NavLink exact to='/network' activeClassName={styles.active} className={styles.link}>
<li>
<Isvg styles={{ verticalAlign: 'middle' }} src={networkIcon} />
<span>Network</span>
</li>
</NavLink>
</ul>
<div className={styles.buttons}>
<div className={`buttonPrimary ${styles.button}`} onClick={openPayForm}>
<span>Pay</span>
</div>
<div className={`buttonPrimary ${styles.button}`} onClick={openRequestForm}>
<span>Request</span>
</div>
</div>
</nav>
)
Nav.propTypes = {
openPayForm: PropTypes.func.isRequired,
openRequestForm: PropTypes.func.isRequired
}
export default Nav

187
app/components/Nav/Nav.scss

@ -1,187 +0,0 @@
@import '../../variables.scss';
.nav {
display: inline-block;
vertical-align: top;
width: 20%;
font-size: 24px;
background: $secondary;
height: 100vh;
position: relative;
min-width: 15%;
color: $white;
}
.header {
padding: 20px;
h1 {
color: $main;
font-size: 30px;
font-weight: 300;
text-align: center;
float: left;
-webkit-font-smoothing: antialiased;
margin-right: 5px;
}
span {
font-family: "Open Sans", "Helvetica Neue", Helvetica;
color: #bbb;
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
text-align: right;
line-height: 100%;
display: inline-block;
vertical-align: middle;
}
}
.info {
padding: 25px 10px 10px 10px;
.link {
display: inline-block;
vertical-align: top;
list-style-type: none;
width: 50%;
cursor: pointer;
}
}
.currency {
margin: 0 1px;
&.active {
color: $main;
}
span {
display: inline-block;
vertical-align: middle;
svg[data-icon='ltc'] {
width: 24px;
height: 28px;
g {
transform: scale(1.75) translate(-5px, -5px);
}
}
}
}
.logo {
text-align: center;
margin-top: 20px;
margin-bottom: 35%;
svg {
width: 100px;
height: 100px;
}
}
.balance {
text-align: right;
color: $main;
p {
margin: 2px 0;
&:first-child {
font-size: 14px;
}
&:nth-child(2) {
font-size: 12px;
}
span {
display: inline-block;
vertical-align: top;
svg[data-icon='ltc'] {
width: 10px;
height: 10px;
g {
transform: scale(1.75) translate(-5px, -5px);
}
}
}
}
}
.links {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 50%;
.link {
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 100%;
color: $darkestgrey;
opacity: 0.5;
cursor: pointer;
text-decoration: none;
border-left: 20px solid transparent;
transition: all 0.25s;
li {
margin: 12.5px 0;
min-width: 200px;
}
&.active {
color: $main;
opacity: 1.0;
svg g {
stroke: $main;
}
}
svg {
width: 32px;
height: 32px;
vertical-align: middle;
}
span {
margin-left: 15px;
line-height: 22px;
font-size: 20px;
font-weight: 500;
letter-spacing: .2px;
}
}
}
.buttons {
width: 75%;
font-size: 18px;
position: absolute;
bottom: 10px;
right: 12.5%;
}
.button {
margin-bottom: 20px;
font-weight: bold;
cursor: pointer;
text-transform: uppercase;
letter-spacing: .2px;
}
.content {
width: 80%;
}

3
app/components/Nav/index.js

@ -1,3 +0,0 @@
import Nav from './Nav'
export default Nav

274
app/components/Network/CanvasNetworkGraph.js

@ -1,274 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import * as d3Force from 'd3-force'
import * as d3Selection from 'd3-selection'
import * as d3Zoom from 'd3-zoom'
import styles from './CanvasNetworkGraph.scss'
const d3 = Object.assign({}, d3Force, d3Selection, d3Zoom)
function generateSimulationData(nodes, edges) {
const resNodes = nodes.map(node => Object.assign(node, { id: node.pub_key }))
const resEdges = edges.map(node => Object.assign(node, { source: node.node1_pub, target: node.node2_pub }))
return {
nodes: resNodes,
links: resEdges
}
}
class CanvasNetworkGraph extends Component {
constructor(props) {
super(props)
this.state = {
simulationData: {
nodes: [],
links: []
},
svgLoaded: false
}
this.startSimulation = this.startSimulation.bind(this)
this.zoomActions = this.zoomActions.bind(this)
this.ticked = this.ticked.bind(this)
this.restart = this.restart.bind(this)
}
componentDidMount() {
// wait for the svg to be in the DOM before we start the simulation
const svgInterval = setInterval(() => {
if (document.getElementById('mapContainer')) {
d3.select('#mapContainer')
.append('svg')
.attr('id', 'map')
.attr('width', '100%')
.attr('height', '100%')
this.startSimulation()
clearInterval(svgInterval)
}
}, 1000)
}
componentWillReceiveProps(nextProps) {
const { network } = nextProps
const { simulationData: { nodes, links } } = this.state
const simulationDataEmpty = !nodes.length && !links.length
const networkDataLoaded = network.nodes.length || network.edges.length
const prevNetwork = this.props.network
if (
// update the simulationData only if
// the simulationData is empty and we have network data
(simulationDataEmpty && networkDataLoaded) ||
// the nodes or edges have changed
(prevNetwork.nodes.length !== network.nodes.length || prevNetwork.edges.length !== network.edges.length)) {
this.setState({
simulationData: generateSimulationData(network.nodes, network.edges)
})
}
}
componentDidUpdate(prevProps) {
const {
selectedPeerPubkeys,
selectedChannelIds,
currentRouteChanIds
} = this.props
if (prevProps.selectedPeerPubkeys.length !== selectedPeerPubkeys.length) {
this.updateSelectedPeers()
}
if (prevProps.selectedChannelIds.length !== selectedChannelIds.length) {
this.updateSelectedChannels()
}
if (prevProps.currentRouteChanIds.length !== currentRouteChanIds.length) {
this.renderSelectedRoute()
}
}
componentWillUnmount() {
d3.select('#map')
.remove()
}
updateSelectedPeers() {
const { selectedPeerPubkeys } = this.props
// remove active class
d3.selectAll('.active-peer').classed('active-peer', false)
// add active class to all selected peers
selectedPeerPubkeys.forEach((pubkey) => {
d3.select(`#node-${pubkey}`).classed('active-peer', true)
})
}
updateSelectedChannels() {
const { selectedChannelIds } = this.props
// remove active class
d3.selectAll('.active-channel').classed('active-channel', false)
// add active class to all selected peers
selectedChannelIds.forEach((chanid) => {
d3.select(`#link-${chanid}`).classed('active-channel', true)
})
}
startSimulation() {
const { simulationData: { nodes, links } } = this.state
// grab the svg el along with the attributes
const svg = d3.select('#map')
const svgBox = svg.node().getBBox()
this.g = svg.append('g').attr('transform', `translate(${svgBox.width / 2},${svgBox.height / 2})`)
this.link = this.g.append('g').attr('stroke', 'white').attr('stroke-width', 1.5).selectAll('.link')
this.node = this.g.append('g').attr('stroke', 'silver').attr('stroke-width', 1.5).selectAll('.node')
this.simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(-750))
.force('link', d3.forceLink(links).id(d => d.pub_key).distance(500))
.force('collide', d3.forceCollide(300))
.on('tick', this.ticked)
.on('end', () => {
this.setState({ svgLoaded: true })
})
// zoom
const zoom_handler = d3.zoom().on('zoom', this.zoomActions)
zoom_handler(svg)
this.restart()
}
zoomActions() {
this.g.attr('transform', d3Selection.event.transform)
}
ticked() {
this.node.attr('cx', d => d.x)
.attr('cy', d => d.y)
this.link.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y)
}
restart() {
const { identity_pubkey } = this.props
const { simulationData: { nodes, links } } = this.state
// Apply the general update pattern to the nodes.
this.node = this.node.data(nodes, d => d.pub_key)
this.node.exit().remove()
this.node = this.node.enter()
.append('circle')
.attr('stroke', () => 'silver')
.attr('fill', d => (d.pub_key === identity_pubkey ? '#FFF' : '#353535'))
.attr('r', () => 100)
.attr('id', d => `node-${d.pub_key}`)
.attr('class', 'network-node')
.merge(this.node)
// Apply the general update pattern to the links.
this.link = this.link.data(links, d => `${d.source.id}-${d.target.id}`)
this.link.exit().remove()
this.link =
this.link.enter()
.append('line')
.attr('id', d => `link-${d.channel_id}`)
.attr('class', 'network-link')
.merge(this.link)
// Update and restart the simulation.
this.simulation.nodes(nodes)
this.simulation.force('link').links(links)
this.simulation.restart()
}
renderSelectedRoute() {
const { currentRouteChanIds } = this.props
// remove all route animations before rendering new ones
d3.selectAll('.animated-route-circle').remove()
currentRouteChanIds.forEach((chanId) => {
const link = document.getElementById(`link-${chanId}`)
if (!link) { return }
const x1 = link.x1.baseVal.value
const x2 = link.x2.baseVal.value
const y1 = link.y1.baseVal.value
const y2 = link.y2.baseVal.value
// create the circle that represent btc traveling through a channel
this.g
.append('circle')
.attr('id', `circle-${chanId}`)
.attr('class', 'animated-route-circle')
.attr('r', 50)
.attr('cx', x1)
.attr('cy', y1)
.attr('fill', '#FFDC53')
// we want the animation to repeat back and forth, this function executes that visually
const repeat = () => {
d3.select(`#circle-${chanId}`)
.transition()
.attr('cx', x2)
.attr('cy', y2)
.duration(1000)
.transition()
.duration(1000)
.attr('cx', x1)
.attr('cy', y1)
.on('end', repeat)
}
// call repeat to animate the circle
repeat()
})
}
render() {
const { svgLoaded } = this.state
return (
<div className={styles.mapContainer} id='mapContainer'>
{
!svgLoaded &&
<div className={styles.loadingContainer}>
<div className={styles.loadingWrap}>
<div className={styles.loader} />
<div className={styles.loaderbefore} />
<div className={styles.circular} />
<div className={`${styles.circular} ${styles.another}`} />
<div className={styles.text}>loading</div>
</div>
</div>
}
</div>
)
}
}
CanvasNetworkGraph.propTypes = {
identity_pubkey: PropTypes.string.isRequired,
network: PropTypes.object.isRequired,
selectedPeerPubkeys: PropTypes.array.isRequired,
selectedChannelIds: PropTypes.array.isRequired,
currentRouteChanIds: PropTypes.array.isRequired
}
export default CanvasNetworkGraph

156
app/components/Network/CanvasNetworkGraph.scss

@ -1,156 +0,0 @@
@import '../../variables.scss';
@keyframes fadein {
0% { background: $white; }
50% { background: lighten($secondary, 50%); }
100% { background: $secondary; animation-fill-mode:forwards; }
}
.mapContainer {
position: relative;
display: inline-block;
width: 70%;
height: 100%;
}
.loadingContainer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: $secondary;
}
.loadingWrap {
position: relative;
top: calc(50% - 150px);
width: 150px;
margin: 0 auto;
}
.loader {
position: absolute;
top: 0;
z-index: 10;
width: 50px;
height: 50px;
border: 15px solid;
border-radius: 50%;
border-top-color: rgba(44,44,44,0);
border-right-color: rgba(55,55,55,0);
border-bottom-color: rgba(66,66,66,0);
border-left-color: rgba(33,33,33,0);
animation: loadEr 3s infinite;
}
@keyframes loadEr {
0% {
border-top-color: rgba(44,44,44,0);
border-right-color: rgba(55,55,55,0);
border-bottom-color: rgba(66,66,66,0);
border-left-color: rgba(33,33,33,0);
}
10.4% {
border-top-color: rgba(44,44,44,0.5);
border-right-color: rgba(55,55,55,0);
border-bottom-color: rgba(66,66,66,0);
border-left-color: rgba(33,33,33,0);
}
20.8% {
border-top-color: rgba(44,44,44,0);
border-right-color: rgba(55,55,55,0);
border-bottom-color: rgba(66,66,66,0);
border-left-color: rgba(33,33,33,0);
}
31.2% {
border-top-color: rgba(44,44,44,0);
border-right-color: rgba(55,55,55,0.5);
border-bottom-color: rgba(66,66,66,0);
border-left-color: rgba(33,33,33,0);
}
41.6% {
border-top-color: rgba(44,44,44,0);
border-right-color: rgba(55,55,55,0);
border-bottom-color: rgba(66,66,66,0);
border-left-color: rgba(33,33,33,0);
transform: rotate(40deg);
}
52% {
border-top-color: rgba(44,44,44,0);
border-right-color: rgba(55,55,55,0);
border-bottom-color: rgba(66,66,66,0.5);
border-left-color: rgba(33,33,33,0);
}
62.4% {
border-top-color: rgba(44,44,44,0);
border-right-color: rgba(55,55,55,0);
border-bottom-color: rgba(66,66,66,0);
border-left-color: rgba(33,33,33,0);
}
72.8% {
border-top-color: rgba(44,44,44,0);
border-right-color: rgba(55,55,55,0);
border-bottom-color: rgba(66,66,66,0);
border-left-color: rgba(33,33,33,0.5);
}
}
.loaderbefore {
width: 50px;
height:50px;
border: 15px solid #ddd;
border-radius: 50%;
position: absolute;
top: 0;
z-index: 9;
}
.circular {
position: absolute;
top: -15px;
left: -15px;
width: 70px;
height: 70px;
border: 20px solid;
border-radius: 50%;
border-top-color: #333;
border-left-color: #fff;
border-bottom-color: #333;
border-right-color: #fff;
opacity: 0.2;
animation: poof 5s infinite;
}
@keyframes poof {
0% {transform: scale(1,1) rotate(0deg); opacity: 0.2;}
50% {transform: scale(4,4) rotate(360deg); opacity: 0;}
}
.another {
opacity: 0.1;
transform: rotate(90deg);
animation: poofity 5s infinite;
animation-delay: 1s;
}
@keyframes poofity {
0% {transform: scale(1,1) rotate(90deg); opacity: 0.1;}
50% {transform: scale(4,4) rotate(-360deg); opacity: 0;}
}
.text {
position: absolute;
top: 95px;
left: 8px;
font-family: Arial;
text-transform: uppercase;
color: #888;
animation: opaa 10s infinite;
}
@keyframes opaa {
0% {opacity: 1;}
10% {opacity: 0.5}
15% {opacity: 1;}
30% {opacity: 1;}
65% {opacity: 0.3;}
90% {opacity: 0.8;}
}

45
app/components/Network/ChannelsList.js

@ -1,45 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { btc, blockExplorer } from 'utils'
import styles from './ChannelsList.scss'
const ChannelsList = ({ channels, updateSelectedChannels, selectedChannelIds }) => (
<ul className={styles.channels}>
{
channels.map(channel => (
<li key={channel.chan_id} className={styles.channel} onClick={() => updateSelectedChannels(channel)}>
<span className={`${styles.dot} ${selectedChannelIds.includes(channel.chan_id) && styles.active}`} />
<header>
<h1>Capacity: {btc.satoshisToBtc(channel.capacity)}</h1>
<span onClick={() => blockExplorer.showChannelPoint({ channel })}>Channel Point</span>
</header>
<section>
<h4>Remote Pubkey:</h4>
<p>{channel.remote_pubkey.substring(0, Math.min(30, channel.remote_pubkey.length))}...</p>
</section>
<section className={styles.funds}>
<div>
<h4>Sent:</h4>
<p>{btc.satoshisToBtc(channel.total_satoshis_sent)} BTC</p>
</div>
<div>
<h4>Received:</h4>
<p>{btc.satoshisToBtc(channel.total_satoshis_received)} BTC</p>
</div>
</section>
</li>
))
}
</ul>
)
ChannelsList.propTypes = {
channels: PropTypes.array.isRequired,
updateSelectedChannels: PropTypes.func.isRequired,
selectedChannelIds: PropTypes.array.isRequired
}
export default ChannelsList

78
app/components/Network/ChannelsList.scss

@ -1,78 +0,0 @@
@import '../../variables.scss';
.channels {
color: $white;
margin-top: 50px;
}
.channel {
position: relative;
margin: 20px 0;
padding: 10px 40px;
cursor: pointer;
transition: all 0.25s;
&:hover {
background: darken(#353535, 10%);
.dot {
background: #88D4A2;
opacity: 0.5;
}
}
.dot {
position: absolute;
top: calc(15% - 10px);
left: 5%;
width: 10px;
height: 10px;
border: 1px solid #979797;
border-radius: 50%;
&.active {
background: #88D4A2;
}
}
header {
margin-bottom: 10px;
display: flex;
flex-direction: row;
justify-content: space-between;
h1 {
margin-bottom: 10px;
}
span {
font-size: 10px;
text-decoration: underline;
transition: all 0.25s;
&:hover {
color: #88D4A2;
}
}
}
section {
margin: 10px 0;
h4 {
font-weight: bold;
text-transform: uppercase;
font-size: 10px;
margin-bottom: 5px;
}
}
.funds {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: 20px;
}
}

25
app/components/Network/PeersList.js

@ -1,25 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './PeersList.scss'
const PeersList = ({ peers, updateSelectedPeers, selectedPeerPubkeys }) => (
<ul className={styles.peers}>
{
peers.map(peer => (
<li key={peer.peer_id} className={styles.peer} onClick={() => updateSelectedPeers(peer)}>
<span className={`${styles.dot} ${selectedPeerPubkeys.includes(peer.pub_key) && styles.active}`} />
<h1>{peer.address}</h1>
<h4>{peer.pub_key}</h4>
</li>
))
}
</ul>
)
PeersList.propTypes = {
peers: PropTypes.array.isRequired,
updateSelectedPeers: PropTypes.func.isRequired,
selectedPeerPubkeys: PropTypes.array.isRequired
}
export default PeersList

46
app/components/Network/PeersList.scss

@ -1,46 +0,0 @@
@import '../../variables.scss';
.peers {
color: $white;
margin-top: 50px;
}
.peer {
position: relative;
margin: 20px 0;
padding: 10px 40px;
cursor: pointer;
transition: all 0.25s;
&:hover {
background: darken(#353535, 10%);
.dot {
background: #5589F3;
opacity: 0.5;
}
}
.dot {
position: absolute;
top: calc(50% - 10px);
left: 5%;
width: 10px;
height: 10px;
border: 1px solid #979797;
border-radius: 50%;
&.active {
background: #5589F3;
}
}
h1 {
font-size: 16px;
margin-bottom: 10px;
}
h4 {
font-size: 8px;
}
}

63
app/components/Network/TransactionForm.js

@ -1,63 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { btc } from 'utils'
import styles from './TransactionForm.scss'
const TransactionForm = ({
updatePayReq, pay_req, loadingRoutes, payReqRoutes, setCurrentRoute, currentRoute
}) => (
<div className={styles.transactionForm}>
<div className={styles.form}>
<input
className={styles.transactionInput}
placeholder='Payment request...'
value={pay_req}
onChange={event => updatePayReq(event.target.value)}
/>
</div>
{
loadingRoutes &&
<div className={styles.loading}>
<div className={styles.spinner} />
<h1>calculating all routes...</h1>
</div>
}
<ul className={styles.routes}>
{
payReqRoutes.map((route, index) => (
<li className={`${styles.route} ${currentRoute === route && styles.active}`} key={index} onClick={() => setCurrentRoute(route)}>
<header>
<h1>Route #{index + 1}</h1>
<span>Hops: {route.hops.length}</span>
</header>
<div className={styles.data}>
<section>
<h4>Amount</h4>
<span>{btc.satoshisToBtc(route.total_amt)} BTC</span>
</section>
<section>
<h4>Fees</h4>
<span>{btc.satoshisToBtc(route.total_fees)} BTC</span>
</section>
</div>
</li>
))
}
</ul>
</div>
)
TransactionForm.propTypes = {
updatePayReq: PropTypes.func.isRequired,
pay_req: PropTypes.string.isRequired,
loadingRoutes: PropTypes.bool.isRequired,
payReqRoutes: PropTypes.array.isRequired,
setCurrentRoute: PropTypes.func.isRequired,
currentRoute: PropTypes.object.isRequired
}
export default TransactionForm

125
app/components/Network/TransactionForm.scss

@ -1,125 +0,0 @@
@import '../../variables.scss';
@-webkit-keyframes animation-rotate {
100% {
-webkit-transform: rotate(360deg);
}
}
@-moz-keyframes animation-rotate {
100% {
-moz-transform: rotate(360deg);
}
}
@-o-keyframes animation-rotate {
100% {
-o-transform: rotate(360deg);
}
}
@keyframes animation-rotate {
100% {
transform: rotate(360deg);
}
}
.spinner {
border: 1px solid rgba(255, 220, 83, 0.1);
border-left-color: rgba(255, 220, 83, 0.4);
-webkit-border-radius: 999px;
-moz-border-radius: 999px;
border-radius: 999px;
}
.spinner {
margin: 0 auto;
height: 100px;
width: 100px;
-webkit-animation: animation-rotate 1000ms linear infinite;
-moz-animation: animation-rotate 1000ms linear infinite;
-o-animation: animation-rotate 1000ms linear infinite;
animation: animation-rotate 1000ms linear infinite;
}
.loading {
margin-top: 40px;
h1 {
text-align: center;
margin-top: 25px;
}
}
.transactionForm {
color: $white;
margin-top: 50px;
.form {
padding: 0 20px;
}
.transactionInput {
outline: 0;
border: 0;
border-bottom: 1px solid $secondary;
color: $secondary;
background: transparent;
padding: 5px;
width: 100%;
font-size: 14px;
color: $white;
}
}
.routes {
margin-top: 40px;
}
.route {
margin: 20px 0;
padding: 20px;
cursor: pointer;
&:hover {
background: darken(#353535, 10%);
}
&.active {
background: darken(#353535, 10%);
}
header {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 20px;
h1 {
font-size: 16px;
font-weight: bold;
}
span {
font-weight: bold;
text-transform: uppercase;
font-size: 12px;
letter-spacing: 1.2px;
}
}
.data {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
section {
h4 {
font-size: 12px;
font-weight: bold;
margin-bottom: 5px;
}
}
}
}

23
app/components/Onboarding/Alias.js

@ -0,0 +1,23 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './Alias.scss'
const Alias = ({ alias, updateAlias }) => (
<div className={styles.container}>
<input
type='text'
placeholder='Satoshi'
className={styles.alias}
ref={input => input && input.focus()}
value={alias}
onChange={event => updateAlias(event.target.value)}
/>
</div>
)
Alias.propTypes = {
alias: PropTypes.string.isRequired,
updateAlias: PropTypes.func.isRequired
}
export default Alias

15
app/components/Onboarding/Alias.scss

@ -0,0 +1,15 @@
@import '../../variables.scss';
.alias {
background: transparent;
outline: none;
border: 0;
color: $gold;
-webkit-text-fill-color: $white;
font-size: 22px;
}
.alias::-webkit-input-placeholder {
text-shadow: none;
-webkit-text-fill-color: initial;
}

55
app/components/Onboarding/FormContainer.js

@ -0,0 +1,55 @@
import React from 'react'
import PropTypes from 'prop-types'
import Isvg from 'react-inlinesvg'
import zapLogo from 'icons/zap_logo.svg'
import styles from './FormContainer.scss'
const FormContainer = ({ title, description, back, next, children }) => (
<div className={styles.container}>
<div className={styles.titleBar} />
<header className={styles.header}>
<section>
<Isvg src={zapLogo} />
</section>
<section />
</header>
<div className={styles.info}>
<h1>{title}</h1>
<p>{description}</p>
</div>
<div className={styles.content}>
{children}
</div>
<footer className={styles.footer}>
<div className={styles.buttonsContainer}>
<section>
{
back && <div onClick={back}>Back</div>
}
</section>
<section>
{
next && <div onClick={next}>Next</div>
}
</section>
</div>
</footer>
</div>
)
FormContainer.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
back: PropTypes.func,
next: PropTypes.func,
children: PropTypes.object.isRequired
}
export default FormContainer

67
app/components/Onboarding/FormContainer.scss

@ -0,0 +1,67 @@
@import '../../variables.scss';
.container {
position: relative;
height: 100vh;
background: $darkspaceblue;
}
.titleBar {
background: $spacegrey;
height: 20px;
-webkit-user-select: none;
-webkit-app-region: drag;
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 20px 40px;
}
.info {
color: $white;
margin: 20px 0 20px 0;
padding: 20px 40px;
h1 {
font-size: 22px;
margin-bottom: 10px;
}
p {
font-size: 12px;
}
}
.content {
position: relative;
background: $spaceblue;
height: 100vh;
padding: 60px 40px;
}
.footer {
position: absolute;
bottom: 0;
padding: 20px 40px;
color: $white;
width: calc(100% - 80px);
.buttonsContainer {
display: flex;
flex-direction: row;
justify-content: space-between;
div {
cursor: pointer;
transition: all 0.25s;
&:hover {
opacity: 0.5;
}
}
}
}

49
app/components/Onboarding/Onboarding.js

@ -0,0 +1,49 @@
import React from 'react'
import PropTypes from 'prop-types'
import LoadingBolt from 'components/LoadingBolt'
import FormContainer from './FormContainer'
import Alias from './Alias'
import styles from './Onboarding.scss'
const Onboarding = ({
onboarding: {
step,
alias
},
submit,
aliasProps
}) => {
const renderStep = () => {
switch (step) {
case 1:
return (
<FormContainer
title={'1. What should we call you?'}
description={'Set your nickname to help others connect with you on the Lightning Network'}
back={null}
next={() => submit(alias)}
>
<Alias {...aliasProps} />
</FormContainer>
)
default:
return <LoadingBolt />
}
}
return (
<div className={styles.container}>
{renderStep()}
</div>
)
}
Onboarding.propTypes = {
onboarding: PropTypes.object.isRequired,
aliasProps: PropTypes.object.isRequired,
submit: PropTypes.func.isRequired
}
export default Onboarding

0
app/components/Contacts/OnlineContact.scss → app/components/Onboarding/Onboarding.scss

45
app/components/Onboarding/Syncing.js

@ -0,0 +1,45 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
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()
}
render() {
const { syncPercentage } = this.props
return (
<div className={styles.container}>
<div className={styles.titleBar} />
<div className={styles.content}>
<header>
<Isvg className={styles.bitcoinLogo} src={zapLogo} />
</header>
<section className={styles.progressContainer}>
<h1>Syncing to the blockchain...</h1>
<div className={styles.progressBar}>
<div className={styles.progress} style={{ width: isNaN(syncPercentage) ? 0 : `${syncPercentage}%` }} />
</div>
<h4>{isNaN(parseInt(syncPercentage, 10)) || syncPercentage.toString().length === 0 ? '' : `${syncPercentage}%`}</h4>
</section>
</div>
</div>
)
}
}
Syncing.propTypes = {
fetchBlockHeight: PropTypes.func.isRequired,
syncPercentage: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]).isRequired
}
export default Syncing

50
app/components/Onboarding/Syncing.scss

@ -0,0 +1,50 @@
@import '../../variables.scss';
.container {
position: relative;
height: 100vh;
background: $spaceblue;
}
.titleBar {
background: $spacegrey;
height: 20px;
-webkit-user-select: none;
-webkit-app-region: drag;
}
.content {
padding: 20px 40px;
}
.progressContainer {
color: $white;
text-align: center;
margin-top: 20%;
h1 {
margin-bottom: 20px;
}
.progressBar {
width: 75%;
max-width: 700px;
margin: 0 auto;
height: 10px;
border-radius: 5px;
background: $spaceborder;
}
.progress {
background: $gold;
background: #DEA326;
height: 10px;
border-radius: 5px;
transition: all 0.25s;
}
h4 {
color: $gold;
margin-top: 10px;
}
}

3
app/components/Onboarding/index.js

@ -0,0 +1,3 @@
import Onboarding from './Onboarding'
export default Onboarding

2
app/components/Wallet/ReceiveModal.js

@ -17,7 +17,7 @@ const ReceiveModal = ({
},
content: {
top: 'auto',
left: '20%',
left: '0',
right: '0',
bottom: 'auto',
width: '40%',

50
app/components/Wallet/Wallet.js

@ -1,9 +1,11 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { FaQrcode } from 'react-icons/lib/fa'
import { FaAngleDown } from 'react-icons/lib/fa'
import Isvg from 'react-inlinesvg'
import { btc } from 'utils'
import skinnyBitcoinIcon from 'icons/skinny_bitcoin.svg'
import bitcoinIcon from 'icons/bitcoin.svg'
import zapLogo from 'icons/zap_logo.svg'
import qrCode from 'icons/qrcode.svg'
import ReceiveModal from './ReceiveModal'
import styles from './Wallet.scss'
@ -23,10 +25,14 @@ class Wallet extends Component {
balance,
address,
info,
newAddress
newAddress,
currentTicker,
openPayForm,
openRequestForm
} = this.props
const { modalOpen, qrCodeType } = this.state
const usdAmount = btc.satoshisToUsd((parseInt(balance.walletBalance, 10) + parseInt(balance.channelBalance, 10)), currentTicker.price_usd)
const changeQrCode = () => {
const qrCodeNum = this.state.qrCodeType === 1 ? 2 : 1
@ -51,22 +57,39 @@ class Wallet extends Component {
)
}
<div className={styles.content}>
<header className={styles.header}>
<section className={styles.logo}>
<Isvg className={styles.bitcoinLogo} src={zapLogo} />
</section>
<section className={styles.user}>
<div>
<span>{info.data.alias}</span>
<FaAngleDown />
</div>
</section>
</header>
<div className={styles.left}>
<div className={styles.leftContent}>
<Isvg className={styles.bitcoinLogo} src={skinnyBitcoinIcon} />
<Isvg className={styles.bitcoinLogo} src={bitcoinIcon} />
<div className={styles.details}>
<h1>{btc.satoshisToBtc(parseFloat(balance.walletBalance) + parseFloat(balance.channelBalance))} BTC</h1>
<span>{btc.satoshisToBtc(balance.walletBalance)} available</span>
<span>{btc.satoshisToBtc(balance.channelBalance)} in channels</span>
<h1>
<span>
{btc.satoshisToBtc(parseFloat(balance.walletBalance) + parseFloat(balance.channelBalance))}BTC
</span>
<span onClick={() => this.setState({ modalOpen: true })}>
<Isvg className={styles.bitcoinLogo} src={qrCode} />
</span>
</h1>
<span className={styles.usdValue}> ${usdAmount ? usdAmount.toLocaleString() : ''}</span>
</div>
</div>
</div>
<div className={styles.right}>
<div className={styles.rightContent}>
<div className='buttonPrimary' onClick={() => this.setState({ modalOpen: true })}>
<FaQrcode />
Address
</div>
<div className={styles.pay} onClick={openPayForm}>Pay</div>
<div className={styles.request} onClick={openRequestForm}>Request</div>
</div>
</div>
</div>
@ -79,7 +102,10 @@ Wallet.propTypes = {
balance: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
info: PropTypes.object.isRequired,
newAddress: PropTypes.func.isRequired
newAddress: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired,
openPayForm: PropTypes.func.isRequired,
openRequestForm: PropTypes.func.isRequired
}
export default Wallet

90
app/components/Wallet/Wallet.scss

@ -1,10 +1,31 @@
@import '../../variables.scss';
.wallet {
cursor: pointer;
background: $lightgrey;
background: $bluegrey;
color: $white;
transition: background 0.25s;
height: 150px;
padding: 20px 40px;
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
.logo span svg {
width: 64px;
height: 24px;
}
.user {
cursor: pointer;
transition: all 0.25s;
&:hover {
opacity: 0.5;
}
}
}
.left, .right {
@ -14,7 +35,7 @@
height: 150px;
.leftContent, .rightContent {
padding: 25px;
padding: 25px 0;
}
}
@ -23,8 +44,8 @@
flex-direction: row;
.bitcoinLogo svg {
width: 100px;
height: 100px;
width: 32px;
height: 32px;
}
.details {
@ -33,14 +54,37 @@
justify-content: center;
h1 {
font-size: 24px;
font-weight: bold;
margin-bottom: 10px;
letter-spacing: 1.5px;
display: flex;
flex-direction: row;
span:nth-child(1) {
font-size: 24px;
line-height: 32px;
font-weight: 500;
margin-left: 10px;
margin-bottom: 5px;
letter-spacing: 1.5px;
}
span:nth-child(2) svg {
color: $white;
width: 20px;
height: 32px;
opacity: 1;
margin-left: 5px;
cursor: pointer;
transition: all 0.25s;
&:hover {
opacity: 0.5;
}
}
}
span {
margin: 2.5px 0;
.usdValue {
font-size: 12px;
margin-left: 10px;
font-style: italic;
}
}
@ -55,21 +99,25 @@
flex-direction: row;
justify-content: flex-end;
align-items: right;
height: calc(100% - 50px);
div {
padding: 7px 20px;
background: $main;
transition: background 0.25s;
color: $black;
.pay, .request {
font-size: 16px;
font-weight: bold;
color: $white;
background: $spaceblue;
padding: 15px;
width: 100px;
text-align: center;
border-radius: 5px;
cursor: pointer;
transition: all 0.25s;
&:hover {
background: darken($main, 10%);
opacity: 0.5;
}
svg {
font-size: 35px;
margin-right: 10px;
&:nth-child(1) {
margin-right: 20px;
}
}
}

70
app/containers/Root.js

@ -5,42 +5,70 @@ import { ConnectedRouter } from 'react-router-redux'
import PropTypes from 'prop-types'
import LoadingBolt from '../components/LoadingBolt'
import LndSyncing from '../components/LndSyncing'
import Onboarding from '../components/Onboarding'
import Syncing from '../components/Onboarding/Syncing'
import { updateAlias, changeStep, submit } from '../reducers/onboarding'
import { fetchBlockHeight, lndSelectors } from '../reducers/lnd'
import { newAddress } from '../reducers/address'
import Routes from '../routes'
const mapDispatchToProps = {
fetchBlockHeight,
newAddress
updateAlias,
changeStep,
submit,
fetchBlockHeight
}
const mapStateToProps = state => ({
lnd: state.lnd,
address: state.address,
onboarding: state.onboarding,
syncPercentage: lndSelectors.syncPercentage(state)
})
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const syncingProps = {
fetchBlockHeight: dispatchProps.fetchBlockHeight,
syncPercentage: stateProps.syncPercentage
}
const aliasProps = {
updateAlias: dispatchProps.updateAlias,
alias: stateProps.onboarding.alias
}
const onboardingProps = {
onboarding: stateProps.onboarding,
submit: dispatchProps.submit,
aliasProps
}
return {
...stateProps,
...dispatchProps,
...ownProps,
onboardingProps,
syncingProps
}
}
const Root = ({
store,
history,
lnd,
newAddress, // eslint-disable-line no-shadow
fetchBlockHeight, // eslint-disable-line no-shadow
syncPercentage,
address
onboardingProps,
syncingProps
}) => {
// If we are syncing show the syncing screen
if (!onboardingProps.onboarding.onboarded) {
return <Onboarding {...onboardingProps} />
}
// If we are syncing show the syncing screen
if (lnd.syncing) {
return (
<LndSyncing
newAddress={newAddress}
fetchBlockHeight={fetchBlockHeight}
syncPercentage={syncPercentage}
address={address}
/>
)
return <Syncing {...syncingProps} />
}
// Don't launch the app without gRPC connection
@ -59,10 +87,8 @@ Root.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
lnd: PropTypes.object.isRequired,
fetchBlockHeight: PropTypes.func.isRequired,
newAddress: PropTypes.func.isRequired,
syncPercentage: PropTypes.number.isRequired,
address: PropTypes.object.isRequired
onboardingProps: PropTypes.object.isRequired,
syncingProps: PropTypes.object.isRequired
}
export default connect(mapStateToProps, mapDispatchToProps)(Root)
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Root)

16
app/icons/bitcoin.svg

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32px" height="33px" viewBox="0 0 32 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>Bitcoin</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Pay-Hover-Copy" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-56.000000, -92.000000)">
<g id="Group" transform="translate(56.000000, 90.000000)" fill="#F5B300">
<g id="Bitcoin" transform="translate(0.000000, 2.000000)">
<g id="btc">
<path d="M15.85,32.1 C7.09628672,32.1 0,25.0037133 0,16.25 C0,7.49628672 7.09628672,0.4 15.85,0.4 C24.6037133,0.4 31.7,7.49628672 31.7,16.25 C31.7,25.0037133 24.6037133,32.1 15.85,32.1 Z M11.55,8.15 C11.5,8.2 11.5,8.2 11.7,8.05 C11.55,8.6 11.4,9.2 11.25,9.75 C11.2,9.85 11.25,9.9 11.35,9.9 C11.75,10 12.2,10.1 12.65,10.25 C13.1,10.4 13.35,10.85 13.25,11.25 C12.65,13.85 12,16.4 11.35,18.95 C11.3,19.3 11,19.5 10.6,19.4 C10.25,19.35 9.85,19.25 9.5,19.15 C9.35,19.15 9.3,19.15 9.25,19.3 C9.1,19.7 8.9,20.15 8.7,20.6 C8.6,20.8 8.55,20.95 8.45,21.15 C9.65,21.45 10.8,21.75 11.95,22.05 C11.95,22.15 11.9,22.2 11.9,22.25 C11.65,23.05 11.45,23.9 11.25,24.75 C11.2,24.85 11.2,24.9 11.35,24.95 C11.85,25.05 12.35,25.15 12.85,25.3 C13,25.35 13,25.3 13.05,25.2 C13.15,24.75 13.3,24.3 13.4,23.85 C13.5,23.4 13.65,22.95 13.75,22.5 C14.2,22.6 14.6,22.7 15,22.8 C15.1,22.85 15.05,22.9 15.05,23 C14.8,23.8 14.6,24.65 14.4,25.5 C14.35,25.6 14.35,25.65 14.5,25.7 C14.95,25.8 15.45,25.95 15.9,26.05 C16.15,26.15 16.15,26.1 16.2,25.9 C16.4,25.1 16.6,24.25 16.8,23.45 C16.85,23.3 16.95,23.25 17.1,23.3 C18.1,23.45 19.1,23.55 20.15,23.4 C20.95,23.3 21.65,22.95 22.2,22.3 C22.65,21.75 22.9,21.15 23.1,20.45 C23.25,19.9 23.3,19.3 23.2,18.75 C23,18.05 22.6,17.5 22,17.05 C21.8,16.9 21.6,16.8 21.4,16.65 C21.5,16.65 21.55,16.6 21.6,16.6 C22,16.45 22.4,16.25 22.7,15.95 C23.1,15.55 23.3,15.05 23.45,14.5 C23.65,13.7 23.6,12.9 23.15,12.15 C22.8,11.65 22.35,11.25 21.85,10.95 C21.3,10.7 20.75,10.45 20.2,10.2 C20.05,10.15 20,10.1 20.05,9.95 C20.15,9.7 20.2,9.45 20.25,9.2 C20.4,8.6 20.55,8 20.7,7.35 C20.65,7.35 20.65,7.3 20.6,7.3 C20.05,7.15 19.55,7.05 19.05,6.9 C18.95,6.85 18.9,6.9 18.9,7 C18.85,7.2 18.8,7.4 18.75,7.6 C18.55,8.25 18.4,8.85 18.25,9.5 C18.2,9.55 18.15,9.65 18.1,9.65 C17.7,9.55 17.25,9.45 16.85,9.35 C16.9,9.25 16.9,9.2 16.9,9.15 C17.1,8.35 17.3,7.5 17.5,6.7 C17.55,6.6 17.55,6.55 17.4,6.5 C16.9,6.4 16.45,6.3 15.95,6.15 C15.8,6.1 15.75,6.15 15.7,6.3 C15.5,7.15 15.3,7.95 15.1,8.8 C15.05,8.95 15,9 14.85,8.95 C14.3,8.8 13.75,8.7 13.2,8.55 C12.65,8.4 12.1,8.3 11.55,8.15 Z M14.1,20.5 C14.3,19.75 14.45,19.05 14.65,18.35 C14.75,17.85 14.9,17.4 15,16.9 C15.05,16.75 15.05,16.7 15.2,16.75 C16.2,16.95 17.25,17.15 18.15,17.65 C18.55,17.9 18.95,18.15 19.2,18.55 C19.75,19.35 19.35,20.6 18.4,20.9 C17.85,21.1 17.3,21.1 16.7,21.05 C15.85,21 15,20.75 14.2,20.55 C14.2,20.55 14.15,20.5 14.1,20.5 Z M18.3,15.4 C17.5,15.4 16.85,15.25 16.1,15.15 C15.9,15.1 15.75,15.05 15.55,15 C15.45,14.95 15.4,14.9 15.45,14.8 C15.75,13.75 16,12.75 16.25,11.7 C16.3,11.55 16.35,11.5 16.5,11.55 C17.25,11.75 17.95,11.9 18.65,12.2 C19.1,12.4 19.55,12.7 19.8,13.15 C20.3,14 19.8,15.1 18.85,15.3 C18.65,15.35 18.4,15.4 18.3,15.4 Z" id="Combined-Shape" fill-rule="nonzero"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

1
app/icons/check_circle.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check-circle"><path d="M22 11.07V12a10 10 0 1 1-5.93-9.14"></path><polyline points="23 3 12 14 9 11"></polyline></svg>

After

Width:  |  Height:  |  Size: 322 B

1
app/icons/clock.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-clock"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 15 15"></polyline></svg>

After

Width:  |  Height:  |  Size: 304 B

17
app/icons/contacts.svg

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="15px" viewBox="0 0 20 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>Group 7</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Swap-4" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-38.000000, -227.000000)" stroke-linecap="round" stroke-linejoin="round">
<g id="Group" transform="translate(-2.000000, 0.000000)" stroke="#FFFFFF" stroke-width="0.8">
<g id="Group-7" transform="translate(41.000000, 228.000000)">
<path d="M13.5,14 L13.5,12.5416667 C13.5,10.9308361 11.988961,9.625 10.125,9.625 L3.375,9.625 C1.51103897,9.625 1.87350135e-16,10.9308361 0,12.5416667 L0,14" id="Shape"></path>
<ellipse id="Oval" cx="6.75" cy="3.0625" rx="3.15" ry="3.0625"></ellipse>
<path d="M18,14 L18,12.5093697 C17.9987763,11.1507479 16.8884989,9.96465516 15.3,9.625" id="Shape"></path>
<path d="M12.6,0 C14.1887696,0.358150316 15.3,1.61857365 15.3,3.0625 C15.3,4.50642635 14.1887696,5.76684968 12.6,6.125" id="Shape"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
app/icons/help.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-life-buoy"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="4"></circle><line x1="4.93" y1="4.93" x2="9.17" y2="9.17"></line><line x1="14.83" y1="14.83" x2="19.07" y2="19.07"></line><line x1="14.83" y1="9.17" x2="19.07" y2="4.93"></line><line x1="14.83" y1="9.17" x2="18.36" y2="5.64"></line><line x1="4.93" y1="19.07" x2="9.17" y2="14.83"></line></svg>

After

Width:  |  Height:  |  Size: 575 B

18
app/icons/help_2.svg

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>life-buoy</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Main-Page-Copy-15" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-39.000000, -377.000000)" stroke-linecap="round" stroke-linejoin="round">
<g id="life-buoy" transform="translate(40.000000, 378.000000)" stroke="#FFFFFF" stroke-width="0.8">
<circle id="Oval" cx="8" cy="8" r="8"></circle>
<circle id="Oval" cx="8" cy="8" r="3.2"></circle>
<path d="M2.4,2.4 L5.792,5.792" id="Shape"></path>
<path d="M10.4,10.4 L13.792,13.792" id="Shape"></path>
<path d="M10.4,5.792 L13.792,2.4" id="Shape"></path>
<path d="M10.4,6.024 L13.224,3.2" id="Shape"></path>
<path d="M2.4,13.792 L5.792,10.4" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

14
app/icons/network.svg

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>Group 2</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Main-Page-Copy-15" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-39.000000, -275.000000)" stroke-linecap="round" stroke-linejoin="round">
<g id="Group-2" transform="translate(40.000000, 276.000000)" stroke="#FFFFFF" stroke-width="0.8">
<circle id="Oval" cx="8" cy="8" r="8"></circle>
<path d="M0,8 L16,8" id="Shape"></path>
<path d="M8,1.77635684e-16 C9.87596028,2.19068232 10.9420646,5.03362708 11,8 C10.9420646,10.9663729 9.87596028,13.8093177 8,16 C6.12403972,13.8093177 5.05793543,10.9663729 5,8 C5.05793543,5.03362708 6.12403972,2.19068232 8,0 Z" id="Shape"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

27
app/icons/qrcode.svg

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>Group 23</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="v1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-276.000000, -96.000000)" opacity="0.5">
<g id="Group-23" transform="translate(276.000000, 96.000000)" fill="#FFFFFF" fill-rule="nonzero">
<g id="Group-22">
<g id="Group">
<path d="M0.4,4.80896 C0.17904,4.80896 0,4.62992 0,4.40896 L0,0.4 C0,0.17904 0.17904,0 0.4,0 L4.40896,0 C4.62992,0 4.80896,0.17904 4.80896,0.4 C4.80896,0.62096 4.62992,0.8 4.40896,0.8 L0.8,0.8 L0.8,4.40896 C0.8,4.62992 0.62096,4.80896 0.4,4.80896 Z" id="Shape"></path>
<path d="M15.6,16 L11.59088,16 C11.36992,16 11.19088,15.82096 11.19088,15.6 C11.19088,15.37904 11.36992,15.2 11.59088,15.2 L15.2,15.2 L15.2,11.59088 C15.2,11.36992 15.37904,11.19088 15.6,11.19088 C15.82096,11.19088 16,11.36992 16,11.59088 L16,15.6 C16,15.82096 15.82096,16 15.6,16 Z" id="Shape"></path>
</g>
<g id="Group">
<path d="M15.6,4.80896 C15.37904,4.80896 15.2,4.62992 15.2,4.40896 L15.2,0.8 L11.59088,0.8 C11.36992,0.8 11.19088,0.62096 11.19088,0.4 C11.19088,0.17904 11.36992,0 11.59088,0 L15.6,0 C15.82096,0 16,0.17904 16,0.4 L16,4.40896 C16,4.62992 15.82096,4.80896 15.6,4.80896 Z" id="Shape"></path>
<path d="M4.40896,16 L0.4,16 C0.17904,16 0,15.82096 0,15.6 L0,11.59088 C0,11.36992 0.17904,11.19088 0.4,11.19088 C0.62096,11.19088 0.8,11.36992 0.8,11.59088 L0.8,15.2 L4.40896,15.2 C4.62992,15.2 4.80896,15.37904 4.80896,15.6 C4.80896,15.82096 4.62992,16 4.40896,16 Z" id="Shape"></path>
</g>
<path d="M11.5875,11.9803571 L7.6,11.9803571 C7.38298571,11.9803571 7.20714286,11.8045143 7.20714286,11.5875 L7.20714286,7.99285714 L3.6125,7.99285714 C3.39548571,7.99285714 3.21964286,7.81701429 3.21964286,7.6 C3.21964286,7.38298571 3.39548571,7.20714286 3.6125,7.20714286 L7.6,7.20714286 C7.81701429,7.20714286 7.99285714,7.38298571 7.99285714,7.6 L7.99285714,11.1946429 L11.5875,11.1946429 C11.8045143,11.1946429 11.9803571,11.3704857 11.9803571,11.5875 C11.9803571,11.8045143 11.8045143,11.9803571 11.5875,11.9803571 Z" id="Shape"></path>
<path d="M11.5875,9.87857143 C11.3704857,9.87857143 11.1946429,9.70272857 11.1946429,9.48571429 L11.1946429,7.99285714 L9.87857143,7.99285714 L9.87857143,9.48571429 C9.87857143,9.70272857 9.70272857,9.87857143 9.48571429,9.87857143 C9.2687,9.87857143 9.09285714,9.70272857 9.09285714,9.48571429 L9.09285714,7.6 C9.09285714,7.38298571 9.2687,7.20714286 9.48571429,7.20714286 L11.5875,7.20714286 C11.8045143,7.20714286 11.9803571,7.38298571 11.9803571,7.6 L11.9803571,9.48571429 C11.9803571,9.70272857 11.8045143,9.87857143 11.5875,9.87857143 Z" id="Shape"></path>
<path d="M5.71428571,6.10714286 L3.6125,6.10714286 C3.39548571,6.10714286 3.21964286,5.9313 3.21964286,5.71428571 L3.21964286,3.6125 C3.21964286,3.39548571 3.39548571,3.21964286 3.6125,3.21964286 L5.71428571,3.21964286 C5.9313,3.21964286 6.10714286,3.39548571 6.10714286,3.6125 L6.10714286,5.71428571 C6.10714286,5.9313 5.9313,6.10714286 5.71428571,6.10714286 Z M4.00535714,5.32142857 L5.32142857,5.32142857 L5.32142857,4.00535714 L4.00535714,4.00535714 L4.00535714,5.32142857 Z" id="Shape"></path>
<path d="M11.5875,6.10714286 L9.48571429,6.10714286 C9.2687,6.10714286 9.09285714,5.9313 9.09285714,5.71428571 L9.09285714,3.6125 C9.09285714,3.39548571 9.2687,3.21964286 9.48571429,3.21964286 L11.5875,3.21964286 C11.8045143,3.21964286 11.9803571,3.39548571 11.9803571,3.6125 L11.9803571,5.71428571 C11.9803571,5.9313 11.8045143,6.10714286 11.5875,6.10714286 Z M9.87857143,5.32142857 L11.1946429,5.32142857 L11.1946429,4.00535714 L9.87857143,4.00535714 L9.87857143,5.32142857 Z" id="Shape"></path>
<path d="M5.71428571,11.9803571 L3.6125,11.9803571 C3.39548571,11.9803571 3.21964286,11.8045143 3.21964286,11.5875 L3.21964286,9.48571429 C3.21964286,9.2687 3.39548571,9.09285714 3.6125,9.09285714 L5.71428571,9.09285714 C5.9313,9.09285714 6.10714286,9.2687 6.10714286,9.48571429 L6.10714286,11.5875 C6.10714286,11.8045143 5.9313,11.9803571 5.71428571,11.9803571 Z M4.00535714,11.1946429 L5.32142857,11.1946429 L5.32142857,9.87857143 L4.00535714,9.87857143 L4.00535714,11.1946429 Z" id="Shape"></path>
<path d="M7.6,6.10714286 C7.38298571,6.10714286 7.20714286,5.9313 7.20714286,5.71428571 L7.20714286,3.6125 C7.20714286,3.39548571 7.38298571,3.21964286 7.6,3.21964286 C7.81701429,3.21964286 7.99285714,3.39548571 7.99285714,3.6125 L7.99285714,5.71428571 C7.99285714,5.9313 7.81701429,6.10714286 7.6,6.10714286 Z" id="Shape"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

1
app/icons/search.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"><circle cx="10.5" cy="10.5" r="7.5"></circle><line x1="21" y1="21" x2="15.8" y2="15.8"></line></svg>

After

Width:  |  Height:  |  Size: 312 B

15
app/icons/wallet_2.svg

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="16px" viewBox="0 0 18 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>Group 5</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Main-Page-Copy-15" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-39.000000, -175.000000)">
<g id="Group-5" transform="translate(39.000000, 175.000000)" fill="#FFFFFF">
<path d="M16.488,15.8758927 L1.512,15.8758927 C0.67824,15.8758927 0,15.2205883 0,14.4150231 L0,3.3868492 C0,3.2414579 0.09288,3.11693616 0.22392,3.06476224 C0.17496,2.99311007 0.13176,2.91380572 0.0972,2.82615355 C0.02592,2.64667529 0.11808,2.44493616 0.30384,2.37606659 C0.4896,2.30858833 0.69768,2.39624051 0.76896,2.57571877 C0.95328,3.03902311 1.7352,3.04458833 2.07576,3.03902311 L16.488,3.03902311 C17.32176,3.03902311 18,3.69432746 18,4.49989268 L18,14.4150231 C18,15.2205883 17.32176,15.8758927 16.488,15.8758927 L16.488,15.8758927 Z M0.72,3.49050138 L0.72,14.4150231 C0.72,14.8365883 1.07568,15.1802405 1.512,15.1802405 L16.488,15.1802405 C16.92504,15.1802405 17.28,14.8365883 17.28,14.4150231 L17.28,4.49989268 C17.28,4.07832746 16.92504,3.73467529 16.488,3.73467529 L2.08872,3.73467529 C1.79928,3.74858833 1.20168,3.73537094 0.72,3.49050138 L0.72,3.49050138 Z" id="Fill-305"></path>
<path d="M16.06896,3.69015355 C15.87024,3.69015355 15.70896,3.53502311 15.70896,3.34232746 L15.70896,1.27206659 C15.70896,1.0814579 15.63768,0.918675288 15.50736,0.815718766 C15.37704,0.712066592 15.1992,0.674501375 15.00696,0.71137094 L1.54728,1.97119703 C0.77112,2.13815355 0.72,2.75450138 0.71928,2.76076224 L0.72,3.28250138 C0.72,3.47450138 0.55872,3.63032746 0.36,3.63032746 C0.16128,3.63032746 0,3.47450138 0,3.28250138 L0,2.73780572 C0.02808,2.32041442 0.36216,1.51276224 1.43496,1.28528398 L14.90328,0.0240665925 C15.2712,-0.0482812336 15.6708,0.0442405055 15.96528,0.277979636 C16.25976,0.513110071 16.42896,0.875544853 16.42896,1.27206659 L16.42896,3.34232746 C16.42896,3.53502311 16.2684,3.69015355 16.06896,3.69015355" id="Fill-306"></path>
<path d="M14.33952,10.4324144 C13.82328,10.4324144 13.40352,10.0261535 13.40352,9.52737094 C13.40352,9.02928398 13.82328,8.62371877 14.33952,8.62371877 C14.85576,8.62371877 15.27552,9.02928398 15.27552,9.52737094 C15.27552,10.0261535 14.85576,10.4324144 14.33952,10.4324144 L14.33952,10.4324144 Z M14.33952,9.31937094 C14.22,9.31937094 14.12352,9.41258833 14.12352,9.52737094 C14.12352,9.6428492 14.22,9.73676224 14.33952,9.73676224 C14.45832,9.73676224 14.55552,9.6428492 14.55552,9.52737094 C14.55552,9.41258833 14.45832,9.31937094 14.33952,9.31937094 L14.33952,9.31937094 Z" id="Fill-307"></path>
<path d="M17.64,12.243197 L12.48984,12.243197 C11.4912,12.243197 10.67904,11.4585014 10.67904,10.4936318 L10.67904,8.56250138 C10.67904,7.59763181 11.4912,6.81224051 12.48984,6.81224051 L17.64,6.81224051 C17.83872,6.81224051 18,6.96806659 18,7.16006659 L18,11.8953709 C18,12.0880666 17.83872,12.243197 17.64,12.243197 L17.64,12.243197 Z M12.48984,7.50789268 C11.88864,7.50789268 11.39904,7.98093616 11.39904,8.56250138 L11.39904,10.4936318 C11.39904,11.075197 11.88864,11.5475449 12.48984,11.5475449 L17.28,11.5475449 L17.28,7.50789268 L12.48984,7.50789268 L12.48984,7.50789268 Z" id="Fill-308"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
app/icons/zap.icns

Binary file not shown.

16
app/icons/zap_logo.svg

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="60px" height="21px" viewBox="0 0 60 21" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>Group 5</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Pay-Hover-Copy" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-56.000000, -23.000000)">
<g id="Group-5" transform="translate(56.000000, 20.000000)">
<path d="M29.5332033,18.0136719 L36.3984377,18.0136719 L36.3984377,19 L28.0878908,19 L28.0878908,18.140625 L34.4941408,9.43945312 L28.2832033,9.43945312 L28.2832033,8.43359375 L35.9687502,8.43359375 L35.9687502,9.30273438 L29.5332033,18.0136719 Z M46.1582032,19 C46.0410151,18.6679671 45.9661461,18.1764355 45.9335938,17.5253906 C45.5234355,18.0592475 45.0009798,18.4710272 44.366211,18.7607422 C43.7314422,19.0504572 43.0592484,19.1953125 42.3496094,19.1953125 C41.3339794,19.1953125 40.5120475,18.9121122 39.8837891,18.3457031 C39.2555308,17.779294 38.9414063,17.0631554 38.9414063,16.1972656 C38.9414063,15.1686146 39.3694619,14.3548207 40.225586,13.7558594 C41.0817101,13.156898 42.274732,12.8574219 43.8046876,12.8574219 L45.9238282,12.8574219 L45.9238282,11.65625 C45.9238282,10.9010379 45.6910831,10.3069683 45.225586,9.87402344 C44.7600889,9.44107856 44.0813847,9.22460938 43.1894532,9.22460938 C42.375647,9.22460938 41.7018256,9.43294063 41.1679688,9.84960938 C40.634112,10.2662781 40.3671876,10.7675752 40.3671876,11.3535156 L39.1953126,11.34375 C39.1953126,10.5039021 39.5859337,9.77637026 40.3671876,9.16113281 C41.1484415,8.54589536 42.1087183,8.23828125 43.2480469,8.23828125 C44.4264382,8.23828125 45.3557909,8.53287466 46.0361329,9.12207031 C46.7164748,9.71126597 47.0664062,10.5331979 47.0859376,11.5878906 L47.0859376,16.5878906 C47.0859376,17.6100312 47.1933584,18.3749975 47.4082032,18.8828125 L47.4082032,19 L46.1582032,19 Z M42.4863282,18.1601562 C43.2675821,18.1601562 43.9658173,17.9713561 44.5810547,17.59375 C45.1962922,17.2161439 45.6438789,16.7115917 45.9238282,16.0800781 L45.9238282,13.7558594 L43.8339844,13.7558594 C42.668614,13.7688803 41.7571648,13.9820943 41.0996094,14.3955078 C40.4420541,14.8089213 40.1132813,15.3769495 40.1132813,16.0996094 C40.1132813,16.6920603 40.3330057,17.1835918 40.772461,17.5742188 C41.2119163,17.9648457 41.7831997,18.1601562 42.4863282,18.1601562 Z M59.6972656,13.8242188 C59.6972656,15.4713624 59.3326859,16.7783155 58.6035156,17.7451172 C57.8743453,18.7119189 56.8977925,19.1953125 55.6738281,19.1953125 C54.2285083,19.1953125 53.1217486,18.6875051 52.3535156,17.671875 L52.3535156,23.0625 L51.1914062,23.0625 L51.1914062,8.43359375 L52.2753906,8.43359375 L52.3339843,9.92773438 C53.0957069,8.80142666 54.1992115,8.23828125 55.6445312,8.23828125 C56.9075583,8.23828125 57.8987594,8.71679209 58.618164,9.67382812 C59.3375686,10.6308642 59.6972656,11.9589759 59.6972656,13.6582031 L59.6972656,13.8242188 Z M58.5253906,13.6191406 C58.5253906,12.2714776 58.2487006,11.2070352 57.6953124,10.4257812 C57.1419243,9.64452734 56.3704476,9.25390625 55.3808593,9.25390625 C54.6647099,9.25390625 54.0494817,9.42643057 53.5351562,9.77148438 C53.0208307,10.1165382 52.6269544,10.6178353 52.3535156,11.2753906 L52.3535156,16.34375 C52.6334649,16.9492218 53.0338515,17.4114567 53.5546874,17.7304688 C54.0755234,18.0494808 54.6907516,18.2089844 55.4003906,18.2089844 C56.3834684,18.2089844 57.1500623,17.8167357 57.7001953,17.0322266 C58.2503282,16.2477174 58.5253906,15.1100335 58.5253906,13.6191406 Z" id="zap" fill="#FFFFFF"></path>
<g id="cloud-lightning" transform="translate(0.000000, 4.000000)" stroke-linecap="round" stroke-linejoin="round" stroke-width="0.77">
<path d="M15.0416667,12.2863636 C17.0346026,11.8913673 18.396646,10.0866143 18.1922712,8.11170759 C17.9878963,6.13680088 16.283606,4.63436876 14.25,4.63636364 L13.2525,4.63636364 C12.5303096,1.90723362 10.0081977,0.000523827862 7.12041491,0.000523855726 C4.23263213,0.000523883591 1.71052026,1.90723373 0.988329904,4.63636375 C0.26613955,7.36549378 1.52676812,10.2258816 4.05333333,11.5909091" id="Shape" stroke="#FFFFFF"></path>
<polyline id="Shape" stroke="#F5B300" points="10.2916667 7.72727273 7.125 12.3636364 11.875 12.3636364 8.70833333 17"></polyline>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

204
app/lnd/config/rpc.proto

@ -1,6 +1,6 @@
syntax = "proto3";
// import "google/api/annotations.proto";
import "google/api/annotations.proto";
package lnrpc;
/**
@ -25,9 +25,46 @@ package lnrpc;
* https://github.com/MaxFangX/lightning-api
*/
// The WalletUnlocker service is used to set up a wallet password for
// lnd at first startup, and unlock a previously set up wallet.
service WalletUnlocker {
/** lncli: `create`
CreateWallet is used at lnd startup to set the encryption password for
the wallet database.
*/
rpc CreateWallet(CreateWalletRequest) returns (CreateWalletResponse) {
option (google.api.http) = {
post: "/v1/createwallet"
body: "*"
};
}
/** lncli: `unlock`
UnlockWallet is used at startup of lnd to provide a password to unlock
the wallet database.
*/
rpc UnlockWallet(UnlockWalletRequest) returns (UnlockWalletResponse) {
option (google.api.http) = {
post: "/v1/unlockwallet"
body: "*"
};
}
}
message CreateWalletRequest {
bytes password = 1;
}
message CreateWalletResponse {}
message UnlockWalletRequest {
bytes password = 1;
}
message UnlockWalletResponse {}
service Lightning {
/** lncli: `walletbalance`
WalletBalance returns the sum of all confirmed unspent outputs under control
WalletBalance returns total unspent outputs(confirmed and unconfirmed), all confirmed unspent outputs and all unconfirmed unspent outputs under control
by the wallet. This method can be modified by having the request specify
only witness outputs should be factored into the final output sum.
*/
@ -59,7 +96,10 @@ service Lightning {
/** lncli: `sendcoins`
SendCoins executes a request to send coins to a particular address. Unlike
SendMany, this RPC call only allows creating a single output at a time.
SendMany, this RPC call only allows creating a single output at a time. If
neither target_conf, or sat_per_byte are set, then the internal wallet will
consult its fee model to determine a fee for the default confirmation
target.
*/
rpc SendCoins (SendCoinsRequest) returns (SendCoinsResponse) {
option (google.api.http) = {
@ -77,7 +117,9 @@ service Lightning {
/** lncli: `sendmany`
SendMany handles a request for a transaction that creates multiple specified
outputs in parallel.
outputs in parallel. If neither target_conf, or sat_per_byte are set, then
the internal wallet will consult its fee model to determine a fee for the
default confirmation target.
*/
rpc SendMany (SendManyRequest) returns (SendManyResponse);
@ -160,7 +202,7 @@ service Lightning {
workflow and is waiting for confirmations for the funding txn, or is in the
process of closure, either initiated cooperatively or non-cooperatively.
*/
rpc PendingChannels (PendingChannelRequest) returns (PendingChannelResponse) {
rpc PendingChannels (PendingChannelsRequest) returns (PendingChannelsResponse) {
option (google.api.http) = {
get: "/v1/channels/pending"
};
@ -191,7 +233,10 @@ service Lightning {
/** lncli: `openchannel`
OpenChannel attempts to open a singly funded channel specified in the
request to a remote peer.
request to a remote peer. Users are able to specify a target number of
blocks that the funding transaction should be confirmed in, or a manual fee
rate to us for the funding transaction. If neither are specified, then a
lax block confirmation target is used.
*/
rpc OpenChannel (OpenChannelRequest) returns (stream OpenStatusUpdate);
@ -199,7 +244,10 @@ service Lightning {
CloseChannel attempts to close an active channel identified by its channel
outpoint (ChannelPoint). The actions of this method can additionally be
augmented to attempt a force close after a timeout period in the case of an
inactive peer.
inactive peer. If a non-force close (cooperative closure) is requested,
then the user can specify either a target number of blocks until the
closure transaction is confirmed, or a manual fee rate. If neither are
specified, then a default lax, block confirmation target is used.
*/
rpc CloseChannel (CloseChannelRequest) returns (stream CloseStatusUpdate) {
option (google.api.http) = {
@ -375,11 +423,6 @@ service Lightning {
*/
rpc SubscribeChannelGraph(GraphTopologySubscription) returns (stream GraphTopologyUpdate);
/**
SetAlias sets the alias for this node; e.g. "alice"
*/
rpc SetAlias(SetAliasRequest) returns (SetAliasResponse);
/** lncli: `debuglevel`
DebugLevel allows a caller to programmatically set the logging verbosity of
lnd. The logging can be targeted according to a coarse daemon-wide logging
@ -398,11 +441,11 @@ service Lightning {
};
}
/** lncli: `updatefees`
UpdateFees allows the caller to update the fee schedule for all channels
globally, or a particular channel.
/** lncli: `updatechanpolicy`
UpdateChannelPolicy allows the caller to update the fee schedule and
channel policies for all channels globally, or a particular channel.
*/
rpc UpdateFees(FeeUpdateRequest) returns (FeeUpdateResponse) {
rpc UpdateChannelPolicy(PolicyUpdateRequest) returns (PolicyUpdateResponse) {
option (google.api.http) = {
post: "/v1/fees"
body: "*"
@ -431,6 +474,9 @@ message Transaction {
/// Fees paid for this transaction
int64 total_fees = 7 [ json_name = "total_fees" ];
/// Addresses that received funds for this transaction
repeated string dest_addresses = 8 [ json_name = "dest_addresses" ];
}
message GetTransactionsRequest {
}
@ -461,6 +507,9 @@ message SendRequest {
payment to the recipient.
*/
string payment_request = 6;
/// The CLTV delta from the current height that should be used to set the timelock for the final hop.
int32 final_cltv_delta = 7;
}
message SendResponse {
string payment_error = 1 [json_name = "payment_error"];
@ -492,6 +541,12 @@ message LightningAddress {
message SendManyRequest {
/// The map from addresses to amounts
map<string, int64> AddrToAmount = 1;
/// The target number of blocks that this transaction should be confirmed by.
int32 target_conf = 3;
/// A manual fee rate set in sat/byte that should be used when crafting the transaction.
int64 sat_per_byte = 5;
}
message SendManyResponse {
/// The id of the transaction
@ -504,6 +559,12 @@ message SendCoinsRequest {
/// The amount in satoshis to send
int64 amount = 2;
/// The target number of blocks that this transaction should be confirmed by.
int32 target_conf = 3;
/// A manual fee rate set in sat/byte that should be used when crafting the transaction.
int64 sat_per_byte = 5;
}
message SendCoinsResponse {
/// The transaction ID of the transaction
@ -657,6 +718,13 @@ message ActiveChannel {
The list of active, uncleared HTLCs currently pending within the channel.
*/
repeated HTLC pending_htlcs = 15 [json_name = "pending_htlcs"];
/**
The CSV delay expressed in relative blocks. If the channel is force
closed, we'll need to wait for this many blocks before we can regain our
funds.
*/
uint32 csv_delay = 16 [ json_name = "csv_delay" ];
}
message ListChannelsRequest {
@ -728,13 +796,16 @@ message GetInfoResponse {
string block_hash = 8 [json_name = "block_hash"];
/// Whether the wallet's view is synced to the main chain
bool synced_to_chain = 9 [ json_name = "synced_to_chain" ];
bool synced_to_chain = 9 [json_name = "synced_to_chain"];
/// Whether the current node is connected to testnet
bool testnet = 10 [ json_name = "testnet" ];
bool testnet = 10 [json_name = "testnet"];
/// A list of active chains the node is connected to
repeated string chains = 11 [ json_name = "chains" ];
repeated string chains = 11 [json_name = "chains"];
/// The URIs of the current node.
repeated string uris = 12 [json_name = "uris"];
}
message ConfirmationUpdate {
@ -764,6 +835,12 @@ message CloseChannelRequest {
/// If true, then the channel will be closed forcibly. This means the current commitment transaction will be signed and broadcast.
bool force = 2;
/// The target number of blocks that the closure transaction should be confirmed by.
int32 target_conf = 3;
/// A manual fee rate set in sat/byte that should be used when crafting the closure transaction.
int64 sat_per_byte = 5;
}
message CloseStatusUpdate {
oneof update {
@ -794,6 +871,18 @@ message OpenChannelRequest {
/// The number of satoshis to push to the remote side as part of the initial commitment state
int64 push_sat = 5 [json_name = "push_sat"];
/// The target number of blocks that the closure transaction should be confirmed by.
int32 target_conf = 6;
/// A manual fee rate set in sat/byte that should be used when crafting the closure transaction.
int64 sat_per_byte = 7;
/// Whether this channel should be private, not announced to the greater network.
bool private = 8 [json_name = "private"];
/// The minimum value in millisatoshi we will require for incoming HTLCs on the channel.
int64 min_htlc_msat = 9 [json_name = "min_htlc_msat"];
}
message OpenStatusUpdate {
oneof update {
@ -803,8 +892,33 @@ message OpenStatusUpdate {
}
}
message PendingChannelRequest {}
message PendingChannelResponse {
message PendingHTLC {
/// The direction within the channel that the htlc was sent
bool incoming = 1 [ json_name = "incoming" ];
/// The total value of the htlc
int64 amount = 2 [ json_name = "amount" ];
/// The final output to be swept back to the user's wallet
string outpoint = 3 [ json_name = "outpoint" ];
/// The next block height at which we can spend the current stage
uint32 maturity_height = 4 [ json_name = "maturity_height" ];
/**
The number of blocks remaining until the current stage can be swept.
Negative values indicate how many blocks have passed since becoming
mature.
*/
int32 blocks_til_maturity = 5 [ json_name = "blocks_til_maturity" ];
/// Indicates whether the htlc is in its first or second stage of recovery
uint32 stage = 6 [ json_name = "stage" ];
}
message PendingChannelsRequest {}
message PendingChannelsResponse {
message PendingChannel {
string remote_node_pub = 1 [ json_name = "remote_node_pub" ];
string channel_point = 2 [ json_name = "channel_point" ];
@ -822,9 +936,6 @@ message PendingChannelResponse {
/// The height at which this channel will be confirmed
uint32 confirmation_height = 2 [ json_name = "confirmation_height" ];
/// The number of blocks until this channel is open
uint32 blocks_till_open = 3 [ json_name = "blocks_till_open" ];
/**
The amount calculated to be paid in fees for the current set of
commitment transactions. The fee amount is persisted with the channel
@ -857,8 +968,6 @@ message PendingChannelResponse {
/// The pending channel to be force closed
PendingChannel channel = 1 [ json_name = "channel" ];
// TODO(roasbeef): HTLC's as well?
/// The transaction id of the closing transaction
string closing_txid = 2 [ json_name = "closing_txid" ];
@ -868,8 +977,17 @@ message PendingChannelResponse {
/// The height at which funds can be sweeped into the wallet
uint32 maturity_height = 4 [ json_name = "maturity_height" ];
/// Remaining # of blocks until funds can be sweeped into the wallet
uint32 blocks_til_maturity = 5 [ json_name = "blocks_til_maturity" ];
/*
Remaining # of blocks until the commitment output can be swept.
Negative values indicate how many blocks have passed since becoming
mature.
*/
int32 blocks_til_maturity = 5 [ json_name = "blocks_til_maturity" ];
/// The total value of funds successfully recovered from this channel
int64 recovered_balance = 6 [ json_name = "recovered_balance" ];
repeated PendingHTLC pending_htlcs = 8 [ json_name = "pending_htlcs" ];
}
/// The balance in satoshis encumbered in pending channels
@ -891,7 +1009,13 @@ message WalletBalanceRequest {
}
message WalletBalanceResponse {
/// The balance of the wallet
int64 balance = 1 [json_name = "balance"];
int64 total_balance = 1 [json_name = "total_balance"];
/// The confirmed balance of a wallet(with >= 1 confirmations)
int64 confirmed_balance = 2 [json_name = "confirmed_balance"];
/// The unconfirmed balance of a wallet(with 0 confirmations)
int64 unconfirmed_balance = 3 [json_name = "unconfirmed_balance"];
}
message ChannelBalanceRequest {
@ -994,6 +1118,7 @@ message LightningNode {
string pub_key = 2 [ json_name = "pub_key" ];
string alias = 3 [ json_name = "alias" ];
repeated NodeAddress addresses = 4 [ json_name = "addresses" ];
string color = 5 [ json_name = "color" ];
}
message NodeAddress {
@ -1121,12 +1246,6 @@ message ClosedChannelUpdate {
ChannelPoint chan_point = 4;
}
message SetAliasRequest {
string new_alias = 1;
}
message SetAliasResponse {
}
message Invoice {
/**
An optional memo to attach along with the invoice. Used for record keeping
@ -1179,6 +1298,9 @@ message Invoice {
/// Fallback on-chain address.
string fallback_addr = 12 [json_name = "fallback_addr"];
/// Delta to use for the time-lock of the CLTV extended to the final hop.
uint64 cltv_expiry = 13 [json_name = "cltv_expiry"];
}
message AddInvoiceResponse {
bytes r_hash = 1 [json_name = "r_hash"];
@ -1263,6 +1385,7 @@ message PayReq {
string description = 6 [json_name = "description"];
string description_hash = 7 [json_name = "description_hash"];
string fallback_addr = 8 [json_name = "fallback_addr"];
int64 cltv_expiry = 9 [json_name = "cltv_expiry"];
}
message FeeReportRequest {}
@ -1284,12 +1407,12 @@ message FeeReportResponse {
repeated ChannelFeeReport channel_fees = 1 [json_name = "channel_fees"];
}
message FeeUpdateRequest {
message PolicyUpdateRequest {
oneof scope {
/// If set, then this fee update applies to all currently active channels.
/// If set, then this update applies to all currently active channels.
bool global = 1 [json_name = "global"] ;
/// If set, this fee update will target a specific channel.
/// If set, this update will target a specific channel.
ChannelPoint chan_point = 2 [json_name = "chan_point"];
}
@ -1298,6 +1421,9 @@ message FeeUpdateRequest {
/// The effective fee rate in milli-satoshis. The precision of this value goes up to 6 decimal places, so 1e-6.
double fee_rate = 4 [json_name = "fee_rate"];
/// The required timelock delta for HTLCs forwarded over the channel.
uint32 time_lock_delta = 5 [json_name = "time_lock_delta"];
}
message FeeUpdateResponse {
message PolicyUpdateResponse {
}

57
app/lnd/lib/rpc.proto

@ -202,7 +202,7 @@ service Lightning {
workflow and is waiting for confirmations for the funding txn, or is in the
process of closure, either initiated cooperatively or non-cooperatively.
*/
rpc PendingChannels (PendingChannelRequest) returns (PendingChannelResponse) {
rpc PendingChannels (PendingChannelsRequest) returns (PendingChannelsResponse) {
option (google.api.http) = {
get: "/v1/channels/pending"
};
@ -423,11 +423,6 @@ service Lightning {
*/
rpc SubscribeChannelGraph(GraphTopologySubscription) returns (stream GraphTopologyUpdate);
/**
SetAlias sets the alias for this node; e.g. "alice"
*/
rpc SetAlias(SetAliasRequest) returns (SetAliasResponse);
/** lncli: `debuglevel`
DebugLevel allows a caller to programmatically set the logging verbosity of
lnd. The logging can be targeted according to a coarse daemon-wide logging
@ -446,11 +441,11 @@ service Lightning {
};
}
/** lncli: `updatefees`
UpdateFees allows the caller to update the fee schedule for all channels
globally, or a particular channel.
/** lncli: `updatechanpolicy`
UpdateChannelPolicy allows the caller to update the fee schedule and
channel policies for all channels globally, or a particular channel.
*/
rpc UpdateFees(FeeUpdateRequest) returns (FeeUpdateResponse) {
rpc UpdateChannelPolicy(PolicyUpdateRequest) returns (PolicyUpdateResponse) {
option (google.api.http) = {
post: "/v1/fees"
body: "*"
@ -512,6 +507,9 @@ message SendRequest {
payment to the recipient.
*/
string payment_request = 6;
/// The CLTV delta from the current height that should be used to set the timelock for the final hop.
int32 final_cltv_delta = 7;
}
message SendResponse {
string payment_error = 1 [json_name = "payment_error"];
@ -798,13 +796,16 @@ message GetInfoResponse {
string block_hash = 8 [json_name = "block_hash"];
/// Whether the wallet's view is synced to the main chain
bool synced_to_chain = 9 [ json_name = "synced_to_chain" ];
bool synced_to_chain = 9 [json_name = "synced_to_chain"];
/// Whether the current node is connected to testnet
bool testnet = 10 [ json_name = "testnet" ];
bool testnet = 10 [json_name = "testnet"];
/// A list of active chains the node is connected to
repeated string chains = 11 [ json_name = "chains" ];
repeated string chains = 11 [json_name = "chains"];
/// The URIs of the current node.
repeated string uris = 12 [json_name = "uris"];
}
message ConfirmationUpdate {
@ -876,6 +877,12 @@ message OpenChannelRequest {
/// A manual fee rate set in sat/byte that should be used when crafting the closure transaction.
int64 sat_per_byte = 7;
/// Whether this channel should be private, not announced to the greater network.
bool private = 8 [json_name = "private"];
/// The minimum value in millisatoshi we will require for incoming HTLCs on the channel.
int64 min_htlc_msat = 9 [json_name = "min_htlc_msat"];
}
message OpenStatusUpdate {
oneof update {
@ -910,8 +917,8 @@ message PendingHTLC {
uint32 stage = 6 [ json_name = "stage" ];
}
message PendingChannelRequest {}
message PendingChannelResponse {
message PendingChannelsRequest {}
message PendingChannelsResponse {
message PendingChannel {
string remote_node_pub = 1 [ json_name = "remote_node_pub" ];
string channel_point = 2 [ json_name = "channel_point" ];
@ -929,9 +936,6 @@ message PendingChannelResponse {
/// The height at which this channel will be confirmed
uint32 confirmation_height = 2 [ json_name = "confirmation_height" ];
/// The number of blocks until this channel is open
int32 blocks_till_open = 3 [ json_name = "blocks_till_open" ];
/**
The amount calculated to be paid in fees for the current set of
commitment transactions. The fee amount is persisted with the channel
@ -1242,12 +1246,6 @@ message ClosedChannelUpdate {
ChannelPoint chan_point = 4;
}
message SetAliasRequest {
string new_alias = 1;
}
message SetAliasResponse {
}
message Invoice {
/**
An optional memo to attach along with the invoice. Used for record keeping
@ -1409,12 +1407,12 @@ message FeeReportResponse {
repeated ChannelFeeReport channel_fees = 1 [json_name = "channel_fees"];
}
message FeeUpdateRequest {
message PolicyUpdateRequest {
oneof scope {
/// If set, then this fee update applies to all currently active channels.
/// If set, then this update applies to all currently active channels.
bool global = 1 [json_name = "global"] ;
/// If set, this fee update will target a specific channel.
/// If set, this update will target a specific channel.
ChannelPoint chan_point = 2 [json_name = "chan_point"];
}
@ -1423,6 +1421,9 @@ message FeeUpdateRequest {
/// The effective fee rate in milli-satoshis. The precision of this value goes up to 6 decimal places, so 1e-6.
double fee_rate = 4 [json_name = "fee_rate"];
/// The required timelock delta for HTLCs forwarded over the channel.
uint32 time_lock_delta = 5 [json_name = "time_lock_delta"];
}
message FeeUpdateResponse {
message PolicyUpdateResponse {
}

65
app/main.dev.js

@ -8,7 +8,7 @@
* When running `npm run build` or `npm run build-main`, this file is compiled to
* `./app/main.prod.js` using webpack. This gives us some performance wins.
*
* @flow
*
*/
import { app, BrowserWindow, ipcMain } from 'electron'
import path from 'path'
@ -83,6 +83,19 @@ const sendLndSyncing = () => {
}, 1000)
}
const sendStartOnboarding = () => {
const sendStartOnboardingInterval = setInterval(() => {
if (didFinishLoad) {
clearInterval(sendStartOnboardingInterval)
if (mainWindow) {
console.log('STARTING ONBOARDING')
mainWindow.webContents.send('startOnboarding')
}
}
}, 1000)
}
// Send the front end event letting them know the gRPC connection has started
const sendGrpcConnected = () => {
const sendGrpcConnectedInterval = setInterval(() => {
@ -126,18 +139,28 @@ const sendLndSynced = () => {
}
// Starts the LND node
const startLnd = () => {
const lndPath = path.join(__dirname, '..', 'resources', 'bin', plat, plat === 'win32' ? 'lnd.exe' : 'lnd')
const startLnd = (alias) => {
let lndPath
if (process.env.NODE_ENV === 'development') {
lndPath = path.join(__dirname, '..', 'resources', 'bin', plat, plat === 'win32' ? 'lnd.exe' : 'lnd')
} else {
lndPath = path.join(__dirname, '..', 'bin', plat === 'win32' ? 'lnd.exe' : 'lnd')
}
const neutrino = spawn(lndPath,
[
'--bitcoin.active',
'--bitcoin.testnet',
'--neutrino.active',
'--neutrino.connect=btcd0.lightning.computer:18333',
'--bitcoin.node=neutrino',
'--neutrino.connect=btcd.jackmallers.com:18333',
'--neutrino.addpeer=188.166.148.62:18333',
'--neutrino.addpeer=159.65.48.139:18333',
'--neutrino.connect=127.0.0.1:18333',
'--autopilot.active',
'--debuglevel=debug',
'--noencryptwallet'
'--noencryptwallet',
`--alias=${alias}`
]
)
.on('error', error => console.log(`lnd error: ${error}`))
@ -197,16 +220,13 @@ app.on('ready', async () => {
await installExtensions()
}
const icon = path.join(__dirname, '..', 'resources', 'icon.icns')
console.log('icon: ', icon)
mainWindow = new BrowserWindow({
show: false,
frame: true,
width: 700,
height: 1100,
minHeight: 700,
minWidth: 1100,
icon
titleBarStyle: 'hidden',
width: 950,
height: 600,
minWidth: 950,
minHeight: 425
})
mainWindow.loadURL(`file://${__dirname}/app.html`)
@ -227,22 +247,26 @@ app.on('ready', async () => {
mainWindow.on('closed', () => {
mainWindow = null
// shut down zap when a user closes the window
app.quit()
})
const menuBuilder = new MenuBuilder(mainWindow)
menuBuilder.buildMenu()
sendGrpcDisconnected()
// Check to see if and LND process is running
// Check to see if an LND process is running
lookup({ command: 'lnd' }, (err, results) => {
// There was an error checking for the LND process
if (err) { throw new Error(err) }
// No LND process was found
if (!results.length) {
// Assign path to certs to certPath
sendLndSyncing()
// let the application know onboarding has started
sendStartOnboarding()
// Assign path to certs to certPath
switch (os.platform()) {
case 'darwin':
certPath = path.join(homedir, 'Library/Application Support/Lnd/tls.cert')
@ -258,7 +282,12 @@ app.on('ready', async () => {
}
// Start LND
startLnd()
// startLnd()
// once the onboarding has finished we wanna let the application we have started syncing and start LND
ipcMain.on('onboardingFinished', (event, { alias }) => {
sendLndSyncing()
startLnd(alias)
})
} else {
// An LND process was found, no need to start our own
console.log('LND ALREADY RUNNING')

66
app/menu.js

@ -21,37 +21,55 @@ export default class MenuBuilder {
template = this.buildDefaultTemplate()
}
this.setupInputTemplate()
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
return menu
}
setupDevelopmentEnvironment() {
this.mainWindow.openDevTools()
setupInputTemplate() {
const selectionMenu = Menu.buildFromTemplate([
{ role: 'copy' },
{ type: 'separator' },
{ role: 'selectall' }
])
const inputMenu = Menu.buildFromTemplate([
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ type: 'separator' },
{ role: 'selectall' }
])
this.mainWindow.webContents.on('context-menu', (e, props) => {
const { x, y } = props
Menu
.buildFromTemplate([{
label: 'Inspect element',
click: () => {
this.mainWindow.inspectElement(x, y)
}
}])
.popup(this.mainWindow)
const { selectionText, isEditable } = props
if (isEditable) {
inputMenu.popup(this.mainWindow)
} else if (selectionText && selectionText.trim() !== '') {
selectionMenu.popup(this.mainWindow)
}
})
}
setupDevelopmentEnvironment() {
this.mainWindow.openDevTools()
}
buildDarwinTemplate() {
const subMenuAbout = {
label: 'Electron',
submenu: [
{ label: 'About ElectronReact', selector: 'orderFrontStandardAboutPanel:' },
{ type: 'separator' },
{ label: 'Services', submenu: [] },
{ label: 'About Zap', selector: 'orderFrontStandardAboutPanel:' },
{ type: 'separator' },
{ label: 'Hide ElectronReact', accelerator: 'Command+H', selector: 'hide:' },
{ label: 'Hide Zap', accelerator: 'Command+H', selector: 'hide:' },
{ label: 'Hide Others', accelerator: 'Command+Shift+H', selector: 'hideOtherApplications:' },
{ label: 'Show All', selector: 'unhideAllApplications:' },
{ type: 'separator' },
@ -104,10 +122,10 @@ export default class MenuBuilder {
const subMenuHelp = {
label: 'Help',
submenu: [
{ label: 'Learn More', click() { shell.openExternal('http://electron.atom.io') } },
{ label: 'Documentation', click() { shell.openExternal('https://github.com/atom/electron/tree/master/docs#readme') } },
{ label: 'Community Discussions', click() { shell.openExternal('https://discuss.atom.io/c/electron') } },
{ label: 'Search Issues', click() { shell.openExternal('https://github.com/atom/electron/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') } }
]
}
@ -169,22 +187,22 @@ export default class MenuBuilder {
submenu: [{
label: 'Learn More',
click() {
shell.openExternal('http://electron.atom.io')
shell.openExternal('https://zap.jackmallers.com/')
}
}, {
label: 'Documentation',
click() {
shell.openExternal('https://github.com/atom/electron/tree/master/docs#readme')
shell.openExternal('https://github.com/LN-Zap/zap-desktop')
}
}, {
label: 'Community Discussions',
click() {
shell.openExternal('https://discuss.atom.io/c/electron')
shell.openExternal('zaphq.slack.com')
}
}, {
label: 'Search Issues',
click() {
shell.openExternal('https://github.com/atom/electron/issues')
shell.openExternal('https://github.com/LN-Zap/zap-desktop/issues')
}
}]
}]

10
app/package.json

@ -1,7 +1,7 @@
{
"name": "zap-desktop",
"productName": "zap-desktop",
"version": "0.0.1",
"productName": "ZapDesktop",
"version": "0.1.1",
"description": "desktop application for the lightning network",
"main": "./main.prod.js",
"author": {
@ -10,12 +10,12 @@
"url": "https://github.com/LN-Zap/zap-desktop"
},
"scripts": {
"postinstall": "npm rebuild --runtime=electron --target=1.6.6 --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.7.10"
"postinstall": "npm rebuild --runtime=electron --target=1.6.16 --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.6.16"
},
"license": "MIT",
"dependencies": {
"grpc": "^1.7.3",
"grpc": "1.7.1",
"ps-node": "^0.1.6",
"react-icons": "^2.2.5"
}

104
app/reducers/activity.js

@ -7,16 +7,17 @@ const initialState = {
filterPulldown: false,
filter: { key: 'ALL_ACTIVITY', name: 'All Activity' },
filters: [
{ key: 'ALL_ACTIVITY', name: 'All Activity' },
{ key: 'LN_ACTIVITY', name: 'LN Activity' },
{ key: 'PAYMENT_ACTIVITY', name: 'LN Payments' },
{ key: 'INVOICE_ACTIVITY', name: 'LN Invoices' },
{ key: 'TRANSACTION_ACTIVITY', name: 'On-chain Activity' }
{ key: 'ALL_ACTIVITY', name: 'All' },
{ key: 'SENT_ACTIVITY', name: 'Sent' },
{ key: 'REQUESTED_ACTIVITY', name: 'Requested' },
{ key: 'PENDING_ACTIVITY', name: 'Pending' },
{ key: 'FUNDED_ACTIVITY', name: 'Funding Transactions' }
],
modal: {
modalType: null,
modalProps: {}
}
},
searchText: ''
}
// ------------------------------------
@ -29,6 +30,8 @@ export const CHANGE_FILTER = 'CHANGE_FILTER'
export const TOGGLE_PULLDOWN = 'TOGGLE_PULLDOWN'
export const UPDATE_SEARCH_TEXT = 'UPDATE_SEARCH_TEXT'
// ------------------------------------
// Actions
// ------------------------------------
@ -59,6 +62,13 @@ export function toggleFilterPulldown() {
}
}
export function updateSearchText(searchText) {
return {
type: UPDATE_SEARCH_TEXT,
searchText
}
}
// ------------------------------------
// Action Handlers
// ------------------------------------
@ -66,7 +76,9 @@ const ACTION_HANDLERS = {
[SHOW_ACTIVITY_MODAL]: (state, { modalType, modalProps }) => ({ ...state, modal: { modalType, modalProps } }),
[HIDE_ACTIVITY_MODAL]: state => ({ ...state, modal: { modalType: null, modalProps: {} } }),
[CHANGE_FILTER]: (state, { filter }) => ({ ...state, filter, filterPulldown: false }),
[TOGGLE_PULLDOWN]: state => ({ ...state, filterPulldown: !state.filterPulldown })
[TOGGLE_PULLDOWN]: state => ({ ...state, filterPulldown: !state.filterPulldown }),
[UPDATE_SEARCH_TEXT]: (state, { searchText }) => ({ ...state, searchText })
}
// ------------------------------------
@ -75,49 +87,88 @@ const ACTION_HANDLERS = {
const activitySelectors = {}
const filtersSelector = state => state.activity.filters
const filterSelector = state => state.activity.filter
const searchSelector = state => state.activity.searchText
const paymentsSelector = state => state.payment.payments
const invoicesSelector = state => state.invoice.invoices
const transactionsSelector = state => state.transaction.transactions
const channelsSelector = state => state.channels.channels
const allActivity = createSelector(
searchSelector,
paymentsSelector,
invoicesSelector,
transactionsSelector,
(payments, invoices, transactions) => [...payments, ...invoices, ...transactions].sort((a, b) => {
const aTimestamp = Object.prototype.hasOwnProperty.call(a, 'time_stamp') ? a.time_stamp : a.creation_date
const bTimestamp = Object.prototype.hasOwnProperty.call(b, 'time_stamp') ? b.time_stamp : b.creation_date
return bTimestamp - aTimestamp
})
(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))) {
return true
}
return false
})
return searchedArr.sort((a, b) => {
// this will return the correct timestamp to use when sorting (time_stamp, creation_date, or settle_date)
function returnTimestamp(transaction) {
// if on-chain txn
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 invoice that has not been paid or an LN payment
return transaction.creation_date
}
const aTimestamp = returnTimestamp(a)
const bTimestamp = returnTimestamp(b)
return bTimestamp - aTimestamp
})
}
)
const lnActivity = createSelector(
paymentsSelector,
const invoiceActivity = createSelector(
invoicesSelector,
(payments, invoices) => [...payments, ...invoices].sort((a, b) => b.creation_date - a.creation_date)
invoices => invoices
)
const paymentActivity = createSelector(
const sentActivity = createSelector(
transactionsSelector,
paymentsSelector,
payments => payments
(transactions, payments) => {
const sentTransactions = transactions.filter(transaction => transaction.amount < 0)
return [...sentTransactions, ...payments].sort((a, b) => {
const aTimestamp = Object.prototype.hasOwnProperty.call(a, 'time_stamp') ? a.time_stamp : a.creation_date
const bTimestamp = Object.prototype.hasOwnProperty.call(b, 'time_stamp') ? b.time_stamp : b.creation_date
return bTimestamp - aTimestamp
})
}
)
const invoiceActivity = createSelector(
const pendingActivity = createSelector(
invoicesSelector,
invoices => invoices
invoices => invoices.filter(invoice => !invoice.settled)
)
const transactionActivity = createSelector(
const fundedActivity = createSelector(
transactionsSelector,
transactions => transactions
channelsSelector,
(transactions, channels) => {
const fundingTxIds = channels.map(channel => channel.channel_point.split(':')[0])
const fundingTxs = transactions.filter(transaction => fundingTxIds.includes(transaction.tx_hash))
return fundingTxs.sort((a, b) => b.time_stamp - a.time_stamp)
}
)
const FILTERS = {
ALL_ACTIVITY: allActivity,
LN_ACTIVITY: lnActivity,
PAYMENT_ACTIVITY: paymentActivity,
INVOICE_ACTIVITY: invoiceActivity,
TRANSACTION_ACTIVITY: transactionActivity
SENT_ACTIVITY: sentActivity,
REQUESTED_ACTIVITY: invoiceActivity,
PENDING_ACTIVITY: pendingActivity,
FUNDED_ACTIVITY: fundedActivity
}
activitySelectors.currentActivity = createSelector(
@ -131,7 +182,6 @@ activitySelectors.nonActiveFilters = createSelector(
(filters, filter) => filters.filter(f => f.key !== filter.key)
)
export { activitySelectors }

12
app/reducers/channels.js

@ -512,13 +512,13 @@ const initialState = {
viewType: 0,
filterPulldown: false,
filter: { key: 'ALL_CHANNELS', name: 'All Contacts' },
filter: { key: 'ALL_CHANNELS', name: 'All' },
filters: [
{ key: 'ALL_CHANNELS', name: 'All Contacts' },
{ key: 'ACTIVE_CHANNELS', name: 'Online Contacts' },
{ key: 'NON_ACTIVE_CHANNELS', name: 'Offline Contacts' },
{ key: 'OPEN_PENDING_CHANNELS', name: 'Pending Contacts' },
{ key: 'CLOSING_PENDING_CHANNELS', name: 'Closing Contacts' }
{ key: 'ALL_CHANNELS', name: 'All' },
{ key: 'ACTIVE_CHANNELS', name: 'Online' },
{ key: 'NON_ACTIVE_CHANNELS', name: 'Offline' },
{ key: 'OPEN_PENDING_CHANNELS', name: 'Pending' },
{ key: 'CLOSING_PENDING_CHANNELS', name: 'Closing' }
],
loadingChannelPubkeys: [],

50
app/reducers/contactsform.js

@ -1,12 +1,16 @@
import { createSelector } from 'reselect'
import filter from 'lodash/filter'
import isEmpty from 'lodash/isEmpty'
// Initial State
const initialState = {
isOpen: false,
searchQuery: '',
contactCapacity: 0.1
manualSearchQuery: '',
contactCapacity: 0.1,
showErrors: {
manualInput: false
}
}
// Constants
@ -18,6 +22,10 @@ export const UPDATE_CONTACT_FORM_SEARCH_QUERY = 'UPDATE_CONTACT_FORM_SEARCH_QUER
export const UPDATE_CONTACT_CAPACITY = 'UPDATE_CONTACT_CAPACITY'
export const UPDATE_MANUAL_FORM_ERRORS = 'UPDATE_MANUAL_FORM_ERRORS'
export const UPDATE_MANUAL_FORM_SEARCH_QUERY = 'UPDATE_MANUAL_FORM_SEARCH_QUERY'
// ------------------------------------
// Actions
// ------------------------------------
@ -40,6 +48,13 @@ export function updateContactFormSearchQuery(searchQuery) {
}
}
export function updateManualFormSearchQuery(manualSearchQuery) {
return {
type: UPDATE_MANUAL_FORM_SEARCH_QUERY,
manualSearchQuery
}
}
export function updateContactCapacity(contactCapacity) {
return {
type: UPDATE_CONTACT_CAPACITY,
@ -47,6 +62,13 @@ export function updateContactCapacity(contactCapacity) {
}
}
export function updateManualFormErrors(errorsObject) {
return {
type: UPDATE_MANUAL_FORM_ERRORS,
errorsObject
}
}
// ------------------------------------
// Action Handlers
// ------------------------------------
@ -56,7 +78,13 @@ const ACTION_HANDLERS = {
[UPDATE_CONTACT_FORM_SEARCH_QUERY]: (state, { searchQuery }) => ({ ...state, searchQuery }),
[UPDATE_CONTACT_CAPACITY]: (state, { contactCapacity }) => ({ ...state, contactCapacity })
[UPDATE_MANUAL_FORM_SEARCH_QUERY]: (state, { searchQuery }) => ({ ...state, searchQuery }),
[UPDATE_CONTACT_CAPACITY]: (state, { contactCapacity }) => ({ ...state, contactCapacity }),
[UPDATE_MANUAL_FORM_ERRORS]: (state, { errorsObject }) => ({ ...state, showErrors: Object.assign(state.showErrors, errorsObject) }),
[UPDATE_MANUAL_FORM_SEARCH_QUERY]: (state, { manualSearchQuery }) => ({ ...state, manualSearchQuery })
}
// ------------------------------------
@ -65,6 +93,7 @@ const ACTION_HANDLERS = {
const contactFormSelectors = {}
const networkNodesSelector = state => state.network.nodes
const searchQuerySelector = state => state.contactsform.searchQuery
const manualSearchQuerySelector = state => state.contactsform.manualSearchQuery
contactFormSelectors.filteredNetworkNodes = createSelector(
@ -97,6 +126,21 @@ contactFormSelectors.showManualForm = createSelector(
}
)
contactFormSelectors.manualFormIsValid = createSelector(
manualSearchQuerySelector,
(input) => {
const errors = {}
if (!input.length || !input.includes('@')) {
errors.manualInput = 'Invalid format'
}
return {
errors,
isValid: isEmpty(errors)
}
}
)
export { contactFormSelectors }
// ------------------------------------

3
app/reducers/index.js

@ -1,6 +1,6 @@
// @flow
import { combineReducers } from 'redux'
import { routerReducer as router } from 'react-router-redux'
import onboarding from './onboarding'
import lnd from './lnd'
import ticker from './ticker'
import info from './info'
@ -26,6 +26,7 @@ import error from './error'
const rootReducer = combineReducers({
router,
onboarding,
lnd,
ticker,
info,

6
app/reducers/ipc.js

@ -35,6 +35,8 @@ import {
import { receiveDescribeNetwork, receiveQueryRoutes, receiveInvoiceAndQueryRoutes } from './network'
import { startOnboarding } from './onboarding'
// Import all receiving IPC event handlers and pass them into createIpc
const ipc = createIpc({
lndSyncing,
@ -91,7 +93,9 @@ const ipc = createIpc({
receiveDescribeNetwork,
receiveQueryRoutes,
receiveInvoiceAndQueryRoutes
receiveInvoiceAndQueryRoutes,
startOnboarding
})
export default ipc

8
app/reducers/lnd.js

@ -122,7 +122,13 @@ const lndBlockHeightSelector = state => state.lnd.lndBlockHeight
lndSelectors.syncPercentage = createSelector(
blockHeightSelector,
lndBlockHeightSelector,
(blockHeight, lndBlockHeight) => (Math.floor((lndBlockHeight / blockHeight) * 100))
(blockHeight, lndBlockHeight) => {
const percentage = Math.floor((lndBlockHeight / blockHeight) * 100)
if (percentage === Infinity) { return '' }
return percentage
}
)
export { lndSelectors }

69
app/reducers/onboarding.js

@ -0,0 +1,69 @@
import { ipcRenderer } from 'electron'
// ------------------------------------
// Constants
// ------------------------------------
export const UPDATE_ALIAS = 'UPDATE_ALIAS'
export const CHANGE_STEP = 'CHANGE_STEP'
export const ONBOARDING_STARTED = 'ONBOARDING_STARTED'
export const ONBOARDING_FINISHED = 'ONBOARDING_FINISHED'
// ------------------------------------
// Actions
// ------------------------------------
export function updateAlias(alias) {
return {
type: UPDATE_ALIAS,
alias
}
}
export function changeStep(step) {
return {
type: CHANGE_STEP,
step
}
}
export function submit(alias) {
// alert the app we're done onboarding and it's cool to start LND
ipcRenderer.send('onboardingFinished', { alias })
return {
type: ONBOARDING_FINISHED
}
}
export const startOnboarding = () => (dispatch) => {
dispatch({ type: ONBOARDING_STARTED })
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[UPDATE_ALIAS]: (state, { alias }) => ({ ...state, alias }),
[CHANGE_STEP]: (state, { step }) => ({ ...state, step }),
[ONBOARDING_STARTED]: state => ({ ...state, onboarded: false }),
[ONBOARDING_FINISHED]: state => ({ ...state, onboarded: true })
}
// ------------------------------------
// Reducer
// ------------------------------------
const initialState = {
onboarded: true,
step: 1,
alias: ''
}
// ------------------------------------
// Reducer
// ------------------------------------
export default function lndReducer(state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}

4
app/routes.js

@ -3,14 +3,10 @@ import React from 'react'
import { Switch, Route } from 'react-router'
import App from './routes/app'
import Activity from './routes/activity'
import Contacts from './routes/contacts'
import Network from './routes/network'
const routes = () => (
<App>
<Switch>
<Route path='/contacts' component={Contacts} />
<Route path='/network' component={Network} />
<Route path='/' component={Activity} />
</Switch>
</App>

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

@ -1,7 +1,5 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { MdSearch } from 'react-icons/lib/md'
import { FaAngleDown } from 'react-icons/lib/fa'
import Wallet from 'components/Wallet'
import LoadingBolt from 'components/LoadingBolt'
@ -47,20 +45,19 @@ class Activity extends Component {
render() {
const {
ticker,
searchInvoices,
invoice: { invoicesSearchText, invoiceLoading },
invoice: { invoiceLoading },
address: { address },
balance,
info,
payment: { paymentLoading },
currentTicker,
activity: { modal, filter, filterPulldown },
activity: { modal, filters, filter, filterPulldown },
hideActivityModal,
changeFilter,
toggleFilterPulldown,
currentActivity,
nonActiveFilters,
newAddress
newAddress,
openPayForm,
openRequestForm
} = this.props
if (invoiceLoading || paymentLoading) { return <LoadingBolt /> }
@ -77,33 +74,26 @@ class Activity extends Component {
currentTicker={currentTicker}
/>
<Wallet balance={balance} address={address} info={info} newAddress={newAddress} />
<div className={styles.search}>
<label className={`${styles.label} ${styles.input}`} htmlFor='invoiceSearch'>
<MdSearch />
</label>
<input
value={invoicesSearchText}
onChange={event => searchInvoices(event.target.value)}
className={`${styles.text} ${styles.input}`}
placeholder='Search by amount, hash, memo, etc'
type='text'
id='invoiceSearch'
/>
</div>
<Wallet
balance={balance}
address={address}
info={info}
newAddress={newAddress}
currentTicker={currentTicker}
openPayForm={openPayForm}
openRequestForm={openRequestForm}
/>
<div className={styles.activities}>
<header className={styles.header}>
<section>
<h2 onClick={toggleFilterPulldown}>
{filter.name} <span className={filterPulldown && styles.pulldown}><FaAngleDown /></span>
</h2>
<ul className={`${styles.filters} ${filterPulldown && styles.active}`}>
<ul className={styles.filters}>
{
nonActiveFilters.map(f => (
<li key={f.key} onClick={() => changeFilter(f)}>
{f.name}
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>
))
}
@ -132,7 +122,6 @@ Activity.propTypes = {
fetchBalance: PropTypes.func.isRequired,
ticker: PropTypes.object.isRequired,
searchInvoices: PropTypes.func.isRequired,
invoice: PropTypes.object.isRequired,
payment: PropTypes.object.isRequired,
currentTicker: PropTypes.object.isRequired,
@ -141,11 +130,11 @@ Activity.propTypes = {
hideActivityModal: PropTypes.func.isRequired,
changeFilter: PropTypes.func.isRequired,
newAddress: PropTypes.func.isRequired,
toggleFilterPulldown: PropTypes.func.isRequired,
openPayForm: PropTypes.func.isRequired,
openRequestForm: PropTypes.func.isRequired,
activity: PropTypes.object.isRequired,
currentActivity: PropTypes.array.isRequired,
nonActiveFilters: PropTypes.array.isRequired,
address: PropTypes.object.isRequired,
balance: PropTypes.object.isRequired,
info: PropTypes.object.isRequired

114
app/routes/activity/components/Activity.scss

@ -32,91 +32,75 @@
.activities {
background: $white;
height: 100%;
.header {
padding: 60px 0 20px 0;
background: $spaceblue;
color: $white;
margin: 0 auto;
padding: 0 40px;
border-bottom: 1px solid $spaceborder;
.filters {
display: flex;
flex-direction: row;
user-select: none;
li {
position: relative;
opacity: 0.5;
font-size: 14px;
cursor: pointer;
&.activeFilter {
opacity: 1;
}
&:nth-child(1) {
margin-left: 0;
}
span {
display: block;
position: relative;
margin: 0 15px;
padding: 20px 0;
}
}
section {
position: relative;
margin-left: auto;
margin-right: auto;
padding-left: 100px;
padding-right: 100px;
max-width: 964px;
h2 {
color: $bluegrey;
}
}
h2, h2 span {
color: $bluegrey;
cursor: pointer;
transition: color 0.25s;
&:hover {
color: lighten($bluegrey, 10%);
.activeBorder {
left: 15px;
right: 15px;
width: auto;
height: 1px;
background: white;
position: absolute;
bottom: -1px;
}
}
h2, .filters li {
text-transform: uppercase;
letter-spacing: 1.5px;
color: $darkestgrey;
font-size: 14px;
font-weight: 400;
}
h2 span.pulldown {
color: $main;
}
.filters {
display: none;
&.active {
display: block;
position: absolute;
bottom: -100px;
z-index: 10;
li {
margin: 5px 0;
cursor: pointer;
&:hover {
color: $main;
}
}
}
}
}
}
.activityContainer {
background: $white;
background: $spaceblue;
transition: opacity 0.25s;
padding-bottom: 50px;
min-height: 100vh;
height: 100vh;
overflow-y: scroll;
padding-top: 20px;
&.pulldown {
opacity: 0.15;
}
}
.activityList {
width: 75%;
margin: 0 auto;
background: $white;
}
.activity {
padding: 0 100px;
padding: 0 40px;
&:hover {
background-color: #f0f0f0;
transition-delay: 0s;
outline: $grey solid 1px;
}
.left, .center, .right {
@ -140,4 +124,4 @@
.right {
text-align: right;
}
}
}

79
app/routes/activity/components/components/Activity.scss

@ -4,16 +4,17 @@
display: flex;
flex-direction: row;
cursor: pointer;
max-width: 960px;
margin: 0 auto;
height: 76px;
align-items: center;
border-bottom: 1px solid $grey;
font-size: 14px;
transition: background-color .1s linear;
transition-delay: .1s;
color: $darkestgrey;
color: $white;
position: relative;
&.unpaid {
opacity: 0.5;
}
}
.clock {
@ -37,22 +38,20 @@
justify-content: center;
align-items: center;
padding-right: 50px;
min-width: 90px;
time {
text-transform: uppercase;
section {
margin: 2.5px 0;
}
&:nth-child(1) {
display: flex;
align-items: center;
height: 38px;
font-size: 18px;
margin-bottom: 5px;
}
svg {
width: 12.5px;
height: 12.5px;
}
&:nth-child(2) {
font-size: 12px;
}
}
time {
font-size: 12px;
}
}
.data {
@ -61,6 +60,10 @@
flex: 6;
justify-content: space-evenly;
&:nth-child(2) {
font-size: 10px;
}
.title {
margin-bottom: 5px;
}
@ -70,23 +73,24 @@
}
h3, span {
font-size: 18px;
font-size: 14px;
font-weight: bold;
letter-spacing: 1.2px;
}
.icon {
display: inline-block;
flex: none;
position: relative;
width: 36px;
height: 36px;
border: 1px solid $darkestgrey;
width: 20px;
height: 20px;
background: #31343f;
border-radius: 50%;
margin-right: 15px;
margin-right: 5px;
svg {
color: $main;
font-size: 16px;
color: $white;
font-size: 10px;
vertical-align: middle;
display: flex;
top: 0;
@ -116,16 +120,29 @@
flex-direction: column;
flex: 1;
text-align: right;
font-size: 16px;
&.positive span:nth-child(1) {
font-weight: bold;
color: $main;
}
font-size: 12px;
color: $white;
span {
&:nth-child(1) {
margin-bottom: 5px;
}
&:nth-child(2) {
font-size: 12px;
font-size: 10px;
opacity: 0.5;
}
.plus, .minus {
margin-right: 2px;
}
.plus {
color: $green;
}
.minus {
color: $red;
}
}
}

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

@ -2,31 +2,31 @@ import React from 'react'
import PropTypes from 'prop-types'
import Moment from 'react-moment'
import 'moment-timezone'
import { FaBolt, FaClockO } from 'react-icons/lib/fa'
import Isvg from 'react-inlinesvg'
import { FaBolt } from 'react-icons/lib/fa'
import { btc } from 'utils'
import checkmarkIcon from 'icons/check_circle.svg'
import clockIcon from 'icons/clock.svg'
import styles from '../Activity.scss'
const Invoice = ({
invoice, ticker, currentTicker, showActivityModal
}) => (
<div className={styles.container} onClick={() => showActivityModal('INVOICE', { invoice })}>
{
!invoice.settled ?
<div className={styles.clock}>
<i className='hint--top' data-hint='Request has not been paid'>
<FaClockO />
</i>
</div>
:
null
}
<div className={`${styles.container} ${!invoice.settled && styles.unpaid}`} onClick={() => showActivityModal('INVOICE', { invoice })}>
<div className={styles.date}>
<Moment format='D'>
{invoice.creation_date * 1000}
</Moment>
<Moment format='MMMM'>
{invoice.creation_date * 1000}
</Moment>
<section>
{
invoice.settled ?
<Isvg src={checkmarkIcon} />
:
<i className='hint--top' data-hint='Request has not been paid'>
<Isvg src={clockIcon} />
</i>
}
</section>
<section>
<Moment format='MMM'>{invoice.creation_date * 1000}</Moment> <Moment format='D'>{invoice.creation_date * 1000}</Moment>
</section>
</div>
<div className={styles.data}>
<div className={styles.title}>
@ -46,7 +46,7 @@ const Invoice = ({
</div>
<div className={`${styles.amount} ${invoice.settled ? styles.positive : styles.negative}`}>
<span className='hint--top' data-hint='Invoice amount'>
+
<i className={styles.plus}>+</i>
{
ticker.currency === 'usd' ?
btc.satoshisToUsd(invoice.value, currentTicker.price_usd)

2
app/routes/activity/components/components/Modal/Modal.js

@ -24,7 +24,7 @@ const Modal = ({
},
content: {
top: 'auto',
left: '20%',
left: '0',
right: '0',
bottom: 'auto',
width: '40%',

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

@ -2,8 +2,10 @@ import React from 'react'
import PropTypes from 'prop-types'
import Moment from 'react-moment'
import 'moment-timezone'
import Isvg from 'react-inlinesvg'
import { FaBolt } from 'react-icons/lib/fa'
import { btc } from 'utils'
import checkmarkIcon from 'icons/check_circle.svg'
import styles from '../Activity.scss'
const Payment = ({
@ -11,12 +13,12 @@ const Payment = ({
}) => (
<div className={styles.container} onClick={() => showActivityModal('PAYMENT', { payment })}>
<div className={styles.date}>
<Moment format='D'>
{payment.creation_date * 1000}
</Moment>
<Moment format='MMMM'>
{payment.creation_date * 1000}
</Moment>
<section>
<Isvg src={checkmarkIcon} />
</section>
<section>
<Moment format='MMM'>{payment.creation_date * 1000}</Moment> <Moment format='D'>{payment.creation_date * 1000}</Moment>
</section>
</div>
<div className={styles.data}>
<div className={styles.title}>
@ -36,7 +38,7 @@ const Payment = ({
</div>
<div className={styles.amount}>
<span className='hint--top' data-hint='Payment amount'>
-
<i className={styles.minus}>-</i>
{
ticker.currency === 'usd' ?
btc.satoshisToUsd(payment.value, currentTicker.price_usd)

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

@ -2,8 +2,10 @@ import React from 'react'
import PropTypes from 'prop-types'
import Moment from 'react-moment'
import 'moment-timezone'
import Isvg from 'react-inlinesvg'
import { FaChain } from 'react-icons/lib/fa'
import { btc } from 'utils'
import checkmarkIcon from 'icons/check_circle.svg'
import styles from '../Activity.scss'
const Transaction = ({
@ -11,12 +13,12 @@ const Transaction = ({
}) => (
<div className={styles.container} onClick={() => showActivityModal('TRANSACTION', { transaction })}>
<div className={styles.date}>
<Moment format='D'>
{transaction.time_stamp * 1000}
</Moment>
<Moment format='MMMM'>
{transaction.time_stamp * 1000}
</Moment>
<section>
<Isvg src={checkmarkIcon} />
</section>
<section>
<Moment format='MMM'>{transaction.time_stamp * 1000}</Moment> <Moment format='D'>{transaction.time_stamp * 1000}</Moment>
</section>
</div>
<div className={styles.data}>
<div className={styles.title}>
@ -36,7 +38,7 @@ const Transaction = ({
</div>
<div className={`${styles.amount} ${transaction.amount > 0 ? styles.positive : styles.negative}`}>
<span className='hint--top' data-hint='Transaction amount'>
{ transaction.amount > 0 ? '+' : '' }
<i className={transaction.amount > 0 ? styles.plus : styles.minus}>{ transaction.amount > 0 ? '+' : '-' }</i>
{
ticker.currency === 'usd' ?
btc.satoshisToUsd(transaction.amount, currentTicker.price_usd)

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

@ -3,7 +3,6 @@ import { tickerSelectors } from 'reducers/ticker'
import { fetchBalance } from 'reducers/balance'
import {
fetchInvoices,
searchInvoices,
setInvoice,
invoiceSelectors
} from 'reducers/invoice'
@ -18,9 +17,11 @@ import {
hideActivityModal,
changeFilter,
toggleFilterPulldown,
activitySelectors
activitySelectors,
updateSearchText
} from 'reducers/activity'
import { newAddress } from 'reducers/address'
import { setFormType } from 'reducers/form'
import Activity from '../components/Activity'
@ -30,13 +31,14 @@ const mapDispatchToProps = {
fetchPayments,
fetchInvoices,
fetchTransactions,
searchInvoices,
showActivityModal,
hideActivityModal,
changeFilter,
toggleFilterPulldown,
newAddress,
fetchBalance
fetchBalance,
updateSearchText,
setFormType
}
const mapStateToProps = state => ({
@ -62,4 +64,15 @@ const mapStateToProps = state => ({
nonActiveFilters: activitySelectors.nonActiveFilters(state)
})
export default connect(mapStateToProps, mapDispatchToProps)(Activity)
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...stateProps,
...dispatchProps,
...ownProps,
// action to open the pay form
openPayForm: () => dispatchProps.setFormType('PAY_FORM'),
// action to open the request form
openRequestForm: () => dispatchProps.setFormType('REQUEST_FORM')
})
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(Activity)

48
app/routes/app/components/App.js

@ -1,19 +1,34 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import GlobalError from 'components/GlobalError'
import LoadingBolt from 'components/LoadingBolt'
import Form from 'components/Form'
import ModalRoot from 'components/ModalRoot'
import Nav from 'components/Nav'
import Network from 'components/Contacts/Network'
import ContactModal from 'components/Contacts/ContactModal'
import ContactsForm from 'components/Contacts/ContactsForm'
import styles from './App.scss'
class App extends Component {
componentWillMount() {
const { fetchTicker, fetchInfo, newAddress } = this.props
const { fetchTicker, fetchInfo, newAddress, fetchChannels, fetchBalance, fetchDescribeNetwork } = this.props
// fetch price ticker
fetchTicker()
// fetch node info
fetchInfo()
// fetch new address for wallet
newAddress('np2wkh')
// fetch nodes channels
fetchChannels()
// fetch nodes balance
fetchBalance()
// fetch LN network from nides POV
fetchDescribeNetwork()
}
render() {
@ -24,14 +39,16 @@ class App extends Component {
currentTicker,
form,
openPayForm,
openRequestForm,
formProps,
closeForm,
error: { error },
clearError,
contactModalProps,
contactsFormProps,
networkTabProps,
children
} = this.props
@ -39,6 +56,7 @@ class App extends Component {
return (
<div>
<div className={styles.titleBar} />
<GlobalError error={error} clearError={clearError} />
<ModalRoot
modalType={modalType}
@ -48,16 +66,16 @@ class App extends Component {
currency={ticker.currency}
/>
<Form formType={form.formType} formProps={formProps} closeForm={closeForm} />
<ContactModal {...contactModalProps} />
<ContactsForm {...contactsFormProps} />
<Nav
openPayForm={openPayForm}
openRequestForm={openRequestForm}
/>
<Form formType={form.formType} formProps={formProps} closeForm={closeForm} />
<div className={styles.content}>
{children}
</div>
<Network {...networkTabProps} />
</div>
)
}
@ -70,17 +88,19 @@ App.propTypes = {
formProps: PropTypes.object.isRequired,
closeForm: PropTypes.func.isRequired,
error: PropTypes.object.isRequired,
currentTicker: PropTypes.object,
contactModalProps: PropTypes.object,
contactsFormProps: PropTypes.object,
networkTabProps: PropTypes.object,
newAddress: PropTypes.func.isRequired,
fetchInfo: PropTypes.func.isRequired,
hideModal: PropTypes.func.isRequired,
fetchTicker: PropTypes.func.isRequired,
openPayForm: PropTypes.func.isRequired,
openRequestForm: PropTypes.func.isRequired,
clearError: PropTypes.func.isRequired,
currentTicker: PropTypes.object,
fetchChannels: PropTypes.func.isRequired,
fetchBalance: PropTypes.func.isRequired,
fetchDescribeNetwork: PropTypes.func.isRequired,
children: PropTypes.object.isRequired
}

10
app/routes/app/components/App.scss

@ -1,8 +1,16 @@
@import '../../../variables.scss';
.content {
position: relative;
width: 80%;
height: 100vh;
display: inline-block;
vertical-align: top;
overflow-y: scroll;
}
.titleBar {
background: $spacegrey;
height: 20px;
-webkit-user-select: none;
-webkit-app-region: drag;
}

135
app/routes/app/containers/AppContainer.js

@ -1,6 +1,8 @@
import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import { fetchTicker, setCurrency, tickerSelectors } from 'reducers/ticker'
import { newAddress } from 'reducers/address'
import { fetchInfo } from 'reducers/info'
@ -8,23 +10,54 @@ import { fetchInfo } from 'reducers/info'
import { showModal, hideModal } from 'reducers/modal'
import { setFormType } from 'reducers/form'
import { setPayAmount, setPayInput, updatePayErrors, payFormSelectors } from 'reducers/payform'
import { setRequestAmount, setRequestMemo } from 'reducers/requestform'
import { sendCoins } from 'reducers/transaction'
import { payInvoice } from 'reducers/payment'
import { createInvoice, fetchInvoice } from 'reducers/invoice'
import { fetchBlockHeight, lndSelectors } from 'reducers/lnd'
import { clearError } from 'reducers/error'
import {
fetchChannels,
openChannel,
closeChannel,
channelsSelectors,
currentChannels,
toggleFilterPulldown,
changeFilter,
updateChannelSearchQuery,
openContactModal,
closeContactModal
} from 'reducers/channels'
import {
openContactsForm,
closeContactsForm,
updateContactFormSearchQuery,
updateManualFormSearchQuery,
updateContactCapacity,
contactFormSelectors,
updateManualFormErrors
} from 'reducers/contactsform'
import { fetchBalance } from 'reducers/balance'
import { fetchDescribeNetwork } from 'reducers/network'
import { clearError } from 'reducers/error'
import App from '../components/App'
const mapDispatchToProps = {
fetchTicker,
setCurrency,
newAddress,
fetchInfo,
@ -48,7 +81,28 @@ const mapDispatchToProps = {
fetchInvoice,
fetchBlockHeight,
clearError
clearError,
fetchBalance,
fetchChannels,
openChannel,
closeChannel,
toggleFilterPulldown,
changeFilter,
updateChannelSearchQuery,
openContactModal,
closeContactModal,
openContactsForm,
closeContactsForm,
updateContactFormSearchQuery,
updateManualFormSearchQuery,
updateContactCapacity,
contactFormSelectors,
updateManualFormErrors,
fetchDescribeNetwork
}
const mapStateToProps = state => ({
@ -59,6 +113,10 @@ const mapStateToProps = state => ({
info: state.info,
payment: state.payment,
transaction: state.transaction,
peers: state.peers,
channels: state.channels,
contactsform: state.contactsform,
balance: state.balance,
form: state.form,
payform: state.payform,
@ -69,6 +127,8 @@ const mapStateToProps = state => ({
error: state.error,
network: state.network,
currentTicker: tickerSelectors.currentTicker(state),
isOnchain: payFormSelectors.isOnchain(state),
isLn: payFormSelectors.isLn(state),
@ -76,7 +136,18 @@ const mapStateToProps = state => ({
inputCaption: payFormSelectors.inputCaption(state),
showPayLoadingScreen: payFormSelectors.showPayLoadingScreen(state),
payFormIsValid: payFormSelectors.payFormIsValid(state),
syncPercentage: lndSelectors.syncPercentage(state)
syncPercentage: lndSelectors.syncPercentage(state),
filteredNetworkNodes: contactFormSelectors.filteredNetworkNodes(state),
showManualForm: contactFormSelectors.showManualForm(state),
manualFormIsValid: contactFormSelectors.manualFormIsValid(state),
currentChannels: currentChannels(state),
activeChannelPubkeys: channelsSelectors.activeChannelPubkeys(state),
nonActiveChannelPubkeys: channelsSelectors.nonActiveChannelPubkeys(state),
pendingOpenChannelPubkeys: channelsSelectors.pendingOpenChannelPubkeys(state),
nonActiveFilters: channelsSelectors.nonActiveFilters(state),
channelNodes: channelsSelectors.channelNodes(state)
})
const mergeProps = (stateProps, dispatchProps, ownProps) => {
@ -172,20 +243,70 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
return {}
}
const networkTabProps = {
currentChannels: stateProps.currentChannels,
channels: stateProps.channels,
balance: stateProps.balance,
currentTicker: stateProps.currentTicker,
contactsform: stateProps.contactsform,
nodes: stateProps.network.nodes,
nonActiveFilters: stateProps.nonActiveFilters,
fetchChannels: dispatchProps.fetchChannels,
openContactsForm: dispatchProps.openContactsForm,
contactFormSelectors: dispatchProps.contactFormSelectors,
updateManualFormError: dispatchProps.updateManualFormErrors,
toggleFilterPulldown: dispatchProps.toggleFilterPulldown,
changeFilter: dispatchProps.changeFilter,
updateChannelSearchQuery: dispatchProps.updateChannelSearchQuery,
openContactModal: dispatchProps.openContactModal
}
const contactsFormProps = {
closeContactsForm: dispatchProps.closeContactsForm,
updateContactFormSearchQuery: dispatchProps.updateContactFormSearchQuery,
updateManualFormSearchQuery: dispatchProps.updateManualFormSearchQuery,
updateContactCapacity: dispatchProps.updateContactCapacity,
openChannel: dispatchProps.openChannel,
updateManualFormErrors: dispatchProps.updateManualFormErrors,
contactsform: stateProps.contactsform,
filteredNetworkNodes: stateProps.filteredNetworkNodes,
loadingChannelPubkeys: stateProps.channels.loadingChannelPubkeys,
showManualForm: stateProps.showManualForm,
manualFormIsValid: stateProps.manualFormIsValid,
activeChannelPubkeys: stateProps.activeChannelPubkeys,
nonActiveChannelPubkeys: stateProps.nonActiveChannelPubkeys,
pendingOpenChannelPubkeys: stateProps.pendingOpenChannelPubkeys
}
const contactModalProps = {
closeContactModal: dispatchProps.closeContactModal,
closeChannel: dispatchProps.closeChannel,
isOpen: stateProps.channels.contactModal.isOpen,
channel: stateProps.channels.contactModal.channel,
channelNodes: stateProps.channelNodes,
closingChannelIds: stateProps.channels.closingChannelIds
}
return {
...stateProps,
...dispatchProps,
...ownProps,
// props for the network sidebar
networkTabProps,
// props for the contacts form
contactsFormProps,
// props for the contact modal
contactModalProps,
// Props to pass to the pay form
formProps: formProps(stateProps.form.formType),
// action to open the pay form
openPayForm: () => dispatchProps.setFormType('PAY_FORM'),
// action to open the request form
openRequestForm: () => dispatchProps.setFormType('REQUEST_FORM'),
// action to close form
closeForm: () => dispatchProps.setFormType(null)
}
}

195
app/routes/contacts/components/Contacts.js

@ -1,195 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Isvg from 'react-inlinesvg'
import { MdSearch } from 'react-icons/lib/md'
import { FaAngleDown, FaRepeat } from 'react-icons/lib/fa'
import ContactModal from 'components/Contacts/ContactModal'
import ContactsForm from 'components/Contacts/ContactsForm'
import OnlineContact from 'components/Contacts/OnlineContact'
import PendingContact from 'components/Contacts/PendingContact'
import ClosingContact from 'components/Contacts/ClosingContact'
import OfflineContact from 'components/Contacts/OfflineContact'
import LoadingContact from 'components/Contacts/LoadingContact'
import plus from 'icons/plus.svg'
import styles from './Contacts.scss'
class Contacts extends Component {
constructor(props) {
super(props)
this.state = {
refreshing: false
}
}
componentWillMount() {
const { fetchChannels, fetchPeers, fetchDescribeNetwork } = this.props
fetchChannels()
fetchPeers()
fetchDescribeNetwork()
}
render() {
const {
channels: {
searchQuery,
filterPulldown,
filter,
loadingChannelPubkeys,
closingChannelIds
},
currentChannels,
activeChannels,
fetchChannels,
updateChannelSearchQuery,
toggleFilterPulldown,
changeFilter,
nonActiveFilters,
openContactsForm,
openContactModal,
contactModalProps,
contactsFormProps
} = this.props
const refreshClicked = () => {
// turn the spinner on
this.setState({ refreshing: true })
// store event in icon so we dont get an error when react clears it
const icon = this.repeat.childNodes
// fetch channels
fetchChannels()
// wait for the svg to appear as child
const svgTimeout = setTimeout(() => {
if (icon[0].tagName === 'svg') {
// spin icon for 1 sec
icon[0].style.animation = 'spin 1000ms linear 1'
clearTimeout(svgTimeout)
}
}, 1)
// clear animation after the second so we can reuse it
const refreshTimeout = setTimeout(() => {
icon[0].style.animation = ''
this.setState({ refreshing: false })
clearTimeout(refreshTimeout)
}, 1000)
}
return (
<div className={styles.friendsContainer}>
<ContactModal {...contactModalProps} />
<ContactsForm {...contactsFormProps} />
<header className={styles.header}>
<div className={styles.titleContainer}>
<div className={styles.left}>
<h1>Contacts <span>({activeChannels.length} online)</span></h1>
</div>
</div>
<div className={styles.newFriendContainer}>
<div className={`buttonPrimary ${styles.newFriendButton}`} onClick={openContactsForm}>
<Isvg src={plus} />
<span>Add</span>
</div>
</div>
</header>
<div className={styles.search}>
<label className={`${styles.label} ${styles.input}`} htmlFor='channelSearch'>
<MdSearch />
</label>
<input
value={searchQuery}
onChange={event => updateChannelSearchQuery(event.target.value)}
className={`${styles.text} ${styles.input}`}
placeholder='Search your contacts list...'
type='text'
id='channelSearch'
/>
</div>
<div className={styles.filtersContainer}>
<section>
<h2 onClick={toggleFilterPulldown} className={styles.filterTitle}>
{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>
))
}
</ul>
</section>
<section className={styles.refreshContainer}>
<span className={styles.refresh} onClick={refreshClicked} ref={(ref) => { this.repeat = ref }}>
{
this.state.refreshing ?
<FaRepeat />
:
'Refresh'
}
</span>
</section>
</div>
<ul className={`${styles.friends} ${filterPulldown && styles.fade}`}>
{
loadingChannelPubkeys.map(pubkey => <LoadingContact pubkey={pubkey} isClosing={false} key={pubkey} />)
}
{
currentChannels.length > 0 && currentChannels.map((channel, index) => {
if (closingChannelIds.includes(channel.chan_id)) {
return <LoadingContact pubkey={channel.remote_pubkey} isClosing key={index} />
} else if (Object.prototype.hasOwnProperty.call(channel, 'blocks_till_open')) {
return <PendingContact channel={channel} key={index} />
} else if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) {
return <ClosingContact channel={channel} key={index} />
} else if (!channel.active) {
return <OfflineContact channel={channel} key={index} openContactModal={openContactModal} />
}
return <OnlineContact channel={channel} key={index} openContactModal={openContactModal} />
})
}
</ul>
</div>
)
}
}
Contacts.propTypes = {
fetchPeers: PropTypes.func.isRequired,
fetchDescribeNetwork: PropTypes.func.isRequired,
channels: PropTypes.object.isRequired,
currentChannels: PropTypes.array.isRequired,
activeChannels: PropTypes.array.isRequired,
fetchChannels: PropTypes.func.isRequired,
updateChannelSearchQuery: PropTypes.func.isRequired,
toggleFilterPulldown: PropTypes.func.isRequired,
changeFilter: PropTypes.func.isRequired,
nonActiveFilters: PropTypes.array.isRequired,
openContactsForm: PropTypes.func.isRequired,
openContactModal: PropTypes.func.isRequired,
contactModalProps: PropTypes.object.isRequired,
contactsFormProps: PropTypes.object.isRequired
}
export default Contacts

172
app/routes/contacts/components/Contacts.scss

@ -1,172 +0,0 @@
@import '../../../variables.scss';
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
background: $lightgrey;
.titleContainer {
padding: 20px 40px;
.left {
padding: 10px 0;
h1 {
text-transform: uppercase;
font-size: 26px;
margin-right: 5px;
span {
display: inline-block;
vertical-align: middle;
font-size: 16px;
}
}
}
}
.newFriendContainer {
padding: 20px 40px;
.newFriendButton {
box-shadow: none;
transition: all 0.25s;
padding-top: 12px;
padding-bottom: 10px;
font-size: 14px;
&:hover {
background: darken($main, 10%);
}
span {
display: inline-block;
vertical-align: top;
&:nth-child(1) svg {
width: 14px;
height: 14px;
margin-right: 5px;
}
}
}
}
}
.search {
height: 55px;
padding: 2px 25px;
border-top: 1px solid $darkgrey;
border-bottom: 1px solid $darkgrey;
background: $white;
.input {
display: inline-block;
vertical-align: top;
height: 100%;
}
.label {
width: 5%;
line-height: 50px;
font-size: 20px;
text-align: center;
cursor: pointer;
}
.text {
width: 95%;
outline: 0;
padding: 0;
border: 0;
border-radius: 0;
height: 50px;
font-size: 16px;
}
}
.filtersContainer {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 20px;
padding: 20px 40px;
h2, h2 span {
color: $bluegrey;
cursor: pointer;
transition: color 0.25s;
&:hover {
color: lighten($bluegrey, 10%);
}
}
h2, .filters li {
text-transform: uppercase;
letter-spacing: 1.5px;
color: $darkestgrey;
font-size: 14px;
font-weight: 400;
}
h2 span.pulldown {
color: $main;
}
.filters {
display: none;
&.active {
display: block;
position: absolute;
bottom: -100px;
z-index: 10;
li {
margin: 5px 0;
cursor: pointer;
&:hover {
color: $main;
}
}
}
}
.refreshContainer {
text-align: right;
cursor: pointer;
.refresh {
cursor: pointer;
color: $darkestgrey;
transition: all 0.25s;
&:hover {
color: $main;
}
svg {
font-size: 12px;
color: $darkestgrey;
&:hover {
color: $darkestgrey;
}
}
}
}
}
.friends {
padding: 10px 0 60px 0;
opacity: 1;
transition: all 0.25s;
&.fade {
opacity: 0.05;
}
}

109
app/routes/contacts/containers/ContactsContainer.js

@ -1,109 +0,0 @@
import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import {
fetchChannels,
openChannel,
closeChannel,
updateChannelSearchQuery,
toggleFilterPulldown,
changeFilter,
openContactModal,
closeContactModal,
currentChannels,
channelsSelectors
} from 'reducers/channels'
import { fetchPeers } from 'reducers/peers'
import { fetchDescribeNetwork } from 'reducers/network'
import {
openContactsForm,
closeContactsForm,
updateContactFormSearchQuery,
updateContactCapacity,
contactFormSelectors
} from 'reducers/contactsform'
import Contacts from '../components/Contacts'
const mapDispatchToProps = {
openContactsForm,
closeContactsForm,
openContactModal,
closeContactModal,
updateContactFormSearchQuery,
updateContactCapacity,
openChannel,
closeChannel,
updateChannelSearchQuery,
toggleFilterPulldown,
changeFilter,
fetchChannels,
fetchPeers,
fetchDescribeNetwork
}
const mapStateToProps = state => ({
channels: state.channels,
peers: state.peers,
network: state.network,
contactsform: state.contactsform,
currentChannels: currentChannels(state),
activeChannels: channelsSelectors.activeChannels(state),
activeChannelPubkeys: channelsSelectors.activeChannelPubkeys(state),
nonActiveChannels: channelsSelectors.nonActiveChannels(state),
nonActiveChannelPubkeys: channelsSelectors.nonActiveChannelPubkeys(state),
pendingOpenChannels: channelsSelectors.pendingOpenChannels(state),
pendingOpenChannelPubkeys: channelsSelectors.pendingOpenChannelPubkeys(state),
closingPendingChannels: channelsSelectors.closingPendingChannels(state),
nonActiveFilters: channelsSelectors.nonActiveFilters(state),
channelNodes: channelsSelectors.channelNodes(state),
filteredNetworkNodes: contactFormSelectors.filteredNetworkNodes(state),
showManualForm: contactFormSelectors.showManualForm(state)
})
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const contactModalProps = {
closeContactModal: dispatchProps.closeContactModal,
closeChannel: dispatchProps.closeChannel,
isOpen: stateProps.channels.contactModal.isOpen,
channel: stateProps.channels.contactModal.channel,
channelNodes: stateProps.channelNodes,
closingChannelIds: stateProps.channels.closingChannelIds
}
const contactsFormProps = {
closeContactsForm: dispatchProps.closeContactsForm,
updateContactFormSearchQuery: dispatchProps.updateContactFormSearchQuery,
updateContactCapacity: dispatchProps.updateContactCapacity,
openChannel: dispatchProps.openChannel,
contactsform: stateProps.contactsform,
filteredNetworkNodes: stateProps.filteredNetworkNodes,
loadingChannelPubkeys: stateProps.channels.loadingChannelPubkeys,
showManualForm: stateProps.showManualForm,
activeChannelPubkeys: stateProps.activeChannelPubkeys,
nonActiveChannelPubkeys: stateProps.nonActiveChannelPubkeys,
pendingOpenChannelPubkeys: stateProps.pendingOpenChannelPubkeys
}
return {
...stateProps,
...dispatchProps,
...ownProps,
contactModalProps,
contactsFormProps
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Contacts))

3
app/routes/contacts/index.js

@ -1,3 +0,0 @@
import ContactsContainer from './containers/ContactsContainer'
export default ContactsContainer

168
app/routes/network/components/Network.js

@ -1,168 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import CanvasNetworkGraph from 'components/Network/CanvasNetworkGraph'
import PeersList from 'components/Network/PeersList'
import ChannelsList from 'components/Network/ChannelsList'
import TransactionForm from 'components/Network/TransactionForm'
import styles from './Network.scss'
class Network extends Component {
componentWillMount() {
const { fetchDescribeNetwork, fetchPeers, fetchChannels } = this.props
fetchPeers()
fetchChannels()
fetchDescribeNetwork()
}
componentDidUpdate(prevProps) {
const {
payReqIsLn, network: { pay_req }, fetchInvoiceAndQueryRoutes, clearQueryRoutes
} = this.props
// If LN go retrieve invoice details
if ((prevProps.network.pay_req !== pay_req) && payReqIsLn) {
fetchInvoiceAndQueryRoutes(pay_req)
}
if (prevProps.payReqIsLn && !payReqIsLn) {
clearQueryRoutes()
}
}
componentWillUnmount() {
const {
clearQueryRoutes, resetPayReq, clearSelectedChannels, clearSelectedPeers
} = this.props
clearQueryRoutes()
resetPayReq()
clearSelectedChannels()
clearSelectedPeers()
}
render() {
const {
setCurrentTab,
updateSelectedPeers,
setCurrentRoute,
network,
selectedPeerPubkeys,
currentRouteChanIds,
peers: { peers },
activeChannels,
selectedChannelIds,
updateSelectedChannels,
updatePayReq,
identity_pubkey
} = this.props
const renderContent = () => {
switch (network.currentTab) {
case 1:
return <PeersList peers={peers} updateSelectedPeers={updateSelectedPeers} selectedPeerPubkeys={selectedPeerPubkeys} />
case 2:
return <ChannelsList channels={activeChannels} updateSelectedChannels={updateSelectedChannels} selectedChannelIds={selectedChannelIds} />
case 3:
return (
<TransactionForm
updatePayReq={updatePayReq}
pay_req={network.pay_req}
loadingRoutes={network.fetchingInvoiceAndQueryingRoutes}
payReqRoutes={network.payReqRoutes}
setCurrentRoute={setCurrentRoute}
currentRoute={network.currentRoute}
/>
)
default:
return <span />
}
}
return (
<div className={styles.container}>
{
!network.networkLoading &&
<section className={styles.stats}>
<span>{network.nodes.length} nodes</span>
<span>|</span>
<span>{network.edges.length} channels</span>
</section>
}
<CanvasNetworkGraph
className={styles.network}
network={network}
identity_pubkey={identity_pubkey}
selectedPeerPubkeys={selectedPeerPubkeys}
selectedChannelIds={selectedChannelIds}
currentRouteChanIds={currentRouteChanIds}
/>
<section className={styles.toolbox}>
<ul className={styles.tabs}>
<li
className={`${styles.tab} ${styles.peersTab} ${network.currentTab === 1 && styles.active}`}
onClick={() => setCurrentTab(1)}
>
Peers
</li>
<li
className={`${styles.tab} ${styles.channelsTab} ${network.currentTab === 2 && styles.active}`}
onClick={() => setCurrentTab(2)}
>
Channels
</li>
<li
className={`${styles.tab} ${styles.transactionsTab} ${network.currentTab === 3 && styles.active}`}
onClick={() => setCurrentTab(3)}
>
Transactions
</li>
</ul>
<div className={styles.content}>
{renderContent()}
</div>
</section>
</div>
)
}
}
Network.propTypes = {
fetchDescribeNetwork: PropTypes.func.isRequired,
fetchPeers: PropTypes.func.isRequired,
setCurrentTab: PropTypes.func.isRequired,
fetchChannels: PropTypes.func.isRequired,
fetchInvoiceAndQueryRoutes: PropTypes.func.isRequired,
clearQueryRoutes: PropTypes.func.isRequired,
resetPayReq: PropTypes.func.isRequired,
clearSelectedChannels: PropTypes.func.isRequired,
clearSelectedPeers: PropTypes.func.isRequired,
updateSelectedPeers: PropTypes.func.isRequired,
setCurrentRoute: PropTypes.func.isRequired,
updateSelectedChannels: PropTypes.func.isRequired,
updatePayReq: PropTypes.func.isRequired,
network: PropTypes.object.isRequired,
peers: PropTypes.object.isRequired,
selectedPeerPubkeys: PropTypes.array.isRequired,
currentRouteChanIds: PropTypes.array.isRequired,
activeChannels: PropTypes.array.isRequired,
selectedChannelIds: PropTypes.array.isRequired,
identity_pubkey: PropTypes.string.isRequired,
payReqIsLn: PropTypes.bool.isRequired
}
export default Network

99
app/routes/network/components/Network.scss

@ -1,99 +0,0 @@
@import '../../../variables.scss';
@keyframes dash {
to {
stroke-dashoffset: 1000;
}
}
@keyframes fadein {
0% { background: $white; }
50% { background: lighten($secondary, 50%); }
100% { background: $secondary; animation-fill-mode:forwards; }
}
.container {
width: 100%;
height: 100vh;
animation: fadein 0.5s;
animation-timing-function:linear;
animation-fill-mode:forwards;
animation-iteration-count: 1;
line.active {
opacity: 1;
stroke: green;
stroke-width: 5;
stroke-dasharray: 100;
animation: dash 2.5s infinite linear;
}
circle {
cursor: pointer;
}
.stats {
position: absolute;
top: 0;
right: 30%;
padding: 20px;
span {
color: $main;
margin: 0 2.5px;
line-height: 25px;
vertical-align: middle;
&:nth-child(2) {
font-size: 25px;
}
}
}
}
.network, .toolbox {
display: inline-block;
vertical-align: top;
height: 100vh;
}
.network {
width: 70%;
}
.toolbox {
width: 30%;
height: 100%;
background: #353535;
overflow-y: scroll;
}
.tabs {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding-top: 40px;
.tab {
color: $white;
text-align: center;
cursor: pointer;
width: 100%;
padding: 10px 0;
border-bottom: 1px solid #464646;
transition: all 0.5s;
&.peersTab:hover, &.peersTab.active {
border-bottom: 1px solid #588CF0;
}
&.channelsTab:hover, &.channelsTab.active {
border-bottom: 1px solid #88D4A2;
}
&.transactionsTab:hover, &.transactionsTab.active {
border-bottom: 1px solid #FFDC53;
}
}
}

62
app/routes/network/containers/NetworkContainer.js

@ -1,62 +0,0 @@
import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import {
fetchDescribeNetwork,
setCurrentTab,
updateSelectedPeers,
clearSelectedPeers,
updateSelectedChannels,
clearSelectedChannels,
setCurrentRoute,
updatePayReq,
resetPayReq,
fetchInvoiceAndQueryRoutes,
clearQueryRoutes,
networkSelectors
} from '../../../reducers/network'
import { fetchPeers } from '../../../reducers/peers'
import { fetchChannels, channelsSelectors } from '../../../reducers/channels'
import Network from '../components/Network'
const mapDispatchToProps = {
fetchDescribeNetwork,
setCurrentTab,
updateSelectedPeers,
clearSelectedPeers,
updatePayReq,
fetchInvoiceAndQueryRoutes,
setCurrentRoute,
clearQueryRoutes,
resetPayReq,
fetchPeers,
fetchChannels,
updateSelectedChannels,
clearSelectedChannels
}
const mapStateToProps = state => ({
network: state.network,
peers: state.peers,
identity_pubkey: state.info.data.identity_pubkey,
selectedPeerPubkeys: networkSelectors.selectedPeerPubkeys(state),
selectedChannelIds: networkSelectors.selectedChannelIds(state),
payReqIsLn: networkSelectors.payReqIsLn(state),
currentRouteChanIds: networkSelectors.currentRouteChanIds(state),
activeChannels: channelsSelectors.activeChannels(state)
})
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Network))

3
app/routes/network/index.js

@ -1,3 +0,0 @@
import NetworkContainer from './containers/NetworkContainer'
export default NetworkContainer

57
app/rpc.proto

@ -202,7 +202,7 @@ service Lightning {
workflow and is waiting for confirmations for the funding txn, or is in the
process of closure, either initiated cooperatively or non-cooperatively.
*/
rpc PendingChannels (PendingChannelRequest) returns (PendingChannelResponse) {
rpc PendingChannels (PendingChannelsRequest) returns (PendingChannelsResponse) {
option (google.api.http) = {
get: "/v1/channels/pending"
};
@ -423,11 +423,6 @@ service Lightning {
*/
rpc SubscribeChannelGraph(GraphTopologySubscription) returns (stream GraphTopologyUpdate);
/**
SetAlias sets the alias for this node; e.g. "alice"
*/
rpc SetAlias(SetAliasRequest) returns (SetAliasResponse);
/** lncli: `debuglevel`
DebugLevel allows a caller to programmatically set the logging verbosity of
lnd. The logging can be targeted according to a coarse daemon-wide logging
@ -446,11 +441,11 @@ service Lightning {
};
}
/** lncli: `updatefees`
UpdateFees allows the caller to update the fee schedule for all channels
globally, or a particular channel.
/** lncli: `updatechanpolicy`
UpdateChannelPolicy allows the caller to update the fee schedule and
channel policies for all channels globally, or a particular channel.
*/
rpc UpdateFees(FeeUpdateRequest) returns (FeeUpdateResponse) {
rpc UpdateChannelPolicy(PolicyUpdateRequest) returns (PolicyUpdateResponse) {
option (google.api.http) = {
post: "/v1/fees"
body: "*"
@ -512,6 +507,9 @@ message SendRequest {
payment to the recipient.
*/
string payment_request = 6;
/// The CLTV delta from the current height that should be used to set the timelock for the final hop.
int32 final_cltv_delta = 7;
}
message SendResponse {
string payment_error = 1 [json_name = "payment_error"];
@ -798,13 +796,16 @@ message GetInfoResponse {
string block_hash = 8 [json_name = "block_hash"];
/// Whether the wallet's view is synced to the main chain
bool synced_to_chain = 9 [ json_name = "synced_to_chain" ];
bool synced_to_chain = 9 [json_name = "synced_to_chain"];
/// Whether the current node is connected to testnet
bool testnet = 10 [ json_name = "testnet" ];
bool testnet = 10 [json_name = "testnet"];
/// A list of active chains the node is connected to
repeated string chains = 11 [ json_name = "chains" ];
repeated string chains = 11 [json_name = "chains"];
/// The URIs of the current node.
repeated string uris = 12 [json_name = "uris"];
}
message ConfirmationUpdate {
@ -876,6 +877,12 @@ message OpenChannelRequest {
/// A manual fee rate set in sat/byte that should be used when crafting the closure transaction.
int64 sat_per_byte = 7;
/// Whether this channel should be private, not announced to the greater network.
bool private = 8 [json_name = "private"];
/// The minimum value in millisatoshi we will require for incoming HTLCs on the channel.
int64 min_htlc_msat = 9 [json_name = "min_htlc_msat"];
}
message OpenStatusUpdate {
oneof update {
@ -910,8 +917,8 @@ message PendingHTLC {
uint32 stage = 6 [ json_name = "stage" ];
}
message PendingChannelRequest {}
message PendingChannelResponse {
message PendingChannelsRequest {}
message PendingChannelsResponse {
message PendingChannel {
string remote_node_pub = 1 [ json_name = "remote_node_pub" ];
string channel_point = 2 [ json_name = "channel_point" ];
@ -929,9 +936,6 @@ message PendingChannelResponse {
/// The height at which this channel will be confirmed
uint32 confirmation_height = 2 [ json_name = "confirmation_height" ];
/// The number of blocks until this channel is open
int32 blocks_till_open = 3 [ json_name = "blocks_till_open" ];
/**
The amount calculated to be paid in fees for the current set of
commitment transactions. The fee amount is persisted with the channel
@ -1242,12 +1246,6 @@ message ClosedChannelUpdate {
ChannelPoint chan_point = 4;
}
message SetAliasRequest {
string new_alias = 1;
}
message SetAliasResponse {
}
message Invoice {
/**
An optional memo to attach along with the invoice. Used for record keeping
@ -1409,12 +1407,12 @@ message FeeReportResponse {
repeated ChannelFeeReport channel_fees = 1 [json_name = "channel_fees"];
}
message FeeUpdateRequest {
message PolicyUpdateRequest {
oneof scope {
/// If set, then this fee update applies to all currently active channels.
/// If set, then this update applies to all currently active channels.
bool global = 1 [json_name = "global"] ;
/// If set, this fee update will target a specific channel.
/// If set, this update will target a specific channel.
ChannelPoint chan_point = 2 [json_name = "chan_point"];
}
@ -1423,6 +1421,9 @@ message FeeUpdateRequest {
/// The effective fee rate in milli-satoshis. The precision of this value goes up to 6 decimal places, so 1e-6.
double fee_rate = 4 [json_name = "fee_rate"];
/// The required timelock delta for HTLCs forwarded over the channel.
uint32 time_lock_delta = 5 [json_name = "time_lock_delta"];
}
message FeeUpdateResponse {
message PolicyUpdateResponse {
}

3
app/utils/btc.js

@ -9,7 +9,8 @@ export function btcToSatoshis(btc) {
export function satoshisToBtc(satoshis) {
if (satoshis === undefined || satoshis === null || satoshis === '') return null
return sb.toBitcoin(satoshis).toFixed(8)
const btcAmount = sb.toBitcoin(satoshis).toFixed(8)
return btcAmount > 0 ? btcAmount : btcAmount * -1
}
export function btcToUsd(btc, price) {

8
app/variables.scss

@ -8,11 +8,17 @@ $traditionalgrey: #cccccc;
$lightgrey: #F7F7F7;
$darkgrey: #EBEBEB;
$darkestgrey: #999999;
$bluegrey: #555459;
$bluegrey: #2A2D38;
$spacegrey: #222E2B;
$spaceblue: #252832;
$darkspaceblue: #1c1e26;
$spaceborder: #404040;
$gold: #DEA326;
$green: #0bb634;
$terminalgreen: #00FF00;
$red: #FF556A;
$blue: #007bb6;
$orange: #FF8A65;
$yellow: #FFF680;
$curve: cubic-bezier(0.650, 0.000, 0.450, 1.000);

32
app/yarn.lock

@ -210,10 +210,14 @@ extend@~3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
extsprintf@1.3.0, extsprintf@^1.2.0:
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
extsprintf@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
fbjs@^0.8.9:
version "0.8.14"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.14.tgz#d1dbe2be254c35a91e09f31f9cd50a40b2a0ed1c"
@ -293,9 +297,9 @@ graceful-fs@^4.1.2:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
grpc@^1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.7.3.tgz#c9d034324e2ec8a06cfaa577a044a116f96c8c90"
grpc@1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.7.1.tgz#a1eecd074e78ffe5bb3bb64dcc7417d14fdb5cc4"
dependencies:
arguejs "^0.2.3"
lodash "^4.15.0"
@ -355,8 +359,8 @@ inherits@2, inherits@~2.0.0, inherits@~2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
ini@~1.3.0:
version "1.3.4"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
invert-kv@^1.0.0:
version "1.0.0"
@ -614,8 +618,8 @@ qs@~6.4.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
rc@^1.1.7:
version "1.2.1"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
version "1.2.4"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.4.tgz#a0f606caae2a3b862bbd0ef85482c0125b315fa3"
dependencies:
deep-extend "~0.4.0"
ini "~1.3.0"
@ -684,8 +688,8 @@ safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
semver@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
set-blocking@~2.0.0:
version "2.0.0"
@ -754,8 +758,8 @@ table-parser@^0.1.3:
connected-domain "^1.0.0"
tar-pack@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
version "3.4.1"
resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f"
dependencies:
debug "^2.2.0"
fstream "^1.0.10"
@ -803,8 +807,8 @@ util-deprecate@~1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
uuid@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
version "3.2.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
verror@1.10.0:
version "1.10.0"

12
package.json

@ -1,7 +1,7 @@
{
"name": "zap-desktop",
"productName": "ZapDesktop",
"version": "0.0.1",
"version": "0.1.1",
"description": "desktop application for the lightning network",
"scripts": {
"build": "concurrently \"npm run build-main\" \"npm run build-renderer\"",
@ -32,7 +32,7 @@
"test-watch": "npm test -- --watch",
"install-grpc": "cd app && npm run install-grpc"
},
"browserslist": "electron 1.7",
"browserslist": "electron 1.6",
"engines": {
"node": ">=8.0.0",
"npm": ">=5.0.0"
@ -65,6 +65,7 @@
]
},
"win": {
"icon": "./resources/icon.ico",
"target": [
"nsis"
]
@ -80,7 +81,10 @@
"output": "release"
},
"extraResources": [
"**/resources/bin/darwin/lnd"
{
"from": "./resources/bin/win32/lnd.exe",
"to": "./bin/lnd.exe"
}
]
},
"repository": {
@ -148,7 +152,6 @@
"cross-env": "^5.0.0",
"cross-spawn": "^5.1.0",
"css-loader": "^0.28.3",
"electron": "^1.7.10",
"electron-builder": "^19.49.2",
"electron-devtools-installer": "^2.2.1",
"enzyme": "^2.9.1",
@ -205,6 +208,7 @@
"d3-selection": "^1.2.0",
"d3-zoom": "^1.7.1",
"devtron": "^1.4.0",
"electron": "1.6.16",
"electron-debug": "^1.2.0",
"font-awesome": "^4.7.0",
"history": "^4.6.3",

BIN
resources/icon.icns

Binary file not shown.

BIN
resources/icon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

BIN
resources/icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

31
test/components/Nav.spec.js

@ -1,31 +0,0 @@
import React from 'react'
import { shallow } from 'enzyme'
import { NavLink } from 'react-router-dom'
import Nav from 'components/Nav'
const defaultProps = {
ticker: {
currency: 'usd',
crypto: 'btc'
},
balance: {},
setCurrency: () => {},
currentTicker: {},
openPayForm: () => {},
openRequestForm: () => {}
}
describe('default elements', () => {
const props = { ...defaultProps }
const el = shallow(<Nav {...props} />)
it('should render nav links', () => {
expect(el.find(NavLink).at(0).props().to).toBe('/')
expect(el.find(NavLink).at(1).props().to).toBe('/contacts')
expect(el.find(NavLink).at(2).props().to).toBe('/network')
})
it('should render buttons', () => {
expect(el.find('.button').at(0).text()).toContain('Pay')
expect(el.find('.button').at(1).text()).toContain('Request')
})
})

72
test/reducers/__snapshots__/channels.spec.js.snap

@ -19,29 +19,29 @@ Object {
},
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Online Contacts",
"name": "Online",
},
Object {
"key": "NON_ACTIVE_CHANNELS",
"name": "Offline Contacts",
"name": "Offline",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Pending Contacts",
"name": "Pending",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Contacts",
"name": "Closing",
},
],
"loadingChannelPubkeys": Array [],
@ -76,29 +76,29 @@ Object {
},
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Online Contacts",
"name": "Online",
},
Object {
"key": "NON_ACTIVE_CHANNELS",
"name": "Offline Contacts",
"name": "Offline",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Pending Contacts",
"name": "Pending",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Contacts",
"name": "Closing",
},
],
"loadingChannelPubkeys": Array [],
@ -136,29 +136,29 @@ Object {
},
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Online Contacts",
"name": "Online",
},
Object {
"key": "NON_ACTIVE_CHANNELS",
"name": "Offline Contacts",
"name": "Offline",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Pending Contacts",
"name": "Pending",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Contacts",
"name": "Closing",
},
],
"loadingChannelPubkeys": Array [],
@ -191,29 +191,29 @@ Object {
},
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Online Contacts",
"name": "Online",
},
Object {
"key": "NON_ACTIVE_CHANNELS",
"name": "Offline Contacts",
"name": "Offline",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Pending Contacts",
"name": "Pending",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Contacts",
"name": "Closing",
},
],
"loadingChannelPubkeys": Array [],
@ -248,29 +248,29 @@ Object {
},
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Online Contacts",
"name": "Online",
},
Object {
"key": "NON_ACTIVE_CHANNELS",
"name": "Offline Contacts",
"name": "Offline",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Pending Contacts",
"name": "Pending",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Contacts",
"name": "Closing",
},
],
"loadingChannelPubkeys": Array [],
@ -305,29 +305,29 @@ Object {
},
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Contacts",
"name": "All",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Online Contacts",
"name": "Online",
},
Object {
"key": "NON_ACTIVE_CHANNELS",
"name": "Offline Contacts",
"name": "Offline",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Pending Contacts",
"name": "Pending",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Contacts",
"name": "Closing",
},
],
"loadingChannelPubkeys": Array [],

296
yarn.lock

@ -31,8 +31,8 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.78.tgz#5d4a3f579c1524e01ee21bf474e6fba09198f470"
"@types/node@^7.0.18":
version "7.0.30"
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.30.tgz#209af44164286c2d648139ac6d53847ab0e1b5b8"
version "7.0.52"
resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.52.tgz#8990d3350375542b0c21a83cd0331e6a8fc86716"
JSONStream@^0.8.4:
version "0.8.4"
@ -120,6 +120,15 @@ ajv@^5.0.0:
co "^4.6.0"
json-stable-stringify "^1.0.1"
ajv@^5.1.0, ajv@^5.5.1:
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
dependencies:
co "^4.6.0"
fast-deep-equal "^1.0.0"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.3.0"
ajv@^5.1.5, ajv@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.0.tgz#c1735024c5da2ef75cc190713073d44f098bf486"
@ -129,15 +138,6 @@ ajv@^5.1.5, ajv@^5.2.0:
json-schema-traverse "^0.3.0"
json-stable-stringify "^1.0.1"
ajv@^5.5.1:
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
dependencies:
co "^4.6.0"
fast-deep-equal "^1.0.0"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.3.0"
align-text@^0.1.1, align-text@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
@ -427,7 +427,11 @@ aws-sign2@~0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
aws4@^1.2.1:
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
aws4@^1.2.1, aws4@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
@ -1626,6 +1630,18 @@ boom@2.x.x:
dependencies:
hoek "2.x.x"
boom@4.x.x:
version "4.3.1"
resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31"
dependencies:
hoek "4.x.x"
boom@5.x.x:
version "5.2.0"
resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02"
dependencies:
hoek "4.x.x"
boxen@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
@ -2315,7 +2331,7 @@ core-js@^2.4.0, core-js@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
core-util-is@~1.0.0:
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@ -2434,6 +2450,12 @@ cryptiles@2.x.x:
dependencies:
boom "2.x.x"
cryptiles@3.x.x:
version "3.1.2"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
dependencies:
boom "5.x.x"
crypto-browserify@^3.11.0:
version "3.11.0"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522"
@ -2721,12 +2743,18 @@ debug@2.6.7:
dependencies:
ms "2.0.0"
debug@2.6.8, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.4.5, debug@^2.6.0, debug@^2.6.3, debug@^2.6.8:
debug@2.6.8, debug@^2.1.1, debug@^2.4.5, debug@^2.6.0, debug@^2.6.3, debug@^2.6.8:
version "2.6.8"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
dependencies:
ms "2.0.0"
debug@2.6.9, debug@^2.1.3, debug@^2.2.0:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
ms "2.0.0"
debug@^3.0.0, debug@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
@ -3198,9 +3226,9 @@ electron-to-chromium@^1.3.30:
dependencies:
electron-releases "^2.1.0"
electron@^1.7.10:
version "1.7.10"
resolved "https://registry.yarnpkg.com/electron/-/electron-1.7.10.tgz#3a3e83d965fd7fafe473be8ddf8f472561b6253d"
electron@1.6.16:
version "1.6.16"
resolved "https://registry.yarnpkg.com/electron/-/electron-1.6.16.tgz#f37a8f49cc2625059c99eb266dee4dffe0917b63"
dependencies:
"@types/node" "^7.0.18"
electron-download "^3.0.1"
@ -3365,8 +3393,8 @@ es6-map@^0.1.3:
event-emitter "~0.3.5"
es6-promise@^4.0.5:
version "4.1.0"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.0.tgz#dda03ca8f9f89bc597e689842929de7ba8cebdf0"
version "4.2.4"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.4.tgz#dc4221c2b16518760bd8c39a52d8f356fc00ed29"
es6-set@~0.1.5:
version "0.1.5"
@ -3759,7 +3787,7 @@ express@^4.13.3, express@^4.15.2, express@^4.15.3:
utils-merge "1.0.0"
vary "~1.1.1"
extend@~3.0.0:
extend@~3.0.0, extend@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
@ -3786,7 +3814,16 @@ extract-text-webpack-plugin@^3.0.1:
schema-utils "^0.3.0"
webpack-sources "^1.0.1"
extract-zip@^1.0.3, extract-zip@^1.6.5:
extract-zip@^1.0.3:
version "1.6.6"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c"
dependencies:
concat-stream "1.6.0"
debug "2.6.9"
mkdirp "0.5.0"
yauzl "2.4.1"
extract-zip@^1.6.5:
version "1.6.5"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.5.tgz#99a06735b6ea20ea9b705d779acffcc87cff0440"
dependencies:
@ -3795,9 +3832,13 @@ extract-zip@^1.0.3, extract-zip@^1.6.5:
mkdirp "0.5.0"
yauzl "2.4.1"
extsprintf@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550"
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
extsprintf@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
fancy-log@^1.1.0:
version "1.3.0"
@ -4053,6 +4094,14 @@ form-data@~2.1.1:
combined-stream "^1.0.5"
mime-types "^2.1.12"
form-data@~2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf"
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.5"
mime-types "^2.1.12"
formatio@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb"
@ -4389,6 +4438,10 @@ har-schema@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
har-validator@~4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
@ -4396,6 +4449,13 @@ har-validator@~4.2.1:
ajv "^4.9.1"
har-schema "^1.0.5"
har-validator@~5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
dependencies:
ajv "^5.1.0"
har-schema "^2.0.0"
harmony-reflect@^1.4.6:
version "1.5.1"
resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.5.1.tgz#b54ca617b00cc8aef559bbb17b3d85431dc7e329"
@ -4457,6 +4517,15 @@ hawk@~3.1.3:
hoek "2.x.x"
sntp "1.x.x"
hawk@~6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038"
dependencies:
boom "4.x.x"
cryptiles "3.x.x"
hoek "4.x.x"
sntp "2.x.x"
he@1.1.x:
version "1.1.1"
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
@ -4497,6 +4566,10 @@ hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
hoek@4.x.x:
version "4.2.0"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
hoist-non-react-statics@^1.0.3, hoist-non-react-statics@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
@ -4512,11 +4585,7 @@ home-path@^1.0.1:
version "1.0.5"
resolved "https://registry.yarnpkg.com/home-path/-/home-path-1.0.5.tgz#788b29815b12d53bacf575648476e6f9041d133f"
hosted-git-info@^2.1.4:
version "2.4.2"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.4.2.tgz#0076b9f46a270506ddbaaea56496897460612a67"
hosted-git-info@^2.5.0:
hosted-git-info@^2.1.4, hosted-git-info@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
@ -4632,6 +4701,14 @@ http-signature@~1.1.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
dependencies:
assert-plus "^1.0.0"
jsprim "^1.2.2"
sshpk "^1.7.0"
httpplease@^0.16:
version "0.16.4"
resolved "https://registry.yarnpkg.com/httpplease/-/httpplease-0.16.4.tgz#d382ebe230ef5079080b4e9ffebf316a9e75c0da"
@ -4717,7 +4794,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1:
inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
@ -4725,11 +4802,11 @@ inherits@2.0.1, inherits@=2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
ini@^1.3.4, ini@~1.3.0:
ini@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
ini@^1.3.5:
ini@^1.3.5, ini@~1.3.0:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
@ -5517,13 +5594,13 @@ jsonparse@0.0.5:
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-0.0.5.tgz#330542ad3f0a654665b778f3eb2d9a9fa507ac64"
jsprim@^1.2.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.0.tgz#a3b87e40298d8c380552d8cc7628a0bb95a22918"
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
dependencies:
assert-plus "1.0.0"
extsprintf "1.0.2"
extsprintf "1.3.0"
json-schema "0.2.3"
verror "1.3.6"
verror "1.10.0"
jsx-ast-utils@^1.4.0:
version "1.4.1"
@ -6042,7 +6119,17 @@ miller-rabin@^4.0.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7:
mime-db@~1.30.0:
version "1.30.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.7:
version "2.1.17"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
dependencies:
mime-db "~1.30.0"
mime-types@~2.1.11, mime-types@~2.1.15:
version "2.1.15"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
dependencies:
@ -6359,16 +6446,7 @@ nopt@^4.0.1:
abbrev "1"
osenv "^0.1.4"
normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
version "2.3.8"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.8.tgz#d819eda2a9dedbd1ffa563ea4071d936782295bb"
dependencies:
hosted-git-info "^2.1.4"
is-builtin-module "^1.0.0"
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
normalize-package-data@^2.4.0:
normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
dependencies:
@ -6458,7 +6536,7 @@ number-is-nan@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.0.tgz#b4389362170e7ef9798c3c7716d80ebc0106fccf"
oauth-sign@~0.8.1:
oauth-sign@~0.8.1, oauth-sign@~0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
@ -6775,6 +6853,10 @@ performance-now@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
pify@^2.0.0, pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@ -7319,6 +7401,10 @@ qs@6.4.0, qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
qs@~6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
query-string@^4.1.0:
version "4.3.4"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
@ -7367,7 +7453,7 @@ range-parser@^1.0.3, range-parser@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
rc@^1.0.1, rc@^1.1.2, rc@^1.1.6, rc@^1.1.7, rc@^1.2.1:
rc@^1.0.1, rc@^1.1.6, rc@^1.1.7, rc@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
dependencies:
@ -7376,6 +7462,15 @@ rc@^1.0.1, rc@^1.1.2, rc@^1.1.6, rc@^1.1.7, rc@^1.2.1:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
rc@^1.1.2:
version "1.2.4"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.4.tgz#a0f606caae2a3b862bbd0ef85482c0125b315fa3"
dependencies:
deep-extend "~0.4.0"
ini "~1.3.0"
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-addons-test-utils@^15.6.0:
version "15.6.0"
resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.0.tgz#062d36117fe8d18f3ba5e06eb33383b0b85ea5b9"
@ -7581,7 +7676,7 @@ readable-stream@^1.0.33, readable-stream@~1.1.9:
isarray "0.0.1"
string_decoder "~0.10.x"
readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.6, readable-stream@^2.2.9:
readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.6, readable-stream@^2.2.9:
version "2.2.11"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.11.tgz#0796b31f8d7688007ff0b93a8088d34aa17c0f72"
dependencies:
@ -7593,6 +7688,18 @@ readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable
string_decoder "~1.0.0"
util-deprecate "~1.0.1"
readable-stream@^2.2.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~1.0.6"
safe-buffer "~5.1.1"
string_decoder "~1.0.3"
util-deprecate "~1.0.1"
readdirp@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
@ -7767,7 +7874,7 @@ request-promise-native@^1.0.3:
stealthy-require "^1.1.0"
tough-cookie ">=2.3.0"
request@2, request@^2.45.0, request@^2.65.0, request@^2.69.0, request@^2.79.0, request@^2.81.0, request@~2.81.0:
request@2, request@^2.65.0, request@^2.69.0, request@^2.79.0, request@^2.81.0, request@~2.81.0:
version "2.81.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
dependencies:
@ -7794,6 +7901,33 @@ request@2, request@^2.45.0, request@^2.65.0, request@^2.69.0, request@^2.79.0, r
tunnel-agent "^0.6.0"
uuid "^3.0.0"
request@^2.45.0:
version "2.83.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.6.0"
caseless "~0.12.0"
combined-stream "~1.0.5"
extend "~3.0.1"
forever-agent "~0.6.1"
form-data "~2.3.1"
har-validator "~5.0.3"
hawk "~6.0.2"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.17"
oauth-sign "~0.8.2"
performance-now "^2.1.0"
qs "~6.5.1"
safe-buffer "^5.1.1"
stringstream "~0.0.5"
tough-cookie "~2.3.3"
tunnel-agent "^0.6.0"
uuid "^3.1.0"
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@ -7868,12 +8002,18 @@ right-align@^0.1.1:
dependencies:
align-text "^0.1.1"
rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.6.0, rimraf@^2.6.1:
rimraf@2, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.6.0, rimraf@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
dependencies:
glob "^7.0.5"
rimraf@^2.2.8:
version "2.6.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
dependencies:
glob "^7.0.5"
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
@ -7905,7 +8045,11 @@ rx@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
safe-buffer@^5.0.1, safe-buffer@^5.1.0:
safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
safe-buffer@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.0.tgz#fe4c8460397f9eaaaa58e73be46273408a45e223"
@ -7997,7 +8141,11 @@ semver-diff@^2.0.0:
dependencies:
semver "^5.0.3"
"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@~5.3.0:
"semver@2 || 3 || 4 || 5", semver@^5.3.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
semver@^5.0.3, semver@^5.1.0, semver@~5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
@ -8148,6 +8296,12 @@ sntp@1.x.x:
dependencies:
hoek "2.x.x"
sntp@2.x.x:
version "2.1.0"
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8"
dependencies:
hoek "4.x.x"
sockjs-client@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.2.tgz#f0212a8550e4c9468c8cceaeefd2e3493c033ad5"
@ -8416,13 +8570,13 @@ string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
string_decoder@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.2.tgz#b29e1f4e1125fa97a10382b8a533737b7491e179"
string_decoder@~1.0.0, string_decoder@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
dependencies:
safe-buffer "~5.0.1"
safe-buffer "~5.1.0"
stringstream@~0.0.4:
stringstream@~0.0.4, stringstream@~0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
@ -8890,12 +9044,18 @@ toposort@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.3.tgz#f02cd8a74bd8be2fc0e98611c3bacb95a171869c"
tough-cookie@>=2.3.0, tough-cookie@^2.3.2, tough-cookie@~2.3.0:
tough-cookie@>=2.3.0, tough-cookie@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a"
dependencies:
punycode "^1.4.1"
tough-cookie@~2.3.0, tough-cookie@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
dependencies:
punycode "^1.4.1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
@ -9159,7 +9319,11 @@ uuid@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
uuid@^3.0.0, uuid@^3.0.1:
uuid@^3.0.0, uuid@^3.1.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
uuid@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"
@ -9190,11 +9354,13 @@ vendors@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22"
verror@1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c"
verror@1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
dependencies:
extsprintf "1.0.2"
assert-plus "^1.0.0"
core-util-is "1.0.2"
extsprintf "^1.2.0"
vinyl@^0.5.0:
version "0.5.3"

Loading…
Cancel
Save