Browse Source

Merge pull request #166 from LN-Zap/feature/friends

Feature/friends
renovate/lint-staged-8.x
JimmyMow 7 years ago
committed by GitHub
parent
commit
ccb67a927b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      .eslintrc
  2. 90
      app/components/ChannelForm/ChannelForm.js
  3. 57
      app/components/ChannelForm/ChannelForm.scss
  4. 44
      app/components/ChannelForm/Footer.js
  5. 17
      app/components/ChannelForm/Footer.scss
  6. 37
      app/components/ChannelForm/StepFour.js
  7. 36
      app/components/ChannelForm/StepFour.scss
  8. 75
      app/components/ChannelForm/StepOne.js
  9. 74
      app/components/ChannelForm/StepOne.scss
  10. 50
      app/components/ChannelForm/StepThree.js
  11. 58
      app/components/ChannelForm/StepThree.scss
  12. 49
      app/components/ChannelForm/StepTwo.js
  13. 58
      app/components/ChannelForm/StepTwo.scss
  14. 3
      app/components/ChannelForm/index.js
  15. 93
      app/components/Channels/Channel.js
  16. 125
      app/components/Channels/Channel.scss
  17. 127
      app/components/Channels/ChannelForm.js
  18. 123
      app/components/Channels/ChannelForm.scss
  19. 101
      app/components/Channels/ChannelModal.js
  20. 124
      app/components/Channels/ChannelModal.scss
  21. 140
      app/components/Channels/Channels.js
  22. 69
      app/components/Channels/Channels.scss
  23. 67
      app/components/Channels/ClosedPendingChannel.js
  24. 95
      app/components/Channels/ClosedPendingChannel.scss
  25. 70
      app/components/Channels/OpenPendingChannel.js
  26. 98
      app/components/Channels/OpenPendingChannel.scss
  27. 3
      app/components/Channels/index.js
  28. 39
      app/components/Contacts/ClosingContact.js
  29. 156
      app/components/Contacts/Contact.scss
  30. 134
      app/components/Contacts/ContactModal.js
  31. 152
      app/components/Contacts/ContactModal.scss
  32. 230
      app/components/Contacts/ContactsForm.js
  33. 239
      app/components/Contacts/ContactsForm.scss
  34. 35
      app/components/Contacts/LoadingContact.js
  35. 34
      app/components/Contacts/OfflineContact.js
  36. 34
      app/components/Contacts/OnlineContact.js
  37. 0
      app/components/Contacts/OnlineContact.scss
  38. 39
      app/components/Contacts/PendingContact.js
  39. 4
      app/components/Form/PayForm.js
  40. 6
      app/components/Form/RequestForm.js
  41. 6
      app/components/ModalRoot/ModalRoot.js
  42. 4
      app/components/ModalRoot/SuccessfulSendCoins.js
  43. 15
      app/components/Nav/Nav.js
  44. 16
      app/components/Nav/Nav.scss
  45. 1
      app/components/Network/CanvasNetworkGraph.js
  46. 4
      app/components/Network/TransactionForm.js
  47. 20
      app/components/Peers/Peer.js
  48. 38
      app/components/Peers/Peer.scss
  49. 71
      app/components/Peers/PeerForm.js
  50. 107
      app/components/Peers/PeerForm.scss
  51. 78
      app/components/Peers/PeerModal.js
  52. 75
      app/components/Peers/PeerModal.scss
  53. 4
      app/components/Wallet/ReceiveModal.js
  54. 2
      app/components/Wallet/Wallet.js
  55. 1
      app/icons/plus.svg
  56. 73
      app/lnd/methods/channelController.js
  57. 26
      app/lnd/methods/index.js
  58. 4
      app/lnd/methods/paymentsController.js
  59. 2
      app/lnd/methods/peersController.js
  60. 5
      app/lnd/subscribe/channelgraph.js
  61. 2
      app/main.dev.js
  62. 4
      app/reducers/balance.js
  63. 239
      app/reducers/channels.js
  64. 99
      app/reducers/contactsform.js
  65. 3
      app/reducers/index.js
  66. 2
      app/reducers/invoice.js
  67. 4
      app/reducers/lnd.js
  68. 4
      app/reducers/network.js
  69. 8
      app/reducers/peers.js
  70. 4
      app/reducers/ticker.js
  71. 4
      app/reducers/transaction.js
  72. 6
      app/routes.js
  73. 4
      app/routes/activity/components/Activity.js
  74. 4
      app/routes/activity/components/components/Invoice/Invoice.js
  75. 4
      app/routes/activity/components/components/Modal/Modal.js
  76. 4
      app/routes/activity/components/components/Payment/Payment.js
  77. 4
      app/routes/activity/components/components/Transaction/Transaction.js
  78. 104
      app/routes/channels/containers/ChannelsContainer.js
  79. 3
      app/routes/channels/index.js
  80. 148
      app/routes/contacts/components/Contacts.js
  81. 144
      app/routes/contacts/components/Contacts.scss
  82. 109
      app/routes/contacts/containers/ContactsContainer.js
  83. 3
      app/routes/contacts/index.js
  84. 8
      app/routes/network/components/Network.js
  85. 137
      app/routes/peers/components/Peers.js
  86. 81
      app/routes/peers/components/Peers.scss
  87. 52
      app/routes/peers/containers/PeersContainer.js
  88. 3
      app/routes/peers/index.js
  89. 3
      app/store/configureStore.dev.js
  90. 3
      app/variables.scss
  91. 8
      internals/scripts/CheckBuiltsExist.js
  92. 125
      test/components/Channels.spec.js
  93. 4
      test/components/Nav.spec.js
  94. 72
      test/components/Peers.spec.js
  95. 120
      test/reducers/__snapshots__/channels.spec.js.snap

8
.eslintrc

@ -17,6 +17,14 @@
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
"jsx-a11y/no-static-element-interactions": 0, "jsx-a11y/no-static-element-interactions": 0,
"jsx-a11y/no-noninteractive-element-interactions": 0, "jsx-a11y/no-noninteractive-element-interactions": 0,
"jsx-a11y/click-events-have-key-events": 0,
"jsx-a11y/label-has-for": [ 2, {
"components": [ "Label" ],
"required": {
"every": [ "id" ]
},
"allowChildren": false
}],
"react/no-array-index-key": 0, "react/no-array-index-key": 0,
"react/forbid-prop-types": 0, "react/forbid-prop-types": 0,
"camelcase": 0, "camelcase": 0,

90
app/components/ChannelForm/ChannelForm.js

@ -1,90 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactModal from 'react-modal'
import { FaClose } from 'react-icons/lib/fa'
import StepOne from './StepOne'
import StepTwo from './StepTwo'
import StepThree from './StepThree'
import StepFour from './StepFour'
import Footer from './Footer'
import styles from './ChannelForm.scss'
const ChannelForm = ({
channelform,
openChannel,
closeChannelForm,
changeStep,
setNodeKey,
setLocalAmount,
setPushAmount,
channelFormHeader,
channelFormProgress,
stepTwoIsValid,
peers
}) => {
const renderStep = () => {
const { step } = channelform
switch (step) {
case 1:
return <StepOne peers={peers} changeStep={changeStep} setNodeKey={setNodeKey} />
case 2:
return <StepTwo local_amt={channelform.local_amt} setLocalAmount={setLocalAmount} />
case 3:
return <StepThree push_amt={channelform.push_amt} setPushAmount={setPushAmount} />
default:
return <StepFour node_key={channelform.node_key} local_amt={channelform.local_amt} push_amt={channelform.push_amt} />
}
}
return (
<ReactModal
isOpen={channelform.isOpen}
ariaHideApp
shouldCloseOnOverlayClick
contentLabel='No Overlay Click Modal'
onRequestClose={closeChannelForm}
parentSelector={() => document.body}
className={styles.modal}
>
<div onClick={closeChannelForm} className={styles.modalClose}>
<FaClose />
</div>
<header className={styles.header}>
<h3>{channelFormHeader}</h3>
<div className={styles.progress} style={{ width: `${channelFormProgress}%` }} />
</header>
<div className={styles.content}>
{renderStep()}
</div>
<Footer
step={channelform.step}
changeStep={changeStep}
stepTwoIsValid={stepTwoIsValid}
submit={() => openChannel({ pubkey: channelform.node_key, local_amt: channelform.local_amt, push_amt: channelform.push_amt })}
/>
</ReactModal>
)
}
ChannelForm.propTypes = {
channelform: PropTypes.object.isRequired,
openChannel: PropTypes.func.isRequired,
closeChannelForm: PropTypes.func.isRequired,
changeStep: PropTypes.func.isRequired,
setNodeKey: PropTypes.func.isRequired,
setLocalAmount: PropTypes.func.isRequired,
setPushAmount: PropTypes.func.isRequired,
channelFormHeader: PropTypes.string.isRequired,
channelFormProgress: PropTypes.number.isRequired,
stepTwoIsValid: PropTypes.bool.isRequired,
peers: PropTypes.array.isRequired
}
export default ChannelForm

57
app/components/ChannelForm/ChannelForm.scss

@ -1,57 +0,0 @@
@import '../../variables.scss';
.modal {
width: 40%;
margin: 50px auto;
position: absolute;
top: auto;
left: 20%;
right: 0;
bottom: auto;
background: $white;
outline: none;
z-index: -2;
border: 1px solid $darkgrey;
}
.modalClose {
position: absolute;
top: -13px;
right: -13px;
display: block;
font-size: 16px;
line-height: 27px;
width: 32px;
height: 32px;
background: $white;
border-radius: 50%;
color: $darkestgrey;
cursor: pointer;
text-align: center;
z-index: 2;
transition: all 0.25s;
}
.modalClose:hover {
background: $darkgrey;
}
.header {
padding: 20px;
background: $lightgrey;
text-align: center;
font-family: 'Jigsaw Light';
text-transform: uppercase;
position: relative;
z-index: -2;
}
.progress {
transition: all 0.2s ease;
background: $main;
position: absolute;
height: 100%;
top: 0;
left: 0;
z-index: -1;
}

44
app/components/ChannelForm/Footer.js

@ -1,44 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './Footer.scss'
const Footer = ({ step, changeStep, stepTwoIsValid, submit }) => {
if (step === 1) { return null }
// See if the next button on step 2 should be active
const nextIsInactive = step === 2 && !stepTwoIsValid
// Function that's called when the user clicks "next" in the form
const nextFunc = () => {
if (nextIsInactive) { return }
changeStep(step + 1)
}
const rightButtonText = step === 4 ? 'Submit' : 'Next'
const rightButtonOnClick = step === 4 ? () => submit() : nextFunc
return (
<div className={styles.footer}>
<div className='buttonContainer'>
<div className='buttonPrimary' onClick={() => changeStep(step - 1)}>
Back
</div>
</div>
<div className='buttonContainer' onClick={rightButtonOnClick}>
<div className={`buttonPrimary ${nextIsInactive && 'inactive'}`}>
{rightButtonText}
</div>
</div>
</div>
)
}
Footer.propTypes = {
step: PropTypes.number.isRequired,
changeStep: PropTypes.func.isRequired,
stepTwoIsValid: PropTypes.bool.isRequired,
submit: PropTypes.func.isRequired
}
export default Footer

17
app/components/ChannelForm/Footer.scss

@ -1,17 +0,0 @@
@import '../../variables.scss';
.footer {
display: flex;
flex-direction: row;
justify-content: space-between;
padding-bottom: 30px;
div {
margin: 0 20px;
div {
padding: 18px 60px 15px 60px;
color: $black;
}
}
}

37
app/components/ChannelForm/StepFour.js

@ -1,37 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './StepFour.scss'
const StepFour = ({ node_key, local_amt, push_amt }) => (
<div className={styles.container}>
<div className={styles.nodekey}>
<h4>Peer</h4>
<h2>{node_key}</h2>
</div>
<div className={styles.amounts}>
<div className={styles.localamt}>
<h4>Local Amount</h4>
<h3>{local_amt}</h3>
</div>
<div className={styles.pushamt}>
<h4>Push Amount</h4>
<h3>{push_amt}</h3>
</div>
</div>
</div>
)
StepFour.propTypes = {
node_key: PropTypes.string.isRequired,
local_amt: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]),
push_amt: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
])
}
export default StepFour

36
app/components/ChannelForm/StepFour.scss

@ -1,36 +0,0 @@
@import '../../variables.scss';
.container {
padding: 50px;
h4 {
text-transform: uppercase;
font-size: 14px;
margin-bottom: 10px;
}
h3 {
text-align: center;
color: $main;
font-size: 50px;
}
}
.nodekey {
margin-bottom: 50px;
padding: 20px;
border-bottom: 1px solid $main;
h2 {
font-size: 12px;
font-weight: bold;
}
}
.amounts {
display: flex;
flex-direction: row;
justify-content: space-around;
margin-bottom: 50px;
padding: 20px;
}

75
app/components/ChannelForm/StepOne.js

@ -1,75 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { MdSearch } from 'react-icons/lib/md'
import styles from './StepOne.scss'
class StepOne extends Component {
constructor(props) {
super(props)
this.state = {
peers: props.peers,
searchQuery: ''
}
this.onSearchQuery = this.onSearchQuery.bind(this)
this.peerClicked = this.peerClicked.bind(this)
}
onSearchQuery(searchQuery) {
const peers = this.props.peers.filter(peer => peer.pub_key.includes(searchQuery))
this.setState({ peers, searchQuery })
}
peerClicked(peer) {
const { setNodeKey, changeStep } = this.props
setNodeKey(peer.pub_key)
changeStep(2)
}
render() {
const { peers, searchQuery } = this.state
return (
<div>
<div className={styles.search}>
<label className={`${styles.label} ${styles.input}`} htmlFor='peersSearch'>
<MdSearch />
</label>
<input
value={searchQuery}
onChange={event => this.onSearchQuery(event.target.value)}
className={`${styles.text} ${styles.input}`}
placeholder='Search your peers by their public key'
type='text'
id='peersSearch'
/>
</div>
<ul className={styles.peers}>
{peers.length > 0 &&
peers.map(peer => (
<li
key={peer.peer_id}
className={styles.peer}
onClick={() => this.peerClicked(peer)}
>
<h4>{peer.address}</h4>
<h1>{peer.pub_key}</h1>
</li>
)
)}
</ul>
</div>
)
}
}
StepOne.propTypes = {
peers: PropTypes.array.isRequired,
setNodeKey: PropTypes.func.isRequired,
changeStep: PropTypes.func.isRequired
}
export default StepOne

74
app/components/ChannelForm/StepOne.scss

@ -1,74 +0,0 @@
@import '../../variables.scss';
.peers {
h2 {
text-transform: uppercase;
font-weight: 200;
padding: 10px 0;
border-bottom: 1px solid $grey;
color: $darkestgrey;
}
}
.search {
height: 50px;
padding: 2px;
border-bottom: 1px solid $darkgrey;
.input {
display: inline-block;
vertical-align: top;
height: 100%;
}
.label {
width: 5%;
line-height: 50px;
font-size: 16px;
text-align: center;
cursor: pointer;
}
.text {
width: 95%;
outline: 0;
padding: 0;
border: 0;
border-radius: 0;
height: 50px;
font-size: 16px;
}
}
.peer {
position: relative;
background: $white;
padding: 10px;
border-top: 1px solid $grey;
cursor: pointer;
transition: all 0.25s;
&:hover {
opacity: 0.5;
}
&:first-child {
border: none;
}
h4, h1 {
margin: 10px 0;
}
h4 {
font-size: 12px;
font-weight: bold;
color: $black;
}
h1 {
font-size: 14px;
font-weight: 200;
color: $main;
}
}

50
app/components/ChannelForm/StepThree.js

@ -1,50 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import CurrencyIcon from 'components/CurrencyIcon'
import styles from './StepThree.scss'
class StepThree extends Component {
render() {
const { push_amt, setPushAmount } = this.props
return (
<div className={styles.container}>
<div className={styles.explainer}>
<h2>Push Amount</h2>
<p>
The push amount is the amount of bitcoin (if any at all) you&apos;d like
to &quot;push&quot; to the other side of the channel when it opens.
This amount will be set on the remote side of the channel as part of the initial commitment state.
</p>
</div>
<form>
<label htmlFor='amount'>
<CurrencyIcon currency={'btc'} crypto={'btc'} />
</label>
<input
type='number'
min='0'
max='0.16'
ref={(input) => { this.input = input }}
size=''
value={push_amt}
onChange={event => setPushAmount(event.target.value)}
id='amount'
style={{ width: isNaN((push_amt.length + 1) * 55) ? 140 : (push_amt.length + 1) * 55 }}
/>
</form>
</div>
)
}
}
StepThree.propTypes = {
push_amt: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string
]).isRequired,
setPushAmount: PropTypes.func.isRequired
}
export default StepThree

58
app/components/ChannelForm/StepThree.scss

@ -1,58 +0,0 @@
@import '../../variables.scss';
.container {
margin-bottom: 50px;
padding: 20px;
.explainer {
margin: 0px 0 50px 0;
padding-bottom: 20px;
border-bottom: 1px solid $lightgrey;
h2 {
margin: 0 0 20px 0;
font-size: 28px;
}
p {
line-height: 1.5;
font-size: 16px;
}
}
form {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
label {
height: 200px;
color: $main;
svg {
width: 65px;
height: 65px;
}
svg[data-icon='ltc'] {
margin-right: 10px;
g {
transform: scale(1.75) translate(-5px, -5px);
}
}
}
input[type=number] {
color: $main;
width: 30px;
height: 200px;
font-size: 100px;
font-weight: 200;
border: none;
outline: 0;
-webkit-appearance: none;
}
}

49
app/components/ChannelForm/StepTwo.js

@ -1,49 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import CurrencyIcon from 'components/CurrencyIcon'
import styles from './StepTwo.scss'
class StepTwo extends Component {
render() {
const { local_amt, setLocalAmount } = this.props
return (
<div className={styles.container}>
<div className={styles.explainer}>
<h2>Local Amount</h2>
<p>
Local amount is the amount of bitcoin that you would like to commit to the channel.
This is the amount that will be sent in an on-chain transaction to open your Lightning channel.
</p>
</div>
<form>
<label htmlFor='amount'>
<CurrencyIcon currency={'btc'} crypto={'btc'} />
</label>
<input
type='number'
min='0'
max='0.16'
ref={(input) => { this.input = input }}
size=''
value={local_amt}
onChange={event => setLocalAmount(event.target.value)}
id='amount'
style={{ width: isNaN((local_amt.length + 1) * 55) ? 140 : (local_amt.length + 1) * 55 }}
/>
</form>
</div>
)
}
}
StepTwo.propTypes = {
local_amt: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
setLocalAmount: PropTypes.func.isRequired
}
export default StepTwo

58
app/components/ChannelForm/StepTwo.scss

@ -1,58 +0,0 @@
@import '../../variables.scss';
.container {
margin-bottom: 50px;
padding: 20px;
.explainer {
margin: 0px 0 50px 0;
padding-bottom: 20px;
border-bottom: 1px solid $lightgrey;
h2 {
margin: 0 0 20px 0;
font-size: 28px;
}
p {
line-height: 1.5;
font-size: 16px;
}
}
form {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
label {
height: 200px;
color: $main;
svg {
width: 65px;
height: 65px;
}
svg[data-icon='ltc'] {
margin-right: 10px;
g {
transform: scale(1.75) translate(-5px, -5px);
}
}
}
input[type=number] {
color: $main;
width: 30px;
height: 200px;
font-size: 100px;
font-weight: 200;
border: none;
outline: 0;
-webkit-appearance: none;
}
}

3
app/components/ChannelForm/index.js

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

93
app/components/Channels/Channel.js

@ -1,93 +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 './Channel.scss'
const Channel = ({ ticker, channel, closeChannel, currentTicker }) => (
<li className={styles.channel}>
<header className={styles.header}>
<div>
<span className={styles.status}>Open</span>
{
channel.active ?
<span className={styles.active}>
<FaCircle />
<i>Active</i>
</span>
:
<span className={styles.notactive}>
<FaCircle />
<i>Not Active</i>
</span>
}
</div>
<div>
<p
className={styles.close}
onClick={() => closeChannel({ channel_point: channel.channel_point })}
>
Close channel
</p>
</div>
</header>
<div className={styles.content}>
<div className={styles.left}>
<section className={styles.remotePubkey}>
<span>Remote Pubkey</span>
<h4>{channel.remote_pubkey}</h4>
</section>
<section className={styles.channelPoint}>
<span>Channel Point</span>
<h4>{channel.channel_point}</h4>
</section>
</div>
<div className={styles.right}>
<section className={styles.capacity}>
<span>Capacity</span>
<h2>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.capacity)
:
btc.satoshisToUsd(channel.capacity, currentTicker.price_usd)
}
</h2>
</section>
<div className={styles.balances}>
<section>
<span>Local</span>
<h4>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.local_balance)
:
btc.satoshisToUsd(channel.local_balance, currentTicker.price_usd)
}
</h4>
</section>
<section>
<span>Remote</span>
<h4>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.remote_balance)
:
btc.satoshisToUsd(channel.remote_balance, currentTicker.price_usd)
}
</h4>
</section>
</div>
</div>
</div>
</li>
)
Channel.propTypes = {
ticker: PropTypes.object.isRequired,
channel: PropTypes.object.isRequired,
closeChannel: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired
}
export default Channel

125
app/components/Channels/Channel.scss

@ -1,125 +0,0 @@
@import '../../variables.scss';
.channel {
position: relative;
background: $white;
margin: 5px 0;
padding: 10px;
border-top: 1px solid $white;
cursor: pointer;
transition: all 0.25s;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
&:hover {
opacity: 0.75;
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}
&:first-child {
border: none;
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
.status, .active, .notactive {
padding: 10px;
text-transform: uppercase;
font-weight: bold;
font-size: 10px;
}
.status {
color: $main;
}
.active i, .notactive i {
margin-left: 5px;
}
.active {
color: $green;
}
.notactive {
color: $red;
}
.close {
padding: 10px;
font-size: 10px;
text-transform: uppercase;
color: $red;
cursor: pointer;
&:hover {
color: lighten($red, 10%);
text-decoration: underline;
}
}
}
.content {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.left, .right {
padding: 0 10px;
margin-bottom: 5;
section {
margin-bottom: 20px;
span {
text-transform: uppercase;
letter-spacing: 1.6px;
color: $black;
font-size: 10px;
font-weight: bold;
}
h2 {
font-size: 30px;
padding: 5px 0;
color: $main;
}
h4 {
margin-top: 5px;
}
}
}
.left {
flex: 7;
}
.right {
flex: 3;
.capacity {
text-align: center;
margin-bottom: 10px;
}
.balances {
display: flex;
justify-content: space-between;
section {
flex: 5;
text-align: center;
h4 {
color: $main;
font-size: 16px;
}
}
}
}
}

127
app/components/Channels/ChannelForm.js

@ -1,127 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactModal from 'react-modal'
import { FaUser } from 'react-icons/lib/fa'
import CurrencyIcon from 'components/CurrencyIcon'
import { usd, btc } from 'utils'
import styles from './ChannelForm.scss'
const ChannelForm = ({ form, setForm, ticker, peers, openChannel, currentTicker }) => {
const submitClicked = () => {
const { node_key, local_amt, push_amt } = form
const localamt = ticker.currency === 'usd' ? btc.btcToSatoshis(usd.usdToBtc(local_amt, currentTicker.price_usd)) : btc.btcToSatoshis(local_amt)
const pushamt = ticker.currency === 'usd' ? btc.btcToSatoshis(usd.usdToBtc(push_amt, currentTicker.price_usd)) : btc.btcToSatoshis(push_amt)
openChannel({ pubkey: node_key, localamt, pushamt })
// setForm({ isOpen: false })
}
const customStyles = {
overlay: {
cursor: 'pointer',
overflowY: 'auto'
},
content: {
top: 'auto',
left: '20%',
right: '0',
bottom: 'auto',
width: '40%',
margin: '50px auto',
padding: '40px'
}
}
return (
<div>
<ReactModal
isOpen={form.isOpen}
contentLabel='No Overlay Click Modal'
ariaHideApp
shouldCloseOnOverlayClick
onRequestClose={() => setForm({ isOpen: false })}
parentSelector={() => document.body}
style={customStyles}
>
<div className={styles.form}>
<h1 className={styles.title}>Open a new channel</h1>
<section className={styles.pubkey}>
<label htmlFor='nodekey'><FaUser /></label>
<input
type='text'
size=''
placeholder='Peer public key'
value={form.node_key}
onChange={event => setForm({ node_key: event.target.value })}
id='nodekey'
/>
</section>
<section className={styles.local}>
<label htmlFor='localamount'>
<CurrencyIcon currency={ticker.currency} crypto={ticker.crypto} />
</label>
<input
type='text'
size=''
placeholder='Local amount'
value={form.local_amt}
onChange={event => setForm({ local_amt: event.target.value })}
id='localamount'
/>
</section>
<section className={styles.push}>
<label htmlFor='pushamount'>
<CurrencyIcon currency={ticker.currency} crypto={ticker.crypto} />
</label>
<input
type='text'
size=''
placeholder='Push amount'
value={form.push_amt}
onChange={event => setForm({ push_amt: event.target.value })}
id='pushamount'
/>
</section>
<ul className={styles.peers}>
<h2>Connected Peers</h2>
{
peers.length ?
peers.map(peer =>
(
<li
key={peer.peer_id}
className={styles.peer}
onClick={() => setForm({ node_key: peer.pub_key })}
>
<h4>{peer.address}</h4>
<h1>{peer.pub_key}</h1>
</li>
)
)
:
null
}
</ul>
<div className={styles.buttonGroup}>
<div className={styles.button} onClick={submitClicked}>Submit</div>
</div>
</div>
</ReactModal>
</div>
)
}
ChannelForm.propTypes = {
form: PropTypes.object.isRequired,
setForm: PropTypes.func.isRequired,
ticker: PropTypes.object.isRequired,
peers: PropTypes.array.isRequired,
openChannel: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired
}
export default ChannelForm

123
app/components/Channels/ChannelForm.scss

@ -1,123 +0,0 @@
@import '../../variables.scss';
.title {
text-align: center;
font-size: 24px;
color: $black;
margin-bottom: 50px;
}
.pubkey, .local, .push {
display: flex;
justify-content: center;
font-size: 18px;
height: auto;
min-height: 55px;
margin-bottom: 20px;
border: 1px solid $traditionalgrey;
border-radius: 6px;
position: relative;
padding: 0 20px;
label, input[type=text] {
font-size: inherit;
}
label {
padding-top: 19px;
padding-bottom: 12px;
color: $traditionalgrey;
svg[data-icon='ltc'] {
width: 18px;
height: 16px;
g {
transform: scale(1.75) translate(-5px, -5px);
}
}
}
input[type=text] {
width: 100%;
border: none;
outline: 0;
-webkit-appearance: none;
height: 55px;
padding: 0 10px;
}
}
.peers {
margin-bottom: 50px;
h2 {
text-transform: uppercase;
font-weight: 200;
padding: 10px 0;
border-bottom: 1px solid $grey;
color: $darkestgrey;
}
}
.peer {
position: relative;
background: $white;
padding: 10px;
border-top: 1px solid $grey;
cursor: pointer;
transition: all 0.25s;
&:hover {
opacity: 0.5;
}
&:first-child {
border: none;
}
h4, h1 {
margin: 10px 0;
}
h4 {
font-size: 12px;
font-weight: bold;
color: $black;
}
h1 {
font-size: 14px;
font-weight: 200;
color: $main;
}
}
.buttonGroup {
width: 100%;
display: flex;
flex-direction: row;
border-radius: 6px;
overflow: hidden;
.button {
cursor: pointer;
height: 55px;
min-height: 55px;
text-transform: none;
font-size: 18px;
transition: opacity .2s ease-out;
background: $main;
color: $white;
border: none;
font-weight: 500;
padding: 0;
width: 100%;
text-align: center;
line-height: 55px;
&:first-child {
border-right: 1px solid lighten($main, 20%);
}
}
}

101
app/components/Channels/ChannelModal.js

@ -1,101 +0,0 @@
import { shell } from 'electron'
import React from 'react'
import PropTypes from 'prop-types'
import ReactModal from 'react-modal'
import styles from './ChannelModal.scss'
const ChannelModal = ({ isOpen, resetChannel, channel, explorerLinkBase, closeChannel }) => {
const customStyles = {
overlay: {
cursor: 'pointer',
overflowY: 'auto'
},
content: {
top: 'auto',
left: '20%',
right: '0',
bottom: 'auto',
width: '40%',
margin: '50px auto',
padding: '40px'
}
}
const closeChannelClicked = () => {
closeChannel({ channel_point: channel.channel_point })
resetChannel(null)
}
return (
<ReactModal
isOpen={isOpen}
contentLabel='No Overlay Click Modal'
ariaHideApp
shouldCloseOnOverlayClick
onRequestClose={() => resetChannel(null)}
parentSelector={() => document.body}
style={customStyles}
>
{
channel ?
<div className={styles.channel}>
<header className={styles.header}>
<h1 data-hint='Remote public key' className='hint--top-left'>{channel.remote_pubkey}</h1>
<h2
data-hint='Channel point'
className='hint--top-left'
onClick={() => shell.openExternal(`${explorerLinkBase}/tx/${channel.channel_point.split(':')[0]}`)}
>
{channel.channel_point}
</h2>
</header>
<div className={styles.balances}>
<section className={styles.capacity}>
<h3>{channel.capacity}</h3>
<span>Capacity</span>
</section>
<div className={styles.balance}>
<section className={styles.local}>
<h4>{channel.local_balance}</h4>
<span>Local</span>
</section>
<section className={styles.remote}>
<h4>{channel.remote_balance}</h4>
<span>Remote</span>
</section>
</div>
</div>
<div className={styles.details}>
<dl>
<dt>Sent</dt>
<dd>{channel.total_satoshis_sent}</dd>
<dt>Received</dt>
<dd>{channel.total_satoshis_received}</dd>
<dt>Updates</dt>
<dd>{channel.num_updates}</dd>
</dl>
</div>
<div className={styles.close} onClick={closeChannelClicked}>
<div>Close channel</div>
</div>
<footer className={styles.active}>
<p>{channel.active ? 'Active' : 'Not active'}</p>
</footer>
</div>
:
null
}
</ReactModal>
)
}
ChannelModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
resetChannel: PropTypes.func.isRequired,
channel: PropTypes.object,
explorerLinkBase: PropTypes.string.isRequired,
closeChannel: PropTypes.func.isRequired
}
export default ChannelModal

124
app/components/Channels/ChannelModal.scss

@ -1,124 +0,0 @@
@import '../../variables.scss';
.modalChannel {
padding: 40px;
}
.header {
margin-bottom: 50px;
h1 {
color: $black;
text-align: center;
margin-bottom: 5px;
font-weight: bold;
}
h2 {
color: $darkestgrey;
font-size: 14px;
text-align: center;
&:hover {
color: $main;
text-decoration: underline;
}
}
}
.balances {
.capacity {
text-align: center;
align-items: center;
h3 {
color: $main;
font-size: 40px;
}
span {
color: $black;
font-size: 16px;
}
}
.balance {
display: flex;
flex-direction: row;
justify-content: space-between;
.local, .remote {
flex: 5;
padding: 10px 30px;
text-align: center;
h4 {
font-size: 20px;
color: $main;
}
span {
color: $black;
font-size: 12px;
}
}
}
}
.details {
width: 75%;
margin: 20px auto;
dt {
text-align: left;
float: left;
clear: left;
font-weight: 500;
padding: 20px 35px 19px 0;
color: $black;
font-weight: bold;
}
dd {
text-align: right;
font-weight: 400;
padding: 19px 0;
margin-left: 0;
border-top: 1px solid $darkgrey;
}
}
.close {
text-align: center;
div {
width: 35%;
margin: 0 auto;
cursor: pointer;
height: 55px;
min-height: 55px;
text-transform: none;
font-size: 18px;
transition: opacity .2s ease-out;
background: $red;
color: $white;
border: none;
font-weight: 500;
padding: 0;
text-align: center;
line-height: 55px;
transition: all 0.25s;
border-radius: 5px;
&:hover {
background: darken($red, 10%);
}
}
}
.active {
color: $darkestgrey;
text-align: center;
margin-top: 50px;
text-transform: uppercase;
}

140
app/components/Channels/Channels.js

@ -1,140 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { TiPlus } from 'react-icons/lib/ti'
import { FaRepeat } from 'react-icons/lib/fa'
import ChannelModal from './ChannelModal'
import ChannelForm from './ChannelForm'
import Channel from './Channel'
import OpenPendingChannel from './OpenPendingChannel'
import ClosedPendingChannel from './ClosedPendingChannel'
import styles from './Channels.scss'
const Channels = ({
fetchChannels,
ticker,
peers,
channelsLoading,
modalChannel,
setChannel,
channelModalOpen,
channelForm,
setChannelForm,
allChannels,
openChannel,
closeChannel,
currentTicker,
explorerLinkBase
}) => {
const refreshClicked = (event) => {
// store event in icon so we dont get an error when react clears it
const icon = event.currentTarget
// fetch channels
fetchChannels()
// clear animation after the second so we can reuse it
setTimeout(() => { icon.style.animation = '' }, 1000)
// spin icon for 1 sec
icon.style.animation = 'spin 1000ms linear 1'
}
return (
<div className={styles.channels}>
<ChannelModal
isOpen={channelModalOpen}
resetChannel={setChannel}
channel={modalChannel}
explorerLinkBase={explorerLinkBase}
closeChannel={closeChannel}
/>
<ChannelForm
form={channelForm}
setForm={setChannelForm}
ticker={ticker}
peers={peers}
openChannel={openChannel}
currentTicker={currentTicker}
/>
<div className={styles.header}>
<h3>Channels</h3>
<span
className={`${styles.refresh} hint--top`}
data-hint='Refresh your channels list'
>
<FaRepeat
style={{ verticalAlign: 'baseline' }}
onClick={refreshClicked}
/>
</span>
<div
className={`${styles.openChannel} hint--top`}
data-hint='Open a channel'
onClick={() => setChannelForm({ isOpen: true })}
>
<TiPlus />
</div>
</div>
<ul>
{
!channelsLoading ?
allChannels.map((channel, index) => {
if (Object.prototype.hasOwnProperty.call(channel, 'blocks_till_open')) {
return (
<OpenPendingChannel
key={index}
channel={channel}
ticker={ticker}
currentTicker={currentTicker}
explorerLinkBase={explorerLinkBase}
/>
)
} else if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) {
return (
<ClosedPendingChannel
key={index}
channel={channel}
ticker={ticker}
currentTicker={currentTicker}
explorerLinkBase={explorerLinkBase}
/>
)
}
return (
<Channel
key={index}
ticker={ticker}
channel={channel}
setChannel={setChannel}
currentTicker={currentTicker}
closeChannel={closeChannel}
/>
)
})
:
'Loading...'
}
</ul>
</div>
)
}
Channels.propTypes = {
fetchChannels: PropTypes.func.isRequired,
ticker: PropTypes.object.isRequired,
peers: PropTypes.array.isRequired,
channelsLoading: PropTypes.bool.isRequired,
modalChannel: PropTypes.object,
setChannel: PropTypes.func.isRequired,
channelModalOpen: PropTypes.bool.isRequired,
channelForm: PropTypes.object.isRequired,
setChannelForm: PropTypes.func.isRequired,
allChannels: PropTypes.array.isRequired,
openChannel: PropTypes.func.isRequired,
closeChannel: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired,
explorerLinkBase: PropTypes.string.isRequired
}
export default Channels

69
app/components/Channels/Channels.scss

@ -1,69 +0,0 @@
@import '../../variables.scss';
@keyframes spin {
from {
transform: rotate(0deg)
}
to {
transform: rotate(360deg);
}
}
.channels {
width: 75%;
margin: 50px auto;
.header {
margin-bottom: 10px;
h3, .openChannel {
display: inline-block;
}
h3 {
text-align: left;
}
.refresh {
cursor: pointer;
margin-left: 5px;
font-size: 12px;
vertical-align: top;
color: $darkestgrey;
line-height: 14px;
transition: color 0.25s;
&:hover {
color: $main;
}
}
.openChannel {
float: right;
cursor: pointer;
svg {
padding: 3px;
border-radius: 50%;
border: 1px solid $main;
color: $main;
transition: all 0.25s;
&:hover {
border-color: darken($main, 10%);
color: darken($main, 10%);
}
}
}
}
h3 {
text-transform: uppercase;
color: $darkestgrey;
letter-spacing: 1.6px;
font-size: 14px;
font-weight: 400;
margin-bottom: 10px;
}
}

67
app/components/Channels/ClosedPendingChannel.js

@ -1,67 +0,0 @@
import { shell } from 'electron'
import React from 'react'
import PropTypes from 'prop-types'
import { btc } from 'utils'
import styles from './ClosedPendingChannel.scss'
const ClosedPendingChannel = ({ ticker, channel: { channel, closing_txid }, currentTicker, explorerLinkBase }) => (
<li className={styles.channel} onClick={() => shell.openExternal(`${explorerLinkBase}/tx/${closing_txid}`)}>
<h1 className={styles.closing}>Closing Channel...</h1>
<div className={styles.left}>
<section className={styles.remotePubkey}>
<span>Remote Pubkey</span>
<h4>{channel.remote_node_pub}</h4>
</section>
<section className={styles.channelPoint}>
<span>Channel Point</span>
<h4>{channel.channel_point}</h4>
</section>
</div>
<div className={styles.right}>
<section className={styles.capacity}>
<span>Capacity</span>
<h2>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.capacity)
:
btc.satoshisToUsd(channel.capacity, currentTicker.price_usd)
}
</h2>
</section>
<div className={styles.balances}>
<section>
<h4>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.local_balance)
:
btc.satoshisToUsd(channel.local_balance, currentTicker.price_usd)
}
</h4>
<span>Local</span>
</section>
<section>
<h4>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.remote_balance)
:
btc.satoshisToUsd(channel.remote_balance, currentTicker.price_usd)
}
</h4>
<span>Remote</span>
</section>
</div>
</div>
</li>
)
ClosedPendingChannel.propTypes = {
ticker: PropTypes.object.isRequired,
channel: PropTypes.object.isRequired,
currentTicker: PropTypes.object.isRequired,
explorerLinkBase: PropTypes.string.isRequired
}
export default ClosedPendingChannel

95
app/components/Channels/ClosedPendingChannel.scss

@ -1,95 +0,0 @@
@import '../../variables.scss';
.channel {
position: relative;
background: $white;
padding: 10px;
display: flex;
flex-direction: row;
justify-content: space-between;
border-top: 1px solid $grey;
cursor: pointer;
transition: all 0.25s;
opacity: 0.5;
&:hover {
opacity: 0.35;
}
&:first-child {
border: none;
}
.closing {
color: $red;
position: absolute;
top: 0;
left: 10px;
padding: 10px;
text-transform: uppercase;
font-weight: bold;
font-size: 10px;
}
.left, .right {
padding: 0 10px;
margin-bottom: 5;
margin-top: 25px;
section {
margin-bottom: 20px;
span {
text-transform: uppercase;
letter-spacing: 1.6px;
color: $black;
font-size: 10px;
font-weight: bold;
}
h2 {
font-size: 30px;
padding: 5px 0;
color: $main;
}
h4 {
margin-top: 5px;
}
}
}
.left {
flex: 7;
border-right: 1px solid $grey;
}
.right {
flex: 3;
.capacity {
text-align: center;
border-bottom: 1px solid $grey;
margin-bottom: 10px;
}
.balances {
display: flex;
justify-content: space-between;
section {
flex: 5;
text-align: center;
h4 {
color: $main;
font-size: 16px;
}
&:first-child {
border-right: 1px solid $grey;
}
}
}
}
}

70
app/components/Channels/OpenPendingChannel.js

@ -1,70 +0,0 @@
import { shell } from 'electron'
import React from 'react'
import PropTypes from 'prop-types'
import { btc } from 'utils'
import styles from './OpenPendingChannel.scss'
const OpenPendingChannel = ({ ticker, channel, currentTicker, explorerLinkBase }) => (
<li className={styles.channel} onClick={() => shell.openExternal(`${explorerLinkBase}/tx/${channel.channel.channel_point.split(':')[0]}`)}>
<div className={styles.pending}>
<h1>Opening Channel...</h1>
<span>Blocks till open: {channel.blocks_till_open}</span>
</div>
<div className={styles.left}>
<section className={styles.remotePubkey}>
<span>Remote Pubkey</span>
<h4>{channel.channel.remote_node_pub}</h4>
</section>
<section className={styles.channelPoint}>
<span>Channel Point</span>
<h4>{channel.channel.channel_point}</h4>
</section>
</div>
<div className={styles.right}>
<section className={styles.capacity}>
<span>Capacity</span>
<h2>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.channel.capacity)
:
btc.satoshisToUsd(channel.channel.capacity, currentTicker.price_usd)
}
</h2>
</section>
<div className={styles.balances}>
<section>
<h4>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.channel.local_balance)
:
btc.satoshisToUsd(channel.channel.local_balance, currentTicker.price_usd)
}
</h4>
<span>Local</span>
</section>
<section>
<h4>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.channel.remote_balance)
:
btc.satoshisToUsd(channel.channel.remote_balance, currentTicker.price_usd)
}
</h4>
<span>Remote</span>
</section>
</div>
</div>
</li>
)
OpenPendingChannel.propTypes = {
ticker: PropTypes.object.isRequired,
channel: PropTypes.object.isRequired,
currentTicker: PropTypes.object.isRequired,
explorerLinkBase: PropTypes.string.isRequired
}
export default OpenPendingChannel

98
app/components/Channels/OpenPendingChannel.scss

@ -1,98 +0,0 @@
@import '../../variables.scss';
.channel {
position: relative;
background: $lightgrey;
padding: 10px;
display: flex;
flex-direction: row;
justify-content: space-between;
border-top: 1px solid $grey;
cursor: pointer;
transition: all 0.25s;
opacity: 0.5;
.pending {
position: absolute;
top: 0;
left: 10px;
padding: 10px;
text-transform: uppercase;
font-weight: bold;
h1 {
color: $main;
font-size: 10px;
}
span {
font-size: 8px;
}
}
&:first-child {
border: none;
}
.left, .right {
padding: 0 10px;
margin-bottom: 5;
margin-top: 40px;
section {
margin-bottom: 20px;
span {
text-transform: uppercase;
letter-spacing: 1.6px;
color: $black;
font-size: 10px;
font-weight: bold;
}
h2 {
font-size: 30px;
padding: 5px 0;
color: $main;
}
h4 {
margin-top: 5px;
}
}
}
.left {
flex: 7;
border-right: 1px solid $grey;
}
.right {
flex: 3;
.capacity {
text-align: center;
border-bottom: 1px solid $grey;
margin-bottom: 10px;
}
.balances {
display: flex;
justify-content: space-between;
section {
flex: 5;
text-align: center;
h4 {
color: $main;
font-size: 16px;
}
&:first-child {
border-right: 1px solid $grey;
}
}
}
}
}

3
app/components/Channels/index.js

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

39
app/components/Contacts/ClosingContact.js

@ -0,0 +1,39 @@
import { shell } from 'electron'
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 ClosingContact = ({ channel }) => (
<li className={styles.friend}>
<section className={styles.info}>
<p className={styles.closing}>
<FaCircle style={{ verticalAlign: 'top' }} />
<span>
Removing
<i onClick={() => shell.openExternal(`${'https://testnet.smartbit.com.au'}/tx/${channel.closing_txid}`)}>
(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

@ -0,0 +1,156 @@
@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;
}

134
app/components/Contacts/ContactModal.js

@ -0,0 +1,134 @@
import React from 'react'
import PropTypes from 'prop-types'
import find from 'lodash/find'
import ReactModal from 'react-modal'
import { FaClose, FaCircle } from 'react-icons/lib/fa'
import { btc } from 'utils'
import styles from './ContactModal.scss'
const ContactModal = ({
isOpen,
channel,
closeContactModal,
channelNodes,
closeChannel,
closingChannelIds
}) => {
if (!channel) { return <span /> }
const customStyles = {
overlay: {
cursor: 'pointer',
overflowY: 'auto'
},
content: {
top: 'auto',
left: '20%',
right: '0',
bottom: 'auto',
width: '40%',
margin: '50px auto',
borderRadius: 'none',
padding: '0'
}
}
const removeClicked = () => {
closeChannel({ channel_point: channel.channel_point, chan_id: channel.chan_id, force: !channel.active })
}
// the remote node for the channel
const node = find(channelNodes, { pub_key: channel.remote_pubkey })
return (
<ReactModal
isOpen={isOpen}
contentLabel='No Overlay Click Modal'
ariaHideApp
shouldCloseOnOverlayClick
onRequestClose={closeContactModal}
parentSelector={() => document.body}
style={customStyles}
>
{
channel &&
<div className={styles.container}>
<header className={styles.header}>
<div className={`${styles.status} ${channel.active && styles.online}`}>
<FaCircle style={{ verticalAlign: 'top' }} />
<span>
{channel.active ? 'Online' : 'Offline'}
</span>
</div>
<div className={styles.closeContainer}>
<span onClick={closeContactModal}>
<FaClose />
</span>
</div>
</header>
<section className={styles.title}>
{
node &&
<h1>{node.alias}</h1>
}
<h2>{channel.remote_pubkey}</h2>
</section>
<section className={styles.stats}>
<div className={styles.pay}>
<h4>Can Pay</h4>
<div className={styles.meter}>
<div className={styles.amount} style={{ width: `${(channel.local_balance / channel.capacity) * 100}%` }} />
</div>
<span>{btc.satoshisToBtc(channel.local_balance)} BTC</span>
</div>
<div className={styles.pay}>
<h4>Can Receive</h4>
<div className={styles.meter}>
<div className={styles.amount} style={{ width: `${(channel.remote_balance / channel.capacity) * 100}%` }} />
</div>
<span>{btc.satoshisToBtc(channel.remote_balance)} BTC</span>
</div>
<div className={styles.sent}>
<h4>Total Bitcoin Sent</h4>
<p>{btc.satoshisToBtc(channel.total_satoshis_sent)} BTC</p>
</div>
<div className={styles.received}>
<h4>Total Bitcoin Received</h4>
<p>{btc.satoshisToBtc(channel.total_satoshis_received)} BTC</p>
</div>
</section>
<footer>
{
closingChannelIds.includes(channel.chan_id) ?
<span className={styles.inactive}>
<div className={styles.loading}>
<div className={styles.spinner} />
</div>
</span>
:
<div onClick={removeClicked}>Remove</div>
}
</footer>
</div>
}
</ReactModal>
)
}
ContactModal.propTypes = {
channel: PropTypes.object,
isOpen: PropTypes.bool.isRequired,
closeContactModal: PropTypes.func.isRequired,
channelNodes: PropTypes.array.isRequired,
closeChannel: PropTypes.func.isRequired,
closingChannelIds: PropTypes.array.isRequired
}
export default ContactModal

152
app/components/Contacts/ContactModal.scss

@ -0,0 +1,152 @@
@import '../../variables.scss';
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
background: $lightgrey;
padding: 20px;
.status {
font-size: 12px;
color: $darkestgrey;
&.online {
color: $green;
}
span {
margin-left: 5px;
}
}
.closeContainer {
background: $lightgrey;
line-height: 12px;
span {
color: $darkestgrey;
cursor: pointer;
}
}
}
.container section {
margin-bottom: 30px;
padding: 0 20px;
.pay, .receive, .sent, .received {
margin: 40px 0;
}
}
.container .title {
margin: 0;
padding: 30px 20px;
background: $lightgrey;
h1 {
color: $secondary;
font-weight: bold;
font-size: 16px;
letter-spacing: 1.1px;
margin-bottom: 10px;
}
h2 {
font-size: 12px;
color: $darkestgrey;
font-weight: 100;
}
}
.stats {
h4 {
color: $secondary;
font-weight: bold;
font-size: 12px;
}
span {
font-size: 14px;
}
p {
margin-top: 10px;
color: $darkestgrey;
}
.meter, .amount {
height: 10px;
border-radius: 10px;
}
.meter {
background: $darkgrey;
width: 100%;
margin: 10px 0;
}
.amount {
background: $darkestgrey;
}
}
.container footer {
padding: 20px;
text-align: center;
div {
color: $red;
font-size: 18px;
&:hover {
color: lighten($red, 10%);
}
}
}
@-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: 20px;
width: 20px;
-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;
}

230
app/components/Contacts/ContactsForm.js

@ -0,0 +1,230 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactModal from 'react-modal'
import { MdClose } from 'react-icons/lib/md'
import { FaCircle, FaQuestionCircle } from 'react-icons/lib/fa'
import styles from './ContactsForm.scss'
class ContactsForm extends React.Component {
constructor(props) {
super(props)
this.state = {
editing: false,
manualFormInput: ''
}
}
render() {
const {
contactsform,
closeContactsForm,
updateContactFormSearchQuery,
updateContactCapacity,
openChannel,
activeChannelPubkeys,
nonActiveChannelPubkeys,
pendingOpenChannelPubkeys,
filteredNetworkNodes,
loadingChannelPubkeys,
showManualForm
} = this.props
const { editing, manualFormInput } = this.state
const renderRightSide = (node) => {
if (loadingChannelPubkeys.includes(node.pub_key)) {
return (
<span className={styles.inactive}>
<div className={styles.loading}>
<div className={styles.spinner} />
</div>
</span>
)
}
if (activeChannelPubkeys.includes(node.pub_key)) {
return (
<span className={`${styles.online} ${styles.inactive}`}>
<FaCircle style={{ verticalAlign: 'top' }} /> <span>Online</span>
</span>
)
}
if (nonActiveChannelPubkeys.includes(node.pub_key)) {
return (
<span className={`${styles.offline} ${styles.inactive}`}>
<FaCircle style={{ verticalAlign: 'top' }} /> <span>Offline</span>
</span>
)
}
if (pendingOpenChannelPubkeys.includes(node.pub_key)) {
return (
<span className={`${styles.pending} ${styles.inactive}`}>
<FaCircle style={{ verticalAlign: 'top' }} /> <span>Pending</span>
</span>
)
}
if (!node.addresses.length) {
return (
<span className={`${styles.private} ${styles.inactive}`}>
Private
</span>
)
}
return (
<span
className={`${styles.connect} hint--left`}
data-hint={`Connect with ${contactsform.contactCapacity} BTC`}
onClick={() => openChannel({ pubkey: node.pub_key, host: node.addresses[0].addr, local_amt: contactsform.contactCapacity })}
>
Connect
</span>
)
}
const inputClicked = () => {
if (editing) { return }
this.setState({ editing: true })
}
const manualFormSubmit = () => {
if (!manualFormInput.length) { return }
if (!manualFormInput.includes('@')) { return }
const [pubkey, host] = manualFormInput && manualFormInput.split('@')
openChannel({ pubkey, host, local_amt: contactsform.contactCapacity })
this.setState({ manualFormInput: '' })
}
return (
<div>
<ReactModal
isOpen={contactsform.isOpen}
contentLabel='No Overlay Click Modal'
ariaHideApp
shouldCloseOnOverlayClick
onRequestClose={() => closeContactsForm}
parentSelector={() => document.body}
className={styles.modal}
>
<header>
<div>
<h1>Add Contact</h1>
</div>
<div onClick={closeContactsForm} className={styles.modalClose}>
<MdClose />
</div>
</header>
<div className={styles.form}>
<div className={styles.search}>
<input
type='text'
placeholder='Find contact by alias or pubkey'
className={styles.searchInput}
value={contactsform.searchQuery}
onChange={event => updateContactFormSearchQuery(event.target.value)}
/>
</div>
<ul className={styles.networkResults}>
{
contactsform.searchQuery.length > 0 && filteredNetworkNodes.map(node => (
<li key={node.pub_key}>
<section>
{
node.alias.length > 0 ?
<h2>
<span>{node.alias.trim()}</span>
<span>({node.pub_key.substr(0, 10)}...{node.pub_key.substr(node.pub_key.length - 10)})</span>
</h2>
:
<h2>
<span>{node.pub_key}</span>
</h2>
}
</section>
<section>
{renderRightSide(node)}
</section>
</li>
))
}
</ul>
</div>
{
showManualForm &&
<div className={styles.manualForm}>
<h2>Hm, looks like we cant see that contact from here. Want to try and manually connect?</h2>
<section>
<input
type='text'
placeholder='pubkey@host'
value={manualFormInput}
onChange={event => this.setState({ manualFormInput: event.target.value })}
/>
<div className={styles.submit} onClick={manualFormSubmit}>Submit</div>
</section>
</div>
}
<footer className={styles.footer}>
<div>
<span>
Use
</span>
<span className={styles.amount}>
<input
type='text'
value={contactsform.contactCapacity}
onChange={event => updateContactCapacity(event.target.value)}
onClick={inputClicked}
onKeyPress={event => event.charCode === 13 && this.setState({ editing: false })}
readOnly={!editing}
style={{ width: `${editing ? 20 : contactsform.contactCapacity.toString().length + 1}%` }}
/>
</span>
<span className={styles.caption}>
BTC per contact
<i
data-hint="You aren't spending anything, just moving money onto the Lightning Network"
className='hint--top'
>
<FaQuestionCircle style={{ verticalAlign: 'top' }} />
</i>
</span>
</div>
</footer>
</ReactModal>
</div>
)
}
}
ContactsForm.propTypes = {
contactsform: PropTypes.object.isRequired,
closeContactsForm: PropTypes.func.isRequired,
updateContactFormSearchQuery: PropTypes.func.isRequired,
updateContactCapacity: PropTypes.func.isRequired,
openChannel: PropTypes.func.isRequired,
activeChannelPubkeys: PropTypes.array.isRequired,
nonActiveChannelPubkeys: PropTypes.array.isRequired,
pendingOpenChannelPubkeys: PropTypes.array.isRequired,
filteredNetworkNodes: PropTypes.array.isRequired,
loadingChannelPubkeys: PropTypes.array.isRequired,
showManualForm: PropTypes.bool.isRequired
}
export default ContactsForm

239
app/components/Contacts/ContactsForm.scss

@ -0,0 +1,239 @@
@import '../../variables.scss';
.modal {
position: relative;
width: 50%;
margin: 50px auto;
position: absolute;
top: auto;
left: 20%;
right: 0;
bottom: auto;
background: $white;
outline: none;
z-index: -2;
border: 1px solid $darkgrey;
header {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 15px;
border-bottom: 1px solid $darkgrey;
h1, svg {
font-size: 22px;
}
svg {
cursor: pointer;
}
}
}
.form {
padding: 30px 15px;
.search {
.searchInput {
width: calc(100% - 30px);
padding: 10px 15px;
outline: 0;
border: 0;
background: $lightgrey;
color: $darkestgrey;
border-radius: 5px;
font-size: 16px;
}
}
.networkResults {
overflow-y: scroll;
height: 300px;
margin-top: 30px;
padding: 20px 0;
li {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 10px 0;
h2 {
font-size: 16px;
font-weight: bold;
letter-spacing: 1.3px;
span {
display: inline-block;
vertical-align: middle;
&:nth-child(1) {
font-size: 14px;
font-weight: bold;
letter-spacing: 1.3px;
}
&:nth-child(2) {
color: $darkestgrey;
font-size: 12px;
line-height: 14px;
}
}
}
.connect {
cursor: pointer;
color: $darkestgrey;
transition: all 0.25s;
font-size: 12px;
&:hover {
color: $main;
}
}
.inactive {
font-size: 12px;
display: inline-block;
vertical-align: top;
&.online {
color: $green;
}
&.offline {
color: $darkestgrey;
}
&.pending {
color: $orange;
}
&.private {
color: darken($darkestgrey, 50%);
}
}
}
}
}
.manualForm {
background: $lightgrey;
color: $darkestgrey;
padding: 30px 15px;
h2 {
font-size: 16px;
margin-bottom: 10px;
}
input {
border: 0;
outline: 0;
background: transparent;
color: $darkestgrey;
border-bottom: 1px solid $darkestgrey;
padding: 10px 5px;
width: 80%;
}
.submit {
display: inline-block;
vertical-align: middle;
width: 15%;
margin-left: 2.5%;
font-size: 12px;
&:hover {
cursor: pointer;
color: $main;
}
}
}
.footer {
padding: 10px 15px;
border-top: 1px solid $darkgrey;
font-size: 14px;
span {
&.amount {
&:hover {
input {
border: 1px solid $darkgrey;
cursor: text;
}
}
input {
border: 1px solid transparent;
padding: 0;
outline: 0;
font-weight: bold;
font-size: 14px;
line-height: 14px;
transition: all 0.25s;
&.isEditing {
width: 100%;
border-bottom: 1px solid $darkgrey;
}
}
}
&:nth-child(2) {
margin-left: 2px;
}
}
.caption svg {
font-size: 10px;
color: $darkestgrey;
}
}
@-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: 20px;
width: 20px;
-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;
}

35
app/components/Contacts/LoadingContact.js

@ -0,0 +1,35 @@
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

34
app/components/Contacts/OfflineContact.js

@ -0,0 +1,34 @@
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

@ -0,0 +1,34 @@
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

0
app/components/Contacts/OnlineContact.scss

39
app/components/Contacts/PendingContact.js

@ -0,0 +1,39 @@
import { shell } from 'electron'
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 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={() => shell.openExternal(`${'https://testnet.smartbit.com.au'}/tx/${channel.channel.channel_point.split(':')[0]}`)}>
(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

4
app/components/Form/PayForm.js

@ -9,7 +9,9 @@ import styles from './PayForm.scss'
class PayForm extends Component { class PayForm extends Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { isOnchain, isLn, payform: { payInput }, fetchInvoice } = this.props const {
isOnchain, isLn, payform: { payInput }, fetchInvoice
} = this.props
// If on-chain, focus on amount to let user know it's editable // If on-chain, focus on amount to let user know it's editable
if (isOnchain) { this.amountInput.focus() } if (isOnchain) { this.amountInput.focus() }

6
app/components/Form/RequestForm.js

@ -28,7 +28,9 @@ const RequestForm = ({
/> />
</section> </section>
<section className={styles.inputContainer}> <section className={styles.inputContainer}>
<label htmlFor='memo'>Request:</label> <label htmlFor='memo'>
Request:
</label>
<input <input
type='text' type='text'
placeholder='Dinner, Rent, etc' placeholder='Dinner, Rent, etc'
@ -39,7 +41,7 @@ const RequestForm = ({
</section> </section>
<section className={styles.buttonGroup}> <section className={styles.buttonGroup}>
<div className={`buttonPrimary ${styles.button}`} onClick={onRequestSubmit}> <div className={`buttonPrimary ${styles.button}`} onClick={onRequestSubmit}>
Request Request
</div> </div>
</section> </section>
</div> </div>

6
app/components/ModalRoot/ModalRoot.js

@ -13,7 +13,9 @@ const MODAL_COMPONENTS = {
/* other modals */ /* other modals */
} }
const ModalRoot = ({ modalType, modalProps, hideModal, currentTicker, currency }) => { const ModalRoot = ({
modalType, modalProps, hideModal, currentTicker, currency
}) => {
if (!modalType) { return null } if (!modalType) { return null }
const SpecificModal = MODAL_COMPONENTS[modalType] const SpecificModal = MODAL_COMPONENTS[modalType]
@ -37,7 +39,7 @@ const ModalRoot = ({ modalType, modalProps, hideModal, currentTicker, currency }
ModalRoot.propTypes = { ModalRoot.propTypes = {
modalType: PropTypes.string, modalType: PropTypes.string,
modalProps: PropTypes.object.isRequired, modalProps: PropTypes.object,
hideModal: PropTypes.func.isRequired, hideModal: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired, currentTicker: PropTypes.object.isRequired,
currency: PropTypes.string.isRequired currency: PropTypes.string.isRequired

4
app/components/ModalRoot/SuccessfulSendCoins.js

@ -5,7 +5,9 @@ import AnimatedCheckmark from 'components/AnimatedCheckmark'
import { btc } from 'utils' import { btc } from 'utils'
import styles from './SuccessfulSendCoins.scss' import styles from './SuccessfulSendCoins.scss'
const SuccessfulSendCoins = ({ amount, addr, txid, hideModal, currentTicker, currency }) => { const SuccessfulSendCoins = ({
amount, addr, txid, hideModal, currentTicker, currency
}) => {
const calculatedAmount = currency === 'usd' ? btc.satoshisToUsd(amount, currentTicker.price_usd) : btc.satoshisToBtc(amount) const calculatedAmount = currency === 'usd' ? btc.satoshisToUsd(amount, currentTicker.price_usd) : btc.satoshisToBtc(amount)
return ( return (

15
app/components/Nav/Nav.js

@ -5,7 +5,6 @@ import Isvg from 'react-inlinesvg'
import walletIcon from 'icons/wallet.svg' import walletIcon from 'icons/wallet.svg'
import peersIcon from 'icons/peers.svg' import peersIcon from 'icons/peers.svg'
import channelsIcon from 'icons/channels.svg'
import networkIcon from 'icons/globe.svg' import networkIcon from 'icons/globe.svg'
import styles from './Nav.scss' import styles from './Nav.scss'
@ -18,28 +17,18 @@ const Nav = ({ openPayForm, openRequestForm }) => (
</header> </header>
<ul className={styles.links}> <ul className={styles.links}>
<NavLink exact to='/' activeClassName={styles.active} className={styles.link}> <NavLink exact to='/' activeClassName={styles.active} className={styles.link}>
<span className={styles.activeBorder} />
<li> <li>
<Isvg styles={{ verticalAlign: 'middle' }} src={walletIcon} /> <Isvg styles={{ verticalAlign: 'middle' }} src={walletIcon} />
<span>Wallet</span> <span>Wallet</span>
</li> </li>
</NavLink> </NavLink>
<NavLink exact to='/peers' activeClassName={styles.active} className={styles.link}> <NavLink exact to='/contacts' activeClassName={styles.active} className={styles.link}>
<span className={styles.activeBorder} />
<li> <li>
<Isvg styles={{ verticalAlign: 'middle' }} src={peersIcon} /> <Isvg styles={{ verticalAlign: 'middle' }} src={peersIcon} />
<span>Peers</span> <span>Contacts</span>
</li>
</NavLink>
<NavLink exact to='/channels' activeClassName={styles.active} className={styles.link}>
<span className={styles.activeBorder} />
<li>
<Isvg styles={{ verticalAlign: 'middle' }} src={channelsIcon} />
<span>Channels</span>
</li> </li>
</NavLink> </NavLink>
<NavLink exact to='/network' activeClassName={styles.active} className={styles.link}> <NavLink exact to='/network' activeClassName={styles.active} className={styles.link}>
<span className={styles.activeBorder} />
<li> <li>
<Isvg styles={{ verticalAlign: 'middle' }} src={networkIcon} /> <Isvg styles={{ verticalAlign: 'middle' }} src={networkIcon} />
<span>Network</span> <span>Network</span>

16
app/components/Nav/Nav.scss

@ -136,27 +136,17 @@
border-left: 20px solid transparent; border-left: 20px solid transparent;
transition: all 0.25s; transition: all 0.25s;
.activeBorder {
position: absolute;
left: -30px;
width: 10px;
height: 100%;
margin: 0;
background: $main;
transition: all 0.25s;
}
li { li {
margin: 12.5px 0; margin: 12.5px 0;
min-width: 200px; min-width: 200px;
} }
&.active { &.active {
color: $white; color: $main;
opacity: 1.0; opacity: 1.0;
.activeBorder { svg g {
left: -10px; stroke: $main;
} }
} }

1
app/components/Network/CanvasNetworkGraph.js

@ -23,7 +23,6 @@ class CanvasNetworkGraph extends Component {
super(props) super(props)
this.state = { this.state = {
simulation: {},
simulationData: { simulationData: {
nodes: [], nodes: [],
links: [] links: []

4
app/components/Network/TransactionForm.js

@ -3,7 +3,9 @@ import PropTypes from 'prop-types'
import { btc } from 'utils' import { btc } from 'utils'
import styles from './TransactionForm.scss' import styles from './TransactionForm.scss'
const TransactionForm = ({ updatePayReq, pay_req, loadingRoutes, payReqRoutes, setCurrentRoute, currentRoute }) => ( const TransactionForm = ({
updatePayReq, pay_req, loadingRoutes, payReqRoutes, setCurrentRoute, currentRoute
}) => (
<div className={styles.transactionForm}> <div className={styles.transactionForm}>
<div className={styles.form}> <div className={styles.form}>
<input <input

20
app/components/Peers/Peer.js

@ -1,20 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import styles from './Peer.scss'
const Peer = ({ peer, setPeer }) => (
<li className={styles.peer} onClick={() => setPeer(peer)}>
<h4>{peer.address}</h4>
<h1>{peer.pub_key}</h1>
</li>
)
Peer.propTypes = {
peer: PropTypes.shape({
address: PropTypes.string.isRequired,
pub_key: PropTypes.string.isRequired
}).isRequired,
setPeer: PropTypes.func.isRequired
}
export default Peer

38
app/components/Peers/Peer.scss

@ -1,38 +0,0 @@
@import '../../variables.scss';
.peer {
position: relative;
margin: 5px 0;
padding: 10px;
border-top: 1px solid $white;
cursor: pointer;
transition: all 0.25s;
list-style: none;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
transition: 0.3s;
&:hover {
opacity: 0.75;
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
}
&:first-child {
border: none;
}
h4, h1 {
margin: 10px 0;
}
h4 {
font-size: 14px;
font-weight: bold;
color: $black;
}
h1 {
font-size: 18px;
font-weight: 200;
color: $main;
}
}

71
app/components/Peers/PeerForm.js

@ -1,71 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactModal from 'react-modal'
import { FaClose } from 'react-icons/lib/fa'
import styles from './PeerForm.scss'
const PeerForm = ({ form, setForm, connect }) => {
const submit = () => {
const { pubkey, host } = form
connect({ pubkey, host })
}
return (
<div>
<ReactModal
isOpen={form.isOpen}
contentLabel='No Overlay Click Modal'
ariaHideApp
shouldCloseOnOverlayClick
onRequestClose={() => setForm({ isOpen: false })}
parentSelector={() => document.body}
className={styles.modal}
>
<div onClick={() => setForm({ isOpen: false })} className={styles.modalClose}>
<FaClose />
</div>
<div className={styles.form} onKeyPress={event => event.charCode === 13 && submit()}>
<h1 className={styles.title}>Connect to a peer</h1>
<section className={styles.pubkey}>
<label htmlFor='pubkey'>Pubkey</label>
<input
type='text'
size=''
placeholder='Public key'
value={form.pubkey}
onChange={event => setForm({ pubkey: event.target.value })}
id='pubkey'
/>
</section>
<section className={styles.local}>
<label htmlFor='address'>Address</label>
<input
type='text'
size=''
placeholder='Host address'
value={form.host}
onChange={event => setForm({ host: event.target.value })}
id='address'
/>
</section>
<div className='buttonContainer' onClick={submit}>
<div className='buttonPrimary'>
Submit
</div>
</div>
</div>
</ReactModal>
</div>
)
}
PeerForm.propTypes = {
form: PropTypes.object.isRequired,
setForm: PropTypes.func.isRequired,
connect: PropTypes.func.isRequired
}
export default PeerForm

107
app/components/Peers/PeerForm.scss

@ -1,107 +0,0 @@
@import '../../variables.scss';
.modal {
position: relative;
width: 40%;
margin: 50px auto;
padding: 40px;
position: absolute;
top: auto;
left: 20%;
right: 0;
bottom: auto;
background: $white;
outline: none;
z-index: -2;
border: 1px solid $darkgrey;
}
.modalClose {
position: absolute;
top: -13px;
right: -13px;
display: block;
font-size: 16px;
line-height: 27px;
width: 32px;
height: 32px;
background: $white;
border-radius: 50%;
color: $darkestgrey;
cursor: pointer;
text-align: center;
z-index: 2;
transition: all 0.25s;
}
.modalClose:hover {
background: $darkgrey;
}
.title {
text-align: center;
font-size: 24px;
color: $black;
margin-bottom: 50px;
}
.pubkey, .local, .push {
display: flex;
justify-content: center;
font-size: 18px;
height: auto;
min-height: 55px;
margin-bottom: 20px;
border: 1px solid $traditionalgrey;
border-radius: 6px;
position: relative;
padding: 0 20px;
label, input[type=text] {
font-size: inherit;
}
label {
padding-top: 19px;
padding-bottom: 12px;
color: $traditionalgrey;
}
input[type=text] {
width: 100%;
border: none;
outline: 0;
-webkit-appearance: none;
height: 55px;
padding: 0 10px;
}
}
.buttonGroup {
width: 100%;
display: flex;
flex-direction: row;
border-radius: 6px;
overflow: hidden;
.button {
cursor: pointer;
height: 55px;
min-height: 55px;
text-transform: none;
font-size: 18px;
transition: opacity .2s ease-out;
background: $main;
color: $white;
border: none;
font-weight: 500;
padding: 0;
width: 100%;
text-align: center;
line-height: 55px;
&:first-child {
border-right: 1px solid lighten($main, 20%);
}
}
}

78
app/components/Peers/PeerModal.js

@ -1,78 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactModal from 'react-modal'
import { FaClose } from 'react-icons/lib/fa'
import styles from './PeerModal.scss'
const PeerModal = ({ isOpen, resetPeer, peer, disconnect }) => {
const customStyles = {
overlay: {
cursor: 'pointer',
overflowY: 'auto'
},
content: {
top: 'auto',
left: '20%',
right: '0',
bottom: 'auto',
width: '40%',
margin: '50px auto',
borderRadius: 'none',
padding: '0'
}
}
return (
<ReactModal
isOpen={isOpen}
contentLabel='No Overlay Click Modal'
ariaHideApp
shouldCloseOnOverlayClick
onRequestClose={() => resetPeer(null)}
parentSelector={() => document.body}
style={customStyles}
>
<div className={styles.closeContainer}>
<span onClick={() => resetPeer(null)}>
<FaClose />
</span>
</div>
{
peer &&
<div className={styles.peer}>
<header className={styles.header}>
<h1 data-hint='Peer address' className='hint--top-left'>{peer.address}</h1>
<h2 data-hint='Peer public key' className='hint--top-left'>{peer.pub_key}</h2>
</header>
<div className={styles.details}>
<dl>
<dt>Satoshis Received</dt>
<dd>{peer.sat_recv}</dd>
<dt>Satoshis Sent</dt>
<dd>{peer.sat_sent}</dd>
<dt>Bytes Received</dt>
<dd>{peer.bytes_recv}</dd>
<dt>Bytes Sent</dt>
<dd>{peer.bytes_sent}</dd>
</dl>
</div>
<div className={styles.close} onClick={() => disconnect({ pubkey: peer.pub_key })}>
<div>Disconnect peer</div>
</div>
</div>
}
</ReactModal>
)
}
PeerModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
resetPeer: PropTypes.func.isRequired,
peer: PropTypes.object,
disconnect: PropTypes.func.isRequired
}
export default PeerModal

75
app/components/Peers/PeerModal.scss

@ -1,75 +0,0 @@
@import '../../variables.scss';
.closeContainer {
background: $lightgrey;
text-align: right;
padding: 10px;
span {
color: $darkestgrey;
font-size: 20px;
cursor: pointer;
}
}
.header {
background: $lightgrey;
padding: 20px;
h1 {
color: $black;
text-align: center;
margin-bottom: 20px;
font-weight: bold;
}
h2 {
color: $darkestgrey;
font-size: 12px;
text-align: center;
}
}
.details {
dl {
padding: 40px 40px 40px 40px;
}
dt {
text-align: left;
float: left;
clear: left;
font-weight: 500;
padding: 20px 35px 19px 0;
color: $black;
font-weight: bold;
}
dd {
text-align: right;
font-weight: 400;
padding: 30px 0 10px 0;
margin-left: 0;
border-bottom: 1px solid $darkgrey;
}
}
.close {
text-align: center;
padding-bottom: 40px;
div {
margin: 0 auto;
cursor: pointer;
font-size: 18px;
color: $red;
border: none;
padding: 0;
text-align: center;
transition: all 0.25s;
&:hover {
color: lighten($red, 10%);
}
}
}

4
app/components/Wallet/ReceiveModal.js

@ -7,7 +7,9 @@ import { showNotification } from 'notifications'
import { FaCopy, FaClose } from 'react-icons/lib/fa' import { FaCopy, FaClose } from 'react-icons/lib/fa'
import styles from './ReceiveModal.scss' import styles from './ReceiveModal.scss'
const ReceiveModal = ({ isOpen, hideActivityModal, pubkey, address, newAddress, qrCodeType, changeQrCode }) => { const ReceiveModal = ({
isOpen, hideActivityModal, pubkey, address, newAddress, qrCodeType, changeQrCode
}) => {
const customStyles = { const customStyles = {
overlay: { overlay: {
cursor: 'pointer' cursor: 'pointer'

2
app/components/Wallet/Wallet.js

@ -63,7 +63,7 @@ class Wallet extends Component {
</div> </div>
<div className={styles.right}> <div className={styles.right}>
<div className={styles.rightContent}> <div className={styles.rightContent}>
<div className={'buttonPrimary'} onClick={() => this.setState({ modalOpen: true })}> <div className='buttonPrimary' onClick={() => this.setState({ modalOpen: true })}>
<FaQrcode /> <FaQrcode />
Address Address
</div> </div>

1
app/icons/plus.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-plus"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>

After

Width:  |  Height:  |  Size: 304 B

73
app/lnd/methods/channelController.js

@ -1,9 +1,55 @@
import bitcore from 'bitcore-lib' import bitcore from 'bitcore-lib'
import find from 'lodash/find'
import { listPeers, connectPeer } from './peersController'
import pushopenchannel from '../push/openchannel' import pushopenchannel from '../push/openchannel'
import pushclosechannel from '../push/closechannel'
const BufferUtil = bitcore.util.buffer const BufferUtil = bitcore.util.buffer
/**
* Attempts to open a singly funded channel specified in the request to a remote peer.
* @param {[type]} lnd [description]
* @param {[type]} event [description]
* @param {[type]} payload [description]
* @return {[type]} [description]
*/
export function connectAndOpen(lnd, meta, event, payload) {
const { pubkey, host, localamt } = payload
const channelPayload = {
node_pubkey: BufferUtil.hexToBuffer(pubkey),
local_funding_amount: Number(localamt)
}
return new Promise((resolve, reject) => {
listPeers(lnd, meta)
.then(({ peers }) => {
const peer = find(peers, { pub_key: pubkey })
if (peer) {
const call = lnd.openChannel(channelPayload, meta)
call.on('data', data => event.sender.send('pushchannelupdated', { pubkey, data }))
call.on('error', error => event.sender.send('pushchannelerror', { pubkey, error: error.toString() }))
} else {
connectPeer(lnd, meta, { pubkey, host })
.then(() => {
const call = lnd.openChannel(channelPayload, meta)
call.on('data', data => event.sender.send('pushchannelupdated', { pubkey, data }))
call.on('error', error => event.sender.send('pushchannelerror', { pubkey, error: error.toString() }))
})
.catch((err) => {
event.sender.send('pushchannelerror', { pubkey, error: err.toString() })
reject(err)
})
}
})
.catch((err) => {
event.sender.send('pushchannelerror', { pubkey, error: err.toString() })
reject(err)
})
})
}
/** /**
* Attempts to open a singly funded channel specified in the request to a remote peer. * Attempts to open a singly funded channel specified in the request to a remote peer.
* @param {[type]} lnd [description] * @param {[type]} lnd [description]
@ -22,8 +68,7 @@ export function openChannel(lnd, meta, event, payload) {
return new Promise((resolve, reject) => return new Promise((resolve, reject) =>
pushopenchannel(lnd, meta, event, res) pushopenchannel(lnd, meta, event, res)
.then(data => resolve(data)) .then(data => resolve(data))
.catch(error => reject(error)) .catch(error => reject(error)))
)
} }
@ -67,20 +112,30 @@ export function listChannels(lnd, meta) {
* @return {[type]} [description] * @return {[type]} [description]
*/ */
export function closeChannel(lnd, meta, event, payload) { export function closeChannel(lnd, meta, event, payload) {
const { chan_id, force } = payload
const tx = payload.channel_point.funding_txid.match(/.{2}/g).reverse().join('') const tx = payload.channel_point.funding_txid.match(/.{2}/g).reverse().join('')
const res = { const res = {
channel_point: { channel_point: {
funding_txid: BufferUtil.hexToBuffer(tx), funding_txid: BufferUtil.hexToBuffer(tx),
output_index: Number(payload.channel_point.output_index) output_index: Number(payload.channel_point.output_index)
}, },
force: true force
} }
return new Promise((resolve, reject) => return new Promise((resolve, reject) => {
pushclosechannel(lnd, meta, event, res) try {
.then(data => resolve(data)) const call = lnd.closeChannel(res, meta)
.catch(error => reject(error))
) call.on('data', data => event.sender.send('pushclosechannelupdated', { data, chan_id }))
call.on('end', () => event.sender.send('pushclosechannelend'))
call.on('error', error => event.sender.send('pushclosechannelerror', { error: error.toString(), chan_id }))
call.on('status', status => event.sender.send('pushclosechannelstatus', { status, chan_id }))
resolve(null, res)
} catch (error) {
reject(error, null)
}
})
} }

26
app/lnd/methods/index.js

@ -18,11 +18,6 @@ import * as networkController from './networkController'
// TODO - SendPayment // TODO - SendPayment
// TODO - DeleteAllPayments // TODO - DeleteAllPayments
// const metadata = new grpc.Metadata()
// var macaroonHex = fs.readFileSync('~/Library/Application Support/Lnd/admin.macaroon').toString('hex')
// metadata.add('macaroon', macaroonHex)
export default function (lnd, meta, event, msg, data) { export default function (lnd, meta, event, msg, data) {
switch (msg) { switch (msg) {
case 'info': case 'info':
@ -79,8 +74,7 @@ export default function (lnd, meta, event, msg, data) {
// [ { channels: [] }, { total_limbo_balance: 0, pending_open_channels: [], pending_closing_channels: [], pending_force_closing_channels: [] } ] // [ { channels: [] }, { total_limbo_balance: 0, pending_open_channels: [], pending_closing_channels: [], pending_force_closing_channels: [] } ]
Promise.all([channelController.listChannels, channelController.pendingChannels].map(func => func(lnd, meta))) Promise.all([channelController.listChannels, channelController.pendingChannels].map(func => func(lnd, meta)))
.then(channelsData => .then(channelsData =>
event.sender.send('receiveChannels', { channels: channelsData[0].channels, pendingChannels: channelsData[1] }) event.sender.send('receiveChannels', { channels: channelsData[0].channels, pendingChannels: channelsData[1] }))
)
.catch(error => console.log('channels error: ', error)) .catch(error => console.log('channels error: ', error))
break break
case 'transactions': case 'transactions':
@ -125,11 +119,10 @@ export default function (lnd, meta, event, msg, data) {
Object.assign(newinvoice, { Object.assign(newinvoice, {
memo: data.memo, memo: data.memo,
value: data.value, value: data.value,
r_hash: new Buffer(newinvoice.r_hash, 'hex').toString('hex'), r_hash: Buffer.from(newinvoice.r_hash, 'hex').toString('hex'),
creation_date: Date.now() / 1000 creation_date: Date.now() / 1000
}) })
) ))
)
.catch((error) => { .catch((error) => {
console.log('addInvoice error: ', error) console.log('addInvoice error: ', error)
event.sender.send('invoiceFailed', { error: error.toString() }) event.sender.send('invoiceFailed', { error: error.toString() })
@ -201,6 +194,19 @@ export default function (lnd, meta, event, msg, data) {
}) })
.catch(error => console.log('disconnectPeer error: ', error)) .catch(error => console.log('disconnectPeer error: ', error))
break break
case 'connectAndOpen':
// Connects to a peer if we aren't connected already and then attempt to open a channel
// {} = data
channelController.connectAndOpen(lnd, meta, event, data)
.then((channelData) => {
console.log('connectAndOpen data: ', channelData)
// event.sender.send('connectSuccess', { pub_key: data.pubkey, address: data.host, peer_id })
})
.catch((error) => {
// event.sender.send('connectFailure', { error: error.toString() })
console.log('connectAndOpen error: ', error)
})
break
default: default:
} }
} }

4
app/lnd/methods/paymentsController.js

@ -8,11 +8,11 @@ export function sendPaymentSync(lnd, meta, { paymentRequest }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.sendPaymentSync({ payment_request: paymentRequest }, meta, (error, data) => { lnd.sendPaymentSync({ payment_request: paymentRequest }, meta, (error, data) => {
if (error) { if (error) {
reject({ error }) reject({ error }) // eslint-disable-line
return return
} }
if (!data || !data.payment_route) { reject({ error: data.payment_error }) } if (!data || !data.payment_route) { reject({ error: data.payment_error }) } // eslint-disable-line
resolve(data) resolve(data)
}) })

2
app/lnd/methods/peersController.js

@ -7,7 +7,7 @@
*/ */
export function connectPeer(lnd, meta, { pubkey, host }) { export function connectPeer(lnd, meta, { pubkey, host }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lnd.connectPeer({ addr: { pubkey, host }, perm: true }, meta, (err, data) => { lnd.connectPeer({ addr: { pubkey, host } }, meta, (err, data) => {
if (err) { reject(err) } if (err) { reject(err) }
resolve(data) resolve(data)

5
app/lnd/subscribe/channelgraph.js

@ -1,11 +1,6 @@
export default function subscribeToChannelGraph(mainWindow, lnd, meta) { export default function subscribeToChannelGraph(mainWindow, lnd, meta) {
console.log('subscribeChannelGraph is happening')
const call = lnd.subscribeChannelGraph({}, meta) const call = lnd.subscribeChannelGraph({}, meta)
call.on('data', channelGraphData => mainWindow.send('channelGraphData', { channelGraphData })) call.on('data', channelGraphData => mainWindow.send('channelGraphData', { channelGraphData }))
call.on('end', () => console.log('channel graph end'))
call.on('error', error => console.log('channelgraph error: ', error))
call.on('status', channelGraphStatus => mainWindow.send('channelGraphStatus', { channelGraphStatus })) call.on('status', channelGraphStatus => mainWindow.send('channelGraphStatus', { channelGraphStatus }))
} }

2
app/main.dev.js

@ -169,7 +169,7 @@ export const startLnd = () => {
'--bitcoin.active', '--bitcoin.active',
'--bitcoin.testnet', '--bitcoin.testnet',
'--neutrino.active', '--neutrino.active',
'--neutrino.connect=btcd.jackmallers.com:18333', '--neutrino.connect=btcd0.lightning.computer:18333',
'--autopilot.active', '--autopilot.active',
'--debuglevel=debug', '--debuglevel=debug',
'--noencryptwallet' '--noencryptwallet'

4
app/reducers/balance.js

@ -31,7 +31,9 @@ export const receiveBalance = (event, { walletBalance, channelBalance }) => (dis
const ACTION_HANDLERS = { const ACTION_HANDLERS = {
[GET_BALANCE]: state => ({ ...state, balanceLoading: true }), [GET_BALANCE]: state => ({ ...state, balanceLoading: true }),
[RECEIVE_BALANCE]: (state, { walletBalance, channelBalance }) => ( [RECEIVE_BALANCE]: (state, { walletBalance, channelBalance }) => (
{ ...state, balanceLoading: false, walletBalance, channelBalance } {
...state, balanceLoading: false, walletBalance, channelBalance
}
) )
} }

239
app/reducers/channels.js

@ -1,5 +1,6 @@
import { createSelector } from 'reselect' import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron' import { ipcRenderer } from 'electron'
import filter from 'lodash/filter'
import { btc } from 'utils' import { btc } from 'utils'
import { showNotification } from 'notifications' import { showNotification } from 'notifications'
import { closeChannelForm, resetChannelForm } from './channelform' import { closeChannelForm, resetChannelForm } from './channelform'
@ -29,6 +30,15 @@ export const SET_VIEW_TYPE = 'SET_VIEW_TYPE'
export const TOGGLE_CHANNEL_PULLDOWN = 'TOGGLE_CHANNEL_PULLDOWN' export const TOGGLE_CHANNEL_PULLDOWN = 'TOGGLE_CHANNEL_PULLDOWN'
export const CHANGE_CHANNEL_FILTER = 'CHANGE_CHANNEL_FILTER' export const CHANGE_CHANNEL_FILTER = 'CHANGE_CHANNEL_FILTER'
export const ADD_LOADING_PUBKEY = 'ADD_LOADING_PUBKEY'
export const REMOVE_LOADING_PUBKEY = 'REMOVE_LOADING_PUBKEY'
export const ADD_ClOSING_CHAN_ID = 'ADD_ClOSING_CHAN_ID'
export const REMOVE_ClOSING_CHAN_ID = 'REMOVE_ClOSING_CHAN_ID'
export const OPEN_CONTACT_MODAL = 'OPEN_CONTACT_MODAL'
export const CLOSE_CONTACT_MODAL = 'CLOSE_CONTACT_MODAL'
// ------------------------------------ // ------------------------------------
// Actions // Actions
// ------------------------------------ // ------------------------------------
@ -91,6 +101,47 @@ export function setViewType(viewType) {
} }
} }
export function addLoadingPubkey(pubkey) {
return {
type: ADD_LOADING_PUBKEY,
pubkey
}
}
export function removeLoadingPubkey(pubkey) {
return {
type: REMOVE_LOADING_PUBKEY,
pubkey
}
}
export function addClosingChanId(chanId) {
return {
type: ADD_ClOSING_CHAN_ID,
chanId
}
}
export function removeClosingChanId(chanId) {
return {
type: REMOVE_ClOSING_CHAN_ID,
chanId
}
}
export function openContactModal(channel) {
return {
type: OPEN_CONTACT_MODAL,
channel
}
}
export function closeContactModal() {
return {
type: CLOSE_CONTACT_MODAL
}
}
// Send IPC event for peers // Send IPC event for peers
export const fetchChannels = () => async (dispatch) => { export const fetchChannels = () => async (dispatch) => {
dispatch(getChannels()) dispatch(getChannels())
@ -101,62 +152,67 @@ export const fetchChannels = () => async (dispatch) => {
export const receiveChannels = (event, { channels, pendingChannels }) => dispatch => dispatch({ type: RECEIVE_CHANNELS, channels, pendingChannels }) export const receiveChannels = (event, { channels, pendingChannels }) => dispatch => dispatch({ type: RECEIVE_CHANNELS, channels, pendingChannels })
// Send IPC event for opening a channel // Send IPC event for opening a channel
export const openChannel = ({ pubkey, local_amt, push_amt }) => (dispatch) => { export const openChannel = ({
pubkey, host, local_amt
}) => (dispatch) => {
const localamt = btc.btcToSatoshis(local_amt) const localamt = btc.btcToSatoshis(local_amt)
const pushamt = btc.btcToSatoshis(push_amt)
dispatch(openingChannel()) dispatch(openingChannel())
ipcRenderer.send('lnd', { msg: 'openChannel', data: { pubkey, localamt, pushamt } }) dispatch(addLoadingPubkey(pubkey))
ipcRenderer.send('lnd', { msg: 'connectAndOpen', data: { pubkey, host, localamt } })
} }
// TODO: Decide how to handle streamed updates for channels // TODO: Decide how to handle streamed updates for channels
// Receive IPC event for openChannel // Receive IPC event for openChannel
export const channelSuccessful = () => (dispatch) => { export const channelSuccessful = () => (dispatch) => {
console.log('CHANNEL channelSuccessful')
dispatch(fetchChannels()) dispatch(fetchChannels())
dispatch(closeChannelForm()) dispatch(closeChannelForm())
dispatch(resetChannelForm()) dispatch(resetChannelForm())
} }
// Receive IPC event for updated channel // Receive IPC event for updated channel
export const pushchannelupdated = (event, data) => (dispatch) => { export const pushchannelupdated = (event, { pubkey }) => (dispatch) => {
console.log('PUSH CHANNEL UPDATED: ', data)
dispatch(fetchChannels()) dispatch(fetchChannels())
dispatch(removeLoadingPubkey(pubkey))
} }
// Receive IPC event for channel end // Receive IPC event for channel end
export const pushchannelend = event => (dispatch) => { // eslint-disable-line export const pushchannelend = event => (dispatch) => { // eslint-disable-line
console.log('PUSH CHANNEL END: ')
dispatch(fetchChannels()) dispatch(fetchChannels())
} }
// Receive IPC event for channel error // Receive IPC event for channel error
export const pushchannelerror = (event, { error }) => (dispatch) => { export const pushchannelerror = (event, { pubkey, error }) => (dispatch) => {
console.log('PUSH CHANNEL ERROR: ', error)
dispatch(openingFailure()) dispatch(openingFailure())
dispatch(setError(error)) dispatch(setError(error))
dispatch(removeLoadingPubkey(pubkey))
} }
// Receive IPC event for channel status // Receive IPC event for channel status
export const pushchannelstatus = (event, data) => (dispatch) => { // eslint-disable-line export const pushchannelstatus = (event, data) => (dispatch) => { // eslint-disable-line
console.log('PUSH CHANNEL STATUS: ', data)
dispatch(fetchChannels()) dispatch(fetchChannels())
} }
// Send IPC event for opening a channel // Send IPC event for opening a channel
export const closeChannel = ({ channel_point }) => (dispatch) => { export const closeChannel = ({ channel_point, chan_id, force }) => (dispatch) => {
dispatch(closingChannel()) dispatch(closingChannel())
const channelPoint = channel_point.split(':') dispatch(addClosingChanId(chan_id))
console.log('force: ', force)
const [funding_txid, output_index] = channel_point.split(':')
ipcRenderer.send( ipcRenderer.send(
'lnd', 'lnd',
{ {
msg: 'closeChannel', msg: 'closeChannel',
data: { data: {
channel_point: { channel_point: {
funding_txid: channelPoint[0], funding_txid,
output_index: channelPoint[1] output_index
}, },
force: true force,
chan_id
} }
} }
) )
@ -164,38 +220,36 @@ export const closeChannel = ({ channel_point }) => (dispatch) => {
// TODO: Decide how to handle streamed updates for closing channels // TODO: Decide how to handle streamed updates for closing channels
// Receive IPC event for closeChannel // Receive IPC event for closeChannel
export const closeChannelSuccessful = (event, data) => (dispatch) => { export const closeChannelSuccessful = () => (dispatch) => {
console.log('PUSH CLOSE CHANNEL SUCCESSFUL: ', data)
dispatch(fetchChannels()) dispatch(fetchChannels())
} }
// Receive IPC event for updated closing channel // Receive IPC event for updated closing channel
export const pushclosechannelupdated = (event, data) => (dispatch) => { export const pushclosechannelupdated = (event, { chan_id }) => (dispatch) => {
console.log('PUSH CLOSE CHANNEL UPDATED: ', data)
dispatch(fetchChannels()) dispatch(fetchChannels())
dispatch(removeClosingChanId(chan_id))
dispatch(closeContactModal())
} }
// Receive IPC event for closing channel end // Receive IPC event for closing channel end
export const pushclosechannelend = (event, data) => (dispatch) => { export const pushclosechannelend = () => (dispatch) => {
console.log('PUSH CLOSE CHANNEL END: ', data)
dispatch(fetchChannels()) dispatch(fetchChannels())
} }
// Receive IPC event for closing channel error // Receive IPC event for closing channel error
export const pushclosechannelerror = (event, data) => (dispatch) => { export const pushclosechannelerror = (event, { error, chan_id }) => (dispatch) => {
console.log('PUSH CLOSE CHANNEL END: ', data) dispatch(setError(error))
dispatch(fetchChannels()) dispatch(removeClosingChanId(chan_id))
} }
// Receive IPC event for closing channel status // Receive IPC event for closing channel status
export const pushclosechannelstatus = (event, data) => (dispatch) => { export const pushclosechannelstatus = () => (dispatch) => {
console.log('PUSH CLOSE CHANNEL STATUS: ', data)
dispatch(fetchChannels()) dispatch(fetchChannels())
} }
// IPC event for channel graph data // IPC event for channel graph data
export const channelGraphData = (event, data) => (dispatch, getState) => { export const channelGraphData = (event, data) => (dispatch, getState) => {
const info = getState().info const { info } = getState()
const { channelGraphData: { channel_updates } } = data const { channelGraphData: { channel_updates } } = data
// if there are any new channel updates // if there are any new channel updates
@ -228,9 +282,7 @@ export const channelGraphData = (event, data) => (dispatch, getState) => {
} }
// IPC event for channel graph status // IPC event for channel graph status
export const channelGraphStatus = (event, data) => () => { export const channelGraphStatus = () => () => {}
console.log('channelGraphStatus: ', data)
}
export function toggleFilterPulldown() { export function toggleFilterPulldown() {
return { return {
@ -238,10 +290,10 @@ export function toggleFilterPulldown() {
} }
} }
export function changeFilter(filter) { export function changeFilter(channelFilter) {
return { return {
type: CHANGE_CHANNEL_FILTER, type: CHANGE_CHANNEL_FILTER,
filter channelFilter
} }
} }
@ -257,7 +309,9 @@ const ACTION_HANDLERS = {
[GET_CHANNELS]: state => ({ ...state, channelsLoading: true }), [GET_CHANNELS]: state => ({ ...state, channelsLoading: true }),
[RECEIVE_CHANNELS]: (state, { channels, pendingChannels }) => ( [RECEIVE_CHANNELS]: (state, { channels, pendingChannels }) => (
{ ...state, channelsLoading: false, channels, pendingChannels } {
...state, channelsLoading: false, channels, pendingChannels
}
), ),
[OPENING_CHANNEL]: state => ({ ...state, openingChannel: true }), [OPENING_CHANNEL]: state => ({ ...state, openingChannel: true }),
@ -270,7 +324,22 @@ const ACTION_HANDLERS = {
[SET_VIEW_TYPE]: (state, { viewType }) => ({ ...state, viewType }), [SET_VIEW_TYPE]: (state, { viewType }) => ({ ...state, viewType }),
[TOGGLE_CHANNEL_PULLDOWN]: state => ({ ...state, filterPulldown: !state.filterPulldown }), [TOGGLE_CHANNEL_PULLDOWN]: state => ({ ...state, filterPulldown: !state.filterPulldown }),
[CHANGE_CHANNEL_FILTER]: (state, { filter }) => ({ ...state, filterPulldown: false, filter }) [CHANGE_CHANNEL_FILTER]: (state, { channelFilter }) => (
{ ...state, filterPulldown: false, filter: channelFilter }
),
[ADD_LOADING_PUBKEY]: (state, { pubkey }) => ({ ...state, loadingChannelPubkeys: [pubkey, ...state.loadingChannelPubkeys] }),
[REMOVE_LOADING_PUBKEY]: (state, { pubkey }) => (
{ ...state, loadingChannelPubkeys: state.loadingChannelPubkeys.filter(loadingPubkey => loadingPubkey !== pubkey) }
),
[ADD_ClOSING_CHAN_ID]: (state, { chanId }) => ({ ...state, closingChannelIds: [chanId, ...state.closingChannelIds] }),
[REMOVE_ClOSING_CHAN_ID]: (state, { chanId }) => (
{ ...state, closingChannelIds: state.closingChannelIds.filter(closingChanId => closingChanId !== chanId) }
),
[OPEN_CONTACT_MODAL]: (state, { channel }) => ({ ...state, contactModal: { isOpen: true, channel } }),
[CLOSE_CONTACT_MODAL]: state => ({ ...state, contactModal: { isOpen: false, channel: null } })
} }
const channelsSelectors = {} const channelsSelectors = {}
@ -282,6 +351,7 @@ const pendingForceClosedChannelsSelector = state => state.channels.pendingChanne
const channelSearchQuerySelector = state => state.channels.searchQuery const channelSearchQuerySelector = state => state.channels.searchQuery
const filtersSelector = state => state.channels.filters const filtersSelector = state => state.channels.filters
const filterSelector = state => state.channels.filter const filterSelector = state => state.channels.filter
const nodesSelector = state => state.network.nodes
channelsSelectors.channelModalOpen = createSelector( channelsSelectors.channelModalOpen = createSelector(
channelSelector, channelSelector,
@ -293,27 +363,35 @@ channelsSelectors.activeChannels = createSelector(
openChannels => openChannels.filter(channel => channel.active) openChannels => openChannels.filter(channel => channel.active)
) )
const closingPendingChannels = createSelector( channelsSelectors.activeChannelPubkeys = createSelector(
pendingClosedChannelsSelector, channelsSelector,
pendingForceClosedChannelsSelector, openChannels => openChannels.filter(channel => channel.active).map(c => c.remote_pubkey)
(pendingClosedChannels, pendingForcedClosedChannels) => [...pendingClosedChannels, ...pendingForcedClosedChannels]
) )
const allChannels = createSelector( channelsSelectors.nonActiveChannels = createSelector(
channelsSelector,
openChannels => openChannels.filter(channel => !channel.active)
)
channelsSelectors.nonActiveChannelPubkeys = createSelector(
channelsSelector, channelsSelector,
openChannels => openChannels.filter(channel => !channel.active).map(c => c.remote_pubkey)
)
channelsSelectors.pendingOpenChannels = createSelector(
pendingOpenChannelsSelector, pendingOpenChannelsSelector,
pendingClosedChannelsSelector, pendingOpenChannels => pendingOpenChannels
pendingForceClosedChannelsSelector, )
channelSearchQuerySelector,
(channels, pendingOpenChannels, pendingClosedChannels, pendingForcedClosedChannels, searchQuery) => {
const filteredChannels = channels.filter(channel => channel.remote_pubkey.includes(searchQuery) || channel.channel_point.includes(searchQuery)) // eslint-disable-line
const filteredPendingOpenChannels = pendingOpenChannels.filter(channel => channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)) // eslint-disable-line
const filteredPendingClosedChannels = pendingClosedChannels.filter(channel => channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)) // eslint-disable-line
const filteredPendingForcedClosedChannels = pendingForcedClosedChannels.filter(channel => channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)) // eslint-disable-line
channelsSelectors.pendingOpenChannelPubkeys = createSelector(
pendingOpenChannelsSelector,
pendingOpenChannels => pendingOpenChannels.map(pendingChannel => pendingChannel.channel.remote_node_pub)
)
return [...filteredChannels, ...filteredPendingOpenChannels, ...filteredPendingClosedChannels, ...filteredPendingForcedClosedChannels] channelsSelectors.closingPendingChannels = createSelector(
} pendingClosedChannelsSelector,
pendingForceClosedChannelsSelector,
(pendingClosedChannels, pendingForcedClosedChannels) => [...pendingClosedChannels, ...pendingForcedClosedChannels]
) )
channelsSelectors.activeChanIds = createSelector( channelsSelectors.activeChanIds = createSelector(
@ -324,18 +402,49 @@ channelsSelectors.activeChanIds = createSelector(
channelsSelectors.nonActiveFilters = createSelector( channelsSelectors.nonActiveFilters = createSelector(
filtersSelector, filtersSelector,
filterSelector, filterSelector,
(filters, filter) => filters.filter(f => f.key !== filter.key) (filters, channelFilter) => filters.filter(f => f.key !== channelFilter.key)
)
channelsSelectors.channelNodes = createSelector(
channelsSelector,
nodesSelector,
(channels, nodes) => {
const chanPubkeys = channels.map(channel => channel.remote_pubkey)
return filter(nodes, node => chanPubkeys.includes(node.pub_key))
}
)
const allChannels = createSelector(
channelsSelectors.activeChannels,
channelsSelectors.nonActiveChannels,
pendingOpenChannelsSelector,
pendingClosedChannelsSelector,
pendingForceClosedChannelsSelector,
channelSearchQuerySelector,
(activeChannels, nonActiveChannels, pendingOpenChannels, pendingClosedChannels, pendingForcedClosedChannels, searchQuery) => {
const filteredActiveChannels = activeChannels.filter(channel => channel.remote_pubkey.includes(searchQuery) || channel.channel_point.includes(searchQuery)) // eslint-disable-line
const filteredNonActiveChannels = nonActiveChannels.filter(channel => channel.remote_pubkey.includes(searchQuery) || channel.channel_point.includes(searchQuery)) // eslint-disable-line
const filteredPendingOpenChannels = pendingOpenChannels.filter(channel => channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)) // eslint-disable-line
const filteredPendingClosedChannels = pendingClosedChannels.filter(channel => channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)) // eslint-disable-line
const filteredPendingForcedClosedChannels = pendingForcedClosedChannels.filter(channel => channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)) // eslint-disable-line
return [...filteredActiveChannels, ...filteredPendingOpenChannels, ...filteredPendingClosedChannels, ...filteredPendingForcedClosedChannels, ...filteredNonActiveChannels] // eslint-disable-line
}
) )
export const currentChannels = createSelector( export const currentChannels = createSelector(
allChannels, allChannels,
channelsSelectors.activeChannels, channelsSelectors.activeChannels,
channelsSelectors.nonActiveChannels,
channelsSelector, channelsSelector,
pendingOpenChannelsSelector, pendingOpenChannelsSelector,
closingPendingChannels, channelsSelectors.closingPendingChannels,
filterSelector, filterSelector,
channelSearchQuerySelector, channelSearchQuerySelector,
(allChannelsArr, activeChannelsArr, openChannels, pendingOpenChannels, pendingClosedChannels, filter, searchQuery) => { (allChannelsArr, activeChannelsArr, nonActiveChannelsArr, openChannels, pendingOpenChannels, pendingClosedChannels, channelFilter, searchQuery) => {
// Helper function to deliver correct channel array based on filter // Helper function to deliver correct channel array based on filter
const filteredArray = (filterKey) => { const filteredArray = (filterKey) => {
switch (filterKey) { switch (filterKey) {
@ -343,6 +452,8 @@ export const currentChannels = createSelector(
return allChannelsArr return allChannelsArr
case 'ACTIVE_CHANNELS': case 'ACTIVE_CHANNELS':
return activeChannelsArr return activeChannelsArr
case 'NON_ACTIVE_CHANNELS':
return nonActiveChannelsArr
case 'OPEN_CHANNELS': case 'OPEN_CHANNELS':
return openChannels return openChannels
case 'OPEN_PENDING_CHANNELS': case 'OPEN_PENDING_CHANNELS':
@ -354,7 +465,7 @@ export const currentChannels = createSelector(
} }
} }
const channelArray = filteredArray(filter.key) const channelArray = filteredArray(channelFilter.key)
return channelArray.filter(channel => (Object.prototype.hasOwnProperty.call(channel, 'channel') ? return channelArray.filter(channel => (Object.prototype.hasOwnProperty.call(channel, 'channel') ?
channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery) channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)
@ -390,14 +501,22 @@ const initialState = {
viewType: 0, viewType: 0,
filterPulldown: false, filterPulldown: false,
filter: { key: 'ALL_CHANNELS', name: 'All Channels' }, filter: { key: 'ALL_CHANNELS', name: 'All Contacts' },
filters: [ filters: [
{ key: 'ALL_CHANNELS', name: 'All Channels' }, { key: 'ALL_CHANNELS', name: 'All Contacts' },
{ key: 'ACTIVE_CHANNELS', name: 'Active Channels' }, { key: 'ACTIVE_CHANNELS', name: 'Online Contacts' },
{ key: 'OPEN_CHANNELS', name: 'Open Channels' }, { key: 'NON_ACTIVE_CHANNELS', name: 'Offline Contacts' },
{ key: 'OPEN_PENDING_CHANNELS', name: 'Open Pending Channels' }, { key: 'OPEN_PENDING_CHANNELS', name: 'Pending Contacts' },
{ key: 'CLOSING_PENDING_CHANNELS', name: 'Closing Pending Channels' } { key: 'CLOSING_PENDING_CHANNELS', name: 'Closing Contacts' }
] ],
loadingChannelPubkeys: [],
closingChannelIds: [],
contactModal: {
isOpen: false,
channel: null
}
} }
export default function channelsReducer(state = initialState, action) { export default function channelsReducer(state = initialState, action) {

99
app/reducers/contactsform.js

@ -0,0 +1,99 @@
import { createSelector } from 'reselect'
import filter from 'lodash/filter'
// Initial State
const initialState = {
isOpen: false,
searchQuery: '',
contactCapacity: 0.1
}
// Constants
// ------------------------------------
export const OPEN_CONTACTS_FORM = 'OPEN_CONTACTS_FORM'
export const CLOSE_CONTACTS_FORM = 'CLOSE_CONTACTS_FORM'
export const UPDATE_CONTACT_FORM_SEARCH_QUERY = 'UPDATE_CONTACT_FORM_SEARCH_QUERY'
export const UPDATE_CONTACT_CAPACITY = 'UPDATE_CONTACT_CAPACITY'
// ------------------------------------
// Actions
// ------------------------------------
export function openContactsForm() {
return {
type: OPEN_CONTACTS_FORM
}
}
export function closeContactsForm() {
return {
type: CLOSE_CONTACTS_FORM
}
}
export function updateContactFormSearchQuery(searchQuery) {
return {
type: UPDATE_CONTACT_FORM_SEARCH_QUERY,
searchQuery
}
}
export function updateContactCapacity(contactCapacity) {
return {
type: UPDATE_CONTACT_CAPACITY,
contactCapacity
}
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[OPEN_CONTACTS_FORM]: state => ({ ...state, isOpen: true }),
[CLOSE_CONTACTS_FORM]: state => ({ ...state, isOpen: false }),
[UPDATE_CONTACT_FORM_SEARCH_QUERY]: (state, { searchQuery }) => ({ ...state, searchQuery }),
[UPDATE_CONTACT_CAPACITY]: (state, { contactCapacity }) => ({ ...state, contactCapacity })
}
// ------------------------------------
// Selector
// ------------------------------------
const contactFormSelectors = {}
const networkNodesSelector = state => state.network.nodes
const searchQuerySelector = state => state.contactsform.searchQuery
contactFormSelectors.filteredNetworkNodes = createSelector(
networkNodesSelector,
searchQuerySelector,
(nodes, searchQuery) => filter(nodes, node => node.alias.includes(searchQuery) || node.pub_key.includes(searchQuery))
)
contactFormSelectors.showManualForm = createSelector(
searchQuerySelector,
contactFormSelectors.filteredNetworkNodes,
(searchQuery, filteredNetworkNodes) => {
if (!searchQuery.length) { return false }
const connectableNodes = filteredNetworkNodes.filter(node => node.addresses.length > 0)
if (!filteredNetworkNodes.length || !connectableNodes.length) { return true }
return false
}
)
export { contactFormSelectors }
// ------------------------------------
// Reducer
// ------------------------------------
export default function contactFormReducer(state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}

3
app/reducers/index.js

@ -10,6 +10,8 @@ import peers from './peers'
import channels from './channels' import channels from './channels'
import channelform from './channelform' import channelform from './channelform'
import contactsform from './contactsform'
import form from './form' import form from './form'
import payform from './payform' import payform from './payform'
import requestform from './requestform' import requestform from './requestform'
@ -32,6 +34,7 @@ const rootReducer = combineReducers({
peers, peers,
channels, channels,
channelform, channelform,
contactsform,
form, form,
payform, payform,

2
app/reducers/invoice.js

@ -125,7 +125,7 @@ export const invoiceFailed = (event, { error }) => (dispatch) => {
dispatch(setError(error)) dispatch(setError(error))
} }
// Listen for invoice updates pushed from backend from subscribeToInvoices // Listen for invoice updates pushed from backend from subscribeToInvoices
export const invoiceUpdate = (event, { invoice }) => (dispatch) => { export const invoiceUpdate = (event, { invoice }) => (dispatch) => {
dispatch({ type: UPDATE_INVOICE, invoice }) dispatch({ type: UPDATE_INVOICE, invoice })

4
app/reducers/lnd.js

@ -52,12 +52,12 @@ export const lndStdout = (event, line) => (dispatch) => {
if (line.includes('Caught up to height')) { if (line.includes('Caught up to height')) {
trimmed = line.slice(line.indexOf('Caught up to height') + 'Caught up to height'.length).trim() trimmed = line.slice(line.indexOf('Caught up to height') + 'Caught up to height'.length).trim()
height = trimmed.split(' ')[0].split(/(\r\n|\n|\r)/gm)[0] height = trimmed.split(' ')[0].split(/(\r\n|\n|\r)/gm)[0] // eslint-disable-line
} }
if (line.includes('Catching up block hashes to height')) { if (line.includes('Catching up block hashes to height')) {
trimmed = line.slice(line.indexOf('Catching up block hashes to height') + 'Catching up block hashes to height'.length).trim() trimmed = line.slice(line.indexOf('Catching up block hashes to height') + 'Catching up block hashes to height'.length).trim()
height = trimmed.match(/[-]{0,1}[\d.]*[\d]+/g)[0] height = trimmed.match(/[-]{0,1}[\d.]*[\d]+/g)[0] // eslint-disable-line
} }
dispatch({ type: RECEIVE_LINE, lndBlockHeight: height }) dispatch({ type: RECEIVE_LINE, lndBlockHeight: height })

4
app/reducers/network.js

@ -158,7 +158,9 @@ export const receiveInvoiceAndQueryRoutes = (event, { routes }) => dispatch =>
// ------------------------------------ // ------------------------------------
const ACTION_HANDLERS = { const ACTION_HANDLERS = {
[GET_DESCRIBE_NETWORK]: state => ({ ...state, networkLoading: true }), [GET_DESCRIBE_NETWORK]: state => ({ ...state, networkLoading: true }),
[RECEIVE_DESCRIBE_NETWORK]: (state, { nodes, edges }) => ({ ...state, networkLoading: false, nodes, edges }), [RECEIVE_DESCRIBE_NETWORK]: (state, { nodes, edges }) => ({
...state, networkLoading: false, nodes, edges
}),
[GET_QUERY_ROUTES]: (state, { pubkey }) => ({ ...state, networkLoading: true, selectedNode: { pubkey, routes: [], currentRoute: {} } }), [GET_QUERY_ROUTES]: (state, { pubkey }) => ({ ...state, networkLoading: true, selectedNode: { pubkey, routes: [], currentRoute: {} } }),
[RECEIVE_QUERY_ROUTES]: (state, { routes }) => ( [RECEIVE_QUERY_ROUTES]: (state, { routes }) => (

8
app/reducers/peers.js

@ -108,13 +108,17 @@ export const disconnectSuccess = (event, { pubkey }) => dispatch => dispatch({ t
const ACTION_HANDLERS = { const ACTION_HANDLERS = {
[DISCONNECT_PEER]: state => ({ ...state, disconnecting: true }), [DISCONNECT_PEER]: state => ({ ...state, disconnecting: true }),
[DISCONNECT_SUCCESS]: (state, { pubkey }) => ( [DISCONNECT_SUCCESS]: (state, { pubkey }) => (
{ ...state, disconnecting: false, peer: null, peers: state.peers.filter(peer => peer.pub_key !== pubkey) } {
...state, disconnecting: false, peer: null, peers: state.peers.filter(peer => peer.pub_key !== pubkey)
}
), ),
[DISCONNECT_FAILURE]: state => ({ ...state, disconnecting: false }), [DISCONNECT_FAILURE]: state => ({ ...state, disconnecting: false }),
[CONNECT_PEER]: state => ({ ...state, connecting: true }), [CONNECT_PEER]: state => ({ ...state, connecting: true }),
[CONNECT_SUCCESS]: (state, { peer }) => ( [CONNECT_SUCCESS]: (state, { peer }) => (
{ ...state, connecting: false, peerForm: { pubkey: '', host: '', isOpen: false }, peers: [...state.peers, peer] } {
...state, connecting: false, peerForm: { pubkey: '', host: '', isOpen: false }, peers: [...state.peers, peer]
}
), ),
[CONNECT_FAILURE]: state => ({ ...state, connecting: false }), [CONNECT_FAILURE]: state => ({ ...state, connecting: false }),

4
app/reducers/ticker.js

@ -68,7 +68,9 @@ const ACTION_HANDLERS = {
[SET_CRYPTO]: (state, { crypto }) => ({ ...state, crypto }), [SET_CRYPTO]: (state, { crypto }) => ({ ...state, crypto }),
[GET_TICKERS]: state => ({ ...state, tickerLoading: true }), [GET_TICKERS]: state => ({ ...state, tickerLoading: true }),
[RECIEVE_TICKERS]: (state, { btcTicker, ltcTicker }) => ( [RECIEVE_TICKERS]: (state, { btcTicker, ltcTicker }) => (
{ ...state, tickerLoading: false, btcTicker, ltcTicker } {
...state, tickerLoading: false, btcTicker, ltcTicker
}
) )
} }

4
app/reducers/transaction.js

@ -45,7 +45,9 @@ export const fetchTransactions = () => (dispatch) => {
// Receive IPC event for payments // Receive IPC event for payments
export const receiveTransactions = (event, { transactions }) => dispatch => dispatch({ type: RECEIVE_TRANSACTIONS, transactions }) export const receiveTransactions = (event, { transactions }) => dispatch => dispatch({ type: RECEIVE_TRANSACTIONS, transactions })
export const sendCoins = ({ value, addr, currency, rate }) => (dispatch) => { export const sendCoins = ({
value, addr, currency, rate
}) => (dispatch) => {
const amount = currency === 'usd' ? btc.btcToSatoshis(usd.usdToBtc(value, rate)) : btc.btcToSatoshis(value) const amount = currency === 'usd' ? btc.btcToSatoshis(usd.usdToBtc(value, rate)) : btc.btcToSatoshis(value)
dispatch(sendTransaction()) dispatch(sendTransaction())
ipcRenderer.send('lnd', { msg: 'sendCoins', data: { amount, addr } }) ipcRenderer.send('lnd', { msg: 'sendCoins', data: { amount, addr } })

6
app/routes.js

@ -3,15 +3,13 @@ import React from 'react'
import { Switch, Route } from 'react-router' import { Switch, Route } from 'react-router'
import App from './routes/app' import App from './routes/app'
import Activity from './routes/activity' import Activity from './routes/activity'
import Peers from './routes/peers' import Contacts from './routes/contacts'
import Channels from './routes/channels'
import Network from './routes/network' import Network from './routes/network'
export default () => ( export default () => (
<App> <App>
<Switch> <Switch>
<Route path='/peers' component={Peers} /> <Route path='/contacts' component={Contacts} />
<Route path='/channels' component={Channels} />
<Route path='/network' component={Network} /> <Route path='/network' component={Network} />
<Route path='/' component={Activity} /> <Route path='/' component={Activity} />
</Switch> </Switch>

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

@ -20,7 +20,9 @@ class Activity extends Component {
} }
componentWillMount() { componentWillMount() {
const { fetchPayments, fetchInvoices, fetchTransactions, fetchBalance } = this.props const {
fetchPayments, fetchInvoices, fetchTransactions, fetchBalance
} = this.props
fetchBalance() fetchBalance()
fetchPayments() fetchPayments()

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

@ -6,7 +6,9 @@ import { FaBolt, FaClockO } from 'react-icons/lib/fa'
import { btc } from 'utils' import { btc } from 'utils'
import styles from '../Activity.scss' import styles from '../Activity.scss'
const Invoice = ({ invoice, ticker, currentTicker, showActivityModal }) => ( const Invoice = ({
invoice, ticker, currentTicker, showActivityModal
}) => (
<div className={styles.container} onClick={() => showActivityModal('INVOICE', { invoice })}> <div className={styles.container} onClick={() => showActivityModal('INVOICE', { invoice })}>
{ {
!invoice.settled ? !invoice.settled ?

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

@ -9,7 +9,9 @@ import Invoice from './Invoice'
import styles from './Modal.scss' import styles from './Modal.scss'
const Modal = ({ modalType, modalProps, hideActivityModal, ticker, currentTicker }) => { const Modal = ({
modalType, modalProps, hideActivityModal, ticker, currentTicker
}) => {
const MODAL_COMPONENTS = { const MODAL_COMPONENTS = {
TRANSACTION: Transaction, TRANSACTION: Transaction,
PAYMENT: Payment, PAYMENT: Payment,

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

@ -6,7 +6,9 @@ import { FaBolt } from 'react-icons/lib/fa'
import { btc } from 'utils' import { btc } from 'utils'
import styles from '../Activity.scss' import styles from '../Activity.scss'
const Payment = ({ payment, ticker, currentTicker, showActivityModal }) => ( const Payment = ({
payment, ticker, currentTicker, showActivityModal
}) => (
<div className={styles.container} onClick={() => showActivityModal('PAYMENT', { payment })}> <div className={styles.container} onClick={() => showActivityModal('PAYMENT', { payment })}>
<div className={styles.date}> <div className={styles.date}>
<Moment format='D'> <Moment format='D'>

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

@ -6,7 +6,9 @@ import { FaChain } from 'react-icons/lib/fa'
import { btc } from 'utils' import { btc } from 'utils'
import styles from '../Activity.scss' import styles from '../Activity.scss'
const Transaction = ({ transaction, ticker, currentTicker, showActivityModal }) => ( const Transaction = ({
transaction, ticker, currentTicker, showActivityModal
}) => (
<div className={styles.container} onClick={() => showActivityModal('TRANSACTION', { transaction })}> <div className={styles.container} onClick={() => showActivityModal('TRANSACTION', { transaction })}>
<div className={styles.date}> <div className={styles.date}>
<Moment format='D'> <Moment format='D'>

104
app/routes/channels/containers/ChannelsContainer.js

@ -1,104 +0,0 @@
import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import {
fetchChannels,
openChannel,
closeChannel,
updateChannelSearchQuery,
setViewType,
currentChannels,
toggleFilterPulldown,
changeFilter,
channelsSelectors
} from 'reducers/channels'
import {
openChannelForm,
changeStep,
setNodeKey,
setLocalAmount,
setPushAmount,
closeChannelForm,
channelFormSelectors
} from 'reducers/channelform'
import { fetchPeers } from 'reducers/peers'
import { tickerSelectors } from 'reducers/ticker'
import { fetchDescribeNetwork, setCurrentChannel } from '../../../reducers/network'
import Channels from '../components/Channels'
const mapDispatchToProps = {
fetchChannels,
openChannel,
closeChannel,
updateChannelSearchQuery,
setViewType,
toggleFilterPulldown,
changeFilter,
openChannelForm,
closeChannelForm,
setNodeKey,
setLocalAmount,
setPushAmount,
changeStep,
fetchPeers,
fetchDescribeNetwork,
setCurrentChannel
}
const mapStateToProps = state => ({
channels: state.channels,
openChannels: state.channels.channels,
channelform: state.channelform,
peers: state.peers,
ticker: state.ticker,
network: state.network,
identity_pubkey: state.info.data.identity_pubkey,
currentChannels: currentChannels(state),
activeChanIds: channelsSelectors.activeChanIds(state),
nonActiveFilters: channelsSelectors.nonActiveFilters(state),
activeChannels: channelsSelectors.activeChannels(state),
currentTicker: tickerSelectors.currentTicker(state),
channelFormHeader: channelFormSelectors.channelFormHeader(state),
channelFormProgress: channelFormSelectors.channelFormProgress(state),
stepTwoIsValid: channelFormSelectors.stepTwoIsValid(state)
})
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const channelFormProps = {
openChannel: dispatchProps.openChannel,
closeChannelForm: dispatchProps.closeChannelForm,
changeStep: dispatchProps.changeStep,
setNodeKey: dispatchProps.setNodeKey,
setLocalAmount: dispatchProps.setLocalAmount,
setPushAmount: dispatchProps.setPushAmount,
channelform: stateProps.channelform,
channelFormHeader: stateProps.channelFormHeader,
channelFormProgress: stateProps.channelFormProgress,
stepTwoIsValid: stateProps.stepTwoIsValid,
peers: stateProps.peers.peers
}
return {
...stateProps,
...dispatchProps,
...ownProps,
channelFormProps
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Channels))

3
app/routes/channels/index.js

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

148
app/routes/channels/components/Channels.js → app/routes/contacts/components/Contacts.js

@ -1,17 +1,23 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { FaAngleDown, FaRepeat } from 'react-icons/lib/fa' import Isvg from 'react-inlinesvg'
import { MdSearch } from 'react-icons/lib/md' 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 OpenPendingChannel from 'components/Channels/OpenPendingChannel' import plus from 'icons/plus.svg'
import ClosedPendingChannel from 'components/Channels/ClosedPendingChannel'
import Channel from 'components/Channels/Channel'
import ChannelForm from 'components/ChannelForm'
import styles from './Channels.scss' import styles from './Contacts.scss'
class Channels extends Component { class Contacts extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
@ -21,36 +27,36 @@ class Channels extends Component {
} }
componentWillMount() { componentWillMount() {
const { fetchChannels, fetchPeers } = this.props const { fetchChannels, fetchPeers, fetchDescribeNetwork } = this.props
fetchChannels() fetchChannels()
fetchPeers() fetchPeers()
fetchDescribeNetwork()
} }
render() { render() {
const { const {
fetchChannels,
closeChannel,
channels: { channels: {
searchQuery, searchQuery,
filterPulldown, filterPulldown,
filter, filter,
viewType loadingChannelPubkeys,
closingChannelIds
}, },
nonActiveFilters,
toggleFilterPulldown,
changeFilter,
currentChannels, currentChannels,
activeChannels,
fetchChannels,
updateChannelSearchQuery, updateChannelSearchQuery,
openChannelForm, toggleFilterPulldown,
changeFilter,
nonActiveFilters,
ticker, openContactsForm,
currentTicker, openContactModal,
channelFormProps contactModalProps,
contactsFormProps
} = this.props } = this.props
const refreshClicked = () => { const refreshClicked = () => {
@ -60,7 +66,7 @@ class Channels extends Component {
// store event in icon so we dont get an error when react clears it // store event in icon so we dont get an error when react clears it
const icon = this.repeat.childNodes const icon = this.repeat.childNodes
// fetch peers // fetch channels
fetchChannels() fetchChannels()
// wait for the svg to appear as child // wait for the svg to appear as child
@ -81,18 +87,20 @@ class Channels extends Component {
} }
return ( return (
<div className={`${styles.container} ${viewType === 1 && styles.graphview}`}> <div className={styles.friendsContainer}>
<ChannelForm {...channelFormProps} /> <ContactModal {...contactModalProps} />
<ContactsForm {...contactsFormProps} />
<header className={styles.header}> <header className={styles.header}>
<div className={styles.titleContainer}> <div className={styles.titleContainer}>
<div className={styles.left}> <div className={styles.left}>
<h1>Channels</h1> <h1>Contacts <span>({activeChannels.length} online)</span></h1>
</div> </div>
</div> </div>
<div className={styles.createChannelContainer}> <div className={styles.newFriendContainer}>
<div className={`buttonPrimary ${styles.newChannelButton}`} onClick={openChannelForm}> <div className={`buttonPrimary ${styles.newFriendButton}`} onClick={openContactsForm}>
Create new channel <Isvg src={plus} />
<span>Add</span>
</div> </div>
</div> </div>
</header> </header>
@ -105,7 +113,7 @@ class Channels extends Component {
value={searchQuery} value={searchQuery}
onChange={event => updateChannelSearchQuery(event.target.value)} onChange={event => updateChannelSearchQuery(event.target.value)}
className={`${styles.text} ${styles.input}`} className={`${styles.text} ${styles.input}`}
placeholder='Search channels by funding transaction or remote public key' placeholder='Search your contacts list...'
type='text' type='text'
id='channelSearch' id='channelSearch'
/> />
@ -138,68 +146,52 @@ class Channels extends Component {
</section> </section>
</div> </div>
<div className={`${styles.channels} ${filterPulldown && styles.fade}`}> <ul className={`${styles.friends} ${filterPulldown && styles.fade}`}>
<ul className={viewType === 1 && styles.cardsContainer}> {
{ loadingChannelPubkeys.map(pubkey => <LoadingContact pubkey={pubkey} isClosing={false} />)
currentChannels.map((channel, index) => { }
if (Object.prototype.hasOwnProperty.call(channel, 'blocks_till_open')) {
return ( {
<OpenPendingChannel currentChannels.length > 0 && currentChannels.map((channel, index) => {
key={index} if (closingChannelIds.includes(channel.chan_id)) {
channel={channel} return <LoadingContact pubkey={channel.remote_pubkey} isClosing />
ticker={ticker} } else if (Object.prototype.hasOwnProperty.call(channel, 'blocks_till_open')) {
currentTicker={currentTicker} return <PendingContact channel={channel} key={index} />
explorerLinkBase={'https://testnet.smartbit.com.au/'} } else if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) {
/> return <ClosingContact channel={channel} key={index} />
) } else if (channel.active) {
} else if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) { return <OnlineContact channel={channel} key={index} openContactModal={openContactModal} />
return ( } else if (!channel.active) {
<ClosedPendingChannel return <OfflineContact channel={channel} key={index} openContactModal={openContactModal} />
key={index} }
channel={channel} return <span />
ticker={ticker} })
currentTicker={currentTicker} }
explorerLinkBase={'https://testnet.smartbit.com.au/'} </ul>
/>
)
}
return (
<Channel
key={index}
ticker={ticker}
channel={channel}
closeChannel={closeChannel}
currentTicker={currentTicker}
/>
)
})
}
</ul>
</div>
</div> </div>
) )
} }
} }
Channels.propTypes = { Contacts.propTypes = {
fetchChannels: PropTypes.func.isRequired, fetchPeers: PropTypes.func.isRequired,
fetchDescribeNetwork: PropTypes.func.isRequired,
channels: PropTypes.object.isRequired, channels: PropTypes.object.isRequired,
currentChannels: PropTypes.array.isRequired, currentChannels: PropTypes.array.isRequired,
nonActiveFilters: PropTypes.array.isRequired, activeChannels: PropTypes.array.isRequired,
fetchChannels: PropTypes.func.isRequired,
updateChannelSearchQuery: PropTypes.func.isRequired, updateChannelSearchQuery: PropTypes.func.isRequired,
setCurrentChannel: PropTypes.func.isRequired,
openChannelForm: PropTypes.func.isRequired,
closeChannel: PropTypes.func.isRequired,
toggleFilterPulldown: PropTypes.func.isRequired, toggleFilterPulldown: PropTypes.func.isRequired,
changeFilter: PropTypes.func.isRequired, changeFilter: PropTypes.func.isRequired,
fetchPeers: PropTypes.func.isRequired, nonActiveFilters: PropTypes.array.isRequired,
ticker: PropTypes.object.isRequired, openContactsForm: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired, openContactModal: PropTypes.func.isRequired,
channelFormProps: PropTypes.object.isRequired contactModalProps: PropTypes.object.isRequired,
contactsFormProps: PropTypes.object.isRequired
} }
export default Channels export default Contacts

144
app/routes/channels/components/Channels.scss → app/routes/contacts/components/Contacts.scss

@ -1,7 +1,57 @@
@import '../../../variables.scss'; @import '../../../variables.scss';
.container.graphview { .header {
background: $black; 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 { .search {
@ -36,41 +86,12 @@
} }
} }
.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;
}
}
}
.createChannelContainer {
padding: 20px 40px;
.createChannelButton {
font-size: 14px;
margin-left: 10px;
}
}
}
.filtersContainer { .filtersContainer {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
margin-top: 20px;
padding: 20px 40px; padding: 20px 40px;
h2, h2 span { h2, h2 span {
@ -120,59 +141,32 @@
cursor: pointer; cursor: pointer;
.refresh { .refresh {
text-decoration: underline; cursor: pointer;
color: $darkestgrey;
transition: all 0.25s;
svg { &:hover {
font-size: 12px; color: $main;
} }
}
}
}
.layoutsContainer { svg {
padding: 40px; font-size: 12px;
color: $darkestgrey;
span {
font-size: 30px;
color: $grey;
cursor: pointer;
transition: all 0.25s;
&:nth-child(1) {
margin-right: 20px;
}
&:hover {
color: $darkestgrey;
}
&.active { &:hover {
color: $darkestgrey; color: $darkestgrey;
}
}
} }
} }
} }
.createChannelContainer { .friends {
padding: 40px; padding: 10px 0 60px 0;
opacity: 1;
.newChannelButton { transition: all 0.25s;
font-size: 14px;
}
}
.channels {
padding: 10px 40px 40px 40px;
transition: opacity 0.25s;
&.fade { &.fade {
opacity: 0.05; opacity: 0.05;
} }
.cardsContainer {
display: flex;
justify-content: center;
flex-wrap: wrap;
box-sizing: border-box;
}
} }

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

@ -0,0 +1,109 @@
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

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

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

@ -18,7 +18,9 @@ class Network extends Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { payReqIsLn, network: { pay_req }, fetchInvoiceAndQueryRoutes, clearQueryRoutes } = this.props const {
payReqIsLn, network: { pay_req }, fetchInvoiceAndQueryRoutes, clearQueryRoutes
} = this.props
// If LN go retrieve invoice details // If LN go retrieve invoice details
if ((prevProps.network.pay_req !== pay_req) && payReqIsLn) { if ((prevProps.network.pay_req !== pay_req) && payReqIsLn) {
@ -31,7 +33,9 @@ class Network extends Component {
} }
componentWillUnmount() { componentWillUnmount() {
const { clearQueryRoutes, resetPayReq, clearSelectedChannels, clearSelectedPeers } = this.props const {
clearQueryRoutes, resetPayReq, clearSelectedChannels, clearSelectedPeers
} = this.props
clearQueryRoutes() clearQueryRoutes()
resetPayReq() resetPayReq()

137
app/routes/peers/components/Peers.js

@ -1,137 +0,0 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { FaRepeat } from 'react-icons/lib/fa'
import { MdSearch } from 'react-icons/lib/md'
import PeerForm from 'components/Peers/PeerForm'
import PeerModal from 'components/Peers/PeerModal'
import Peer from 'components/Peers/Peer'
import styles from './Peers.scss'
class Peers extends Component {
constructor(props) {
super(props)
this.state = {
refreshing: false
}
}
componentWillMount() {
this.props.fetchPeers()
}
render() {
const {
fetchPeers,
peerFormProps,
setPeerForm,
setPeer,
updateSearchQuery,
disconnectRequest,
peerModalOpen,
filteredPeers,
peers: { peer, searchQuery }
} = 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 peers
fetchPeers()
// 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>
<PeerForm {...peerFormProps} />
<PeerModal isOpen={peerModalOpen} resetPeer={setPeer} peer={peer} disconnect={disconnectRequest} />
<header className={styles.header}>
<div className={styles.titleContainer}>
<div className={styles.left}>
<h1>Peers</h1>
</div>
</div>
<div className={styles.addPeerContainer}>
<div className={`buttonPrimary ${styles.newPeerButton}`} onClick={() => setPeerForm({ isOpen: true })}>
Add new peer
</div>
</div>
</header>
<div className={styles.search}>
<label className={`${styles.label} ${styles.input}`} htmlFor='channelSearch'>
<MdSearch />
</label>
<input
value={searchQuery}
onChange={event => updateSearchQuery(event.target.value)}
className={`${styles.text} ${styles.input}`}
placeholder='Search peers by their node public key or IP address'
type='text'
id='peersSearch'
/>
</div>
<div className={styles.refreshContainer}>
<span className={styles.refresh} onClick={refreshClicked} ref={(ref) => { this.repeat = ref }}>
{
this.state.refreshing ?
<FaRepeat />
:
'Refresh'
}
</span>
</div>
<div className={styles.peers}>
{
filteredPeers.map(filteredPeer => <Peer key={filteredPeer.peer_id} peer={filteredPeer} setPeer={setPeer} />)
}
</div>
</div>
)
}
}
Peers.propTypes = {
fetchPeers: PropTypes.func.isRequired,
peerFormProps: PropTypes.object.isRequired,
setPeerForm: PropTypes.func.isRequired,
setPeer: PropTypes.func.isRequired,
updateSearchQuery: PropTypes.func.isRequired,
disconnectRequest: PropTypes.func.isRequired,
peerModalOpen: PropTypes.bool.isRequired,
filteredPeers: PropTypes.array.isRequired,
peers: PropTypes.shape({
peer: PropTypes.object,
searchQuery: PropTypes.string
}).isRequired
}
export default Peers

81
app/routes/peers/components/Peers.scss

@ -1,81 +0,0 @@
@import '../../../variables.scss';
.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;
}
}
.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;
}
}
}
.addPeerContainer {
padding: 20px 40px;
.newPeerButton {
font-size: 14px;
margin-left: 10px;
}
}
}
.refreshContainer {
padding: 20px 40px 0 40px;
text-align: right;
cursor: pointer;
.refresh {
text-decoration: underline;
svg {
font-size: 12px;
}
}
}
.peers {
padding: 40px;
}

52
app/routes/peers/containers/PeersContainer.js

@ -1,52 +0,0 @@
import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import {
fetchPeers,
setPeer,
setPeerForm,
connectRequest,
disconnectRequest,
updateSearchQuery,
peersSelectors
} from 'reducers/peers'
import Peers from '../components/Peers'
const mapDispatchToProps = {
fetchPeers,
setPeer,
peersSelectors,
setPeerForm,
connectRequest,
disconnectRequest,
updateSearchQuery
}
const mapStateToProps = state => ({
peers: state.peers,
info: state.info,
peerModalOpen: peersSelectors.peerModalOpen(state),
filteredPeers: peersSelectors.filteredPeers(state)
})
const mergeProps = (stateProps, dispatchProps, ownProps) => {
const peerFormProps = {
setForm: dispatchProps.setPeerForm,
connect: dispatchProps.connectRequest,
form: stateProps.peers.peerForm
}
return {
...stateProps,
...dispatchProps,
...ownProps,
peerFormProps
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Peers))

3
app/routes/peers/index.js

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

3
app/store/configureStore.dev.js

@ -50,8 +50,7 @@ const configureStore = (initialState?: counterStateType) => {
if (module.hot) { if (module.hot) {
module.hot.accept('../reducers', () => module.hot.accept('../reducers', () =>
store.replaceReducer(require('../reducers')) // eslint-disable-line global-require store.replaceReducer(require('../reducers'))) // eslint-disable-line global-require
)
} }
return store return store

3
app/variables.scss

@ -12,6 +12,7 @@ $bluegrey: #555459;
$green: #0bb634; $green: #0bb634;
$terminalgreen: #00FF00; $terminalgreen: #00FF00;
$red: #ff0b00; $red: #FF556A;
$blue: #007bb6; $blue: #007bb6;
$orange: #FF8A65;
$curve: cubic-bezier(0.650, 0.000, 0.450, 1.000); $curve: cubic-bezier(0.650, 0.000, 0.450, 1.000);

8
internals/scripts/CheckBuiltsExist.js

@ -9,15 +9,11 @@ function CheckBuildsExist() {
const rendererPath = path.join(__dirname, '..', '..', 'app', 'dist', 'renderer.prod.js') const rendererPath = path.join(__dirname, '..', '..', 'app', 'dist', 'renderer.prod.js')
if (!fs.existsSync(mainPath)) { if (!fs.existsSync(mainPath)) {
throw new Error(chalk.whiteBright.bgRed.bold( throw new Error(chalk.whiteBright.bgRed.bold('The main process is not built yet. Build it by running "npm run build-main"'))
'The main process is not built yet. Build it by running "npm run build-main"'
))
} }
if (!fs.existsSync(rendererPath)) { if (!fs.existsSync(rendererPath)) {
throw new Error(chalk.whiteBright.bgRed.bold( throw new Error(chalk.whiteBright.bgRed.bold('The renderer process is not built yet. Build it by running "npm run build-renderer"'))
'The renderer process is not built yet. Build it by running "npm run build-renderer"'
))
} }
} }

125
test/components/Channels.spec.js

@ -1,125 +0,0 @@
import React from 'react'
import { shallow } from 'enzyme'
import { TiPlus } from 'react-icons/lib/ti'
import Channels from '../../app/components/Channels'
import ChannelModal from '../../app/components/Channels/ChannelModal'
import ChannelForm from '../../app/components/Channels/ChannelForm'
import Channel from '../../app/components/Channels/Channel'
import OpenPendingChannel from '../../app/components/Channels/OpenPendingChannel'
import ClosedPendingChannel from '../../app/components/Channels/ClosedPendingChannel'
const defaultProps = {
ticker: {},
peers: [],
channelsLoading: false,
modalChannel: {},
setChannel: () => {},
channelModalOpen: false,
channelForm: {},
setChannelForm: () => {},
allChannels: [],
openChannel: () => {},
closeChannel: () => {},
fetchChannels: () => {},
currentTicker: {},
explorerLinkBase: 'https://testnet.smartbit.com.au'
}
const channel_open = {
active: true,
capacity: '10000000',
chan_id: '1322138543153545216',
channel_point: '7efb80bf568cf55eb43ba439fdafea99b43f53493ec9ae7c0eae88de2d2b4577:0',
commit_fee: '8688',
commit_weight: '600',
fee_per_kw: '12000',
local_balance: '9991312',
num_updates: '0',
pending_htlcs: [],
remote_balance: '0',
remote_pubkey: '020178567c0f881b579a7ddbcd8ce362a33ebba2b3c2d218e667f7e3b390e40d4e',
total_satoshis_received: '0',
total_satoshis_sent: '0',
unsettled_balance: '0'
}
const channel_pending = {
capacity: '10000000',
channel_point: '7efb80bf568cf55eb43ba439fdafea99b43f53493ec9ae7c0eae88de2d2b4577:0',
local_balance: '9991312',
remote_balance: '0',
remote_node_pub: '020178567c0f881b579a7ddbcd8ce362a33ebba2b3c2d218e667f7e3b390e40d4e'
}
const pending_open_channels = {
blocks_till_open: 0,
channel: channel_pending,
commit_fee: '8688',
commit_weight: '600',
confirmation_height: 0,
fee_per_kw: '12000'
}
const pending_closing_channels = {
channel: channel_pending,
closing_txid: '8d623d1ddd32945cace3351d511df2b5be3e0f7c7e5622989d2fc0215e8a2a7e'
}
describe('Channels', () => {
describe('should show default components', () => {
const props = { ...defaultProps, channelsLoading: true }
const el = shallow(<Channels {...props} />)
it('should contain Modal and Form', () => {
expect(el.find(ChannelModal)).toHaveLength(1)
expect(el.find(ChannelForm)).toHaveLength(1)
})
it('should have Channels header, and plus button', () => {
expect(el.contains('Channels')).toBe(true)
expect(el.find(TiPlus)).toHaveLength(1)
})
})
describe('channels are loading', () => {
const props = { ...defaultProps, channelsLoading: true }
const el = shallow(<Channels {...props} />)
it('should display loading msg', () => {
expect(el.contains('Loading...')).toBe(true)
})
})
describe('channels are loaded', () => {
describe('no channels', () => {
const props = { ...defaultProps, allChannels: [] }
const el = shallow(<Channels {...props} />)
it('should not show channels or loading', () => {
expect(el.contains('Loading...')).toBe(false)
expect(el.find(Channel)).toHaveLength(0)
})
})
describe('channel is open-pending', () => {
const props = { ...defaultProps, allChannels: [pending_open_channels] }
const el = shallow(<Channels {...props} />)
it('should display open-pending', () => {
expect(el.find(OpenPendingChannel)).toHaveLength(1)
})
})
describe('channel is open', () => {
const props = { ...defaultProps, allChannels: [channel_open] }
const el = shallow(<Channels {...props} />)
it('should display open channel', () => {
expect(el.find(Channel)).toHaveLength(1)
})
})
describe('channel is closed-pending', () => {
const props = { ...defaultProps, allChannels: [pending_closing_channels] }
const el = shallow(<Channels {...props} />)
it('should display closed-pending', () => {
expect(el.find(ClosedPendingChannel)).toHaveLength(1)
})
})
})
})

4
test/components/Nav.spec.js

@ -21,8 +21,8 @@ describe('default elements', () => {
it('should render nav links', () => { it('should render nav links', () => {
expect(el.find(NavLink).at(0).props().to).toBe('/') expect(el.find(NavLink).at(0).props().to).toBe('/')
expect(el.find(NavLink).at(1).props().to).toBe('/peers') expect(el.find(NavLink).at(1).props().to).toBe('/contacts')
expect(el.find(NavLink).at(2).props().to).toBe('/channels') expect(el.find(NavLink).at(2).props().to).toBe('/network')
}) })
it('should render buttons', () => { it('should render buttons', () => {
expect(el.find('.button').at(0).text()).toContain('Pay') expect(el.find('.button').at(0).text()).toContain('Pay')

72
test/components/Peers.spec.js

@ -1,72 +0,0 @@
import React from 'react'
import { shallow } from 'enzyme'
import Peers from '../../app/routes/peers/components/Peers'
import PeerModal from '../../app/components/Peers/PeerModal'
import PeerForm from '../../app/components/Peers/PeerForm'
import Peer from '../../app/components/Peers/Peer'
const defaultProps = {
fetchPeers: () => {},
peerFormProps: {
form: {},
setForm: () => {},
connect: () => {}
},
setPeerForm: () => {},
setPeer: () => {},
updateSearchQuery: () => {},
disconnectRequest: () => {},
peerModalOpen: false,
filteredPeers: [],
peers: {
peer: null,
searchQuery: ''
}
}
const peer = {
address: '45.77.115.33:9735',
bytes_recv: '63322',
bytes_sent: '68714',
inbound: true,
peer_id: 3,
ping_time: '261996',
pub_key: '0293cb97aac77eacjc5377d761640f1b51ebba350902801e1aa62853fa7bc3a1f30',
sat_recv: '0',
sat_sent: '0'
}
describe('component.Peers', () => {
describe('default components', () => {
const props = { ...defaultProps }
const el = shallow(<Peers {...props} />)
it('should contain Modal and Form', () => {
expect(el.find(PeerModal)).toHaveLength(1)
expect(el.find(PeerForm)).toHaveLength(1)
})
it('should have Peers header, and plus button', () => {
expect(el.contains('Peers')).toBe(true)
expect(el.contains('Add new peer')).toBe(true)
})
})
describe('peers are loaded', () => {
describe('no peers', () => {
const props = { ...defaultProps }
const el = shallow(<Peers {...props} />)
it('should show no peers', () => {
expect(el.find(Peer)).toHaveLength(0)
})
})
describe('peer connected', () => {
const props = { ...defaultProps, filteredPeers: [peer] }
const el = shallow(<Peers {...props} />)
it('should show peer information', () => {
expect(el.find(Peer)).toHaveLength(1)
})
})
})
})

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

@ -12,33 +12,39 @@ Object {
"channels": Array [], "channels": Array [],
"channelsLoading": true, "channelsLoading": true,
"closingChannel": false, "closingChannel": false,
"closingChannelIds": Array [],
"contactModal": Object {
"channel": null,
"isOpen": false,
},
"filter": Object { "filter": Object {
"key": "ALL_CHANNELS", "key": "ALL_CHANNELS",
"name": "All Channels", "name": "All Contacts",
}, },
"filterPulldown": false, "filterPulldown": false,
"filters": Array [ "filters": Array [
Object { Object {
"key": "ALL_CHANNELS", "key": "ALL_CHANNELS",
"name": "All Channels", "name": "All Contacts",
}, },
Object { Object {
"key": "ACTIVE_CHANNELS", "key": "ACTIVE_CHANNELS",
"name": "Active Channels", "name": "Online Contacts",
}, },
Object { Object {
"key": "OPEN_CHANNELS", "key": "NON_ACTIVE_CHANNELS",
"name": "Open Channels", "name": "Offline Contacts",
}, },
Object { Object {
"key": "OPEN_PENDING_CHANNELS", "key": "OPEN_PENDING_CHANNELS",
"name": "Open Pending Channels", "name": "Pending Contacts",
}, },
Object { Object {
"key": "CLOSING_PENDING_CHANNELS", "key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Pending Channels", "name": "Closing Contacts",
}, },
], ],
"loadingChannelPubkeys": Array [],
"openingChannel": false, "openingChannel": false,
"pendingChannels": Object { "pendingChannels": Object {
"pending_closing_channels": Array [], "pending_closing_channels": Array [],
@ -63,33 +69,39 @@ Object {
"channels": Array [], "channels": Array [],
"channelsLoading": false, "channelsLoading": false,
"closingChannel": false, "closingChannel": false,
"closingChannelIds": Array [],
"contactModal": Object {
"channel": null,
"isOpen": false,
},
"filter": Object { "filter": Object {
"key": "ALL_CHANNELS", "key": "ALL_CHANNELS",
"name": "All Channels", "name": "All Contacts",
}, },
"filterPulldown": false, "filterPulldown": false,
"filters": Array [ "filters": Array [
Object { Object {
"key": "ALL_CHANNELS", "key": "ALL_CHANNELS",
"name": "All Channels", "name": "All Contacts",
}, },
Object { Object {
"key": "ACTIVE_CHANNELS", "key": "ACTIVE_CHANNELS",
"name": "Active Channels", "name": "Online Contacts",
}, },
Object { Object {
"key": "OPEN_CHANNELS", "key": "NON_ACTIVE_CHANNELS",
"name": "Open Channels", "name": "Offline Contacts",
}, },
Object { Object {
"key": "OPEN_PENDING_CHANNELS", "key": "OPEN_PENDING_CHANNELS",
"name": "Open Pending Channels", "name": "Pending Contacts",
}, },
Object { Object {
"key": "CLOSING_PENDING_CHANNELS", "key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Pending Channels", "name": "Closing Contacts",
}, },
], ],
"loadingChannelPubkeys": Array [],
"openingChannel": true, "openingChannel": true,
"pendingChannels": Object { "pendingChannels": Object {
"pending_closing_channels": Array [], "pending_closing_channels": Array [],
@ -117,33 +129,39 @@ Object {
], ],
"channelsLoading": false, "channelsLoading": false,
"closingChannel": false, "closingChannel": false,
"closingChannelIds": Array [],
"contactModal": Object {
"channel": null,
"isOpen": false,
},
"filter": Object { "filter": Object {
"key": "ALL_CHANNELS", "key": "ALL_CHANNELS",
"name": "All Channels", "name": "All Contacts",
}, },
"filterPulldown": false, "filterPulldown": false,
"filters": Array [ "filters": Array [
Object { Object {
"key": "ALL_CHANNELS", "key": "ALL_CHANNELS",
"name": "All Channels", "name": "All Contacts",
}, },
Object { Object {
"key": "ACTIVE_CHANNELS", "key": "ACTIVE_CHANNELS",
"name": "Active Channels", "name": "Online Contacts",
}, },
Object { Object {
"key": "OPEN_CHANNELS", "key": "NON_ACTIVE_CHANNELS",
"name": "Open Channels", "name": "Offline Contacts",
}, },
Object { Object {
"key": "OPEN_PENDING_CHANNELS", "key": "OPEN_PENDING_CHANNELS",
"name": "Open Pending Channels", "name": "Pending Contacts",
}, },
Object { Object {
"key": "CLOSING_PENDING_CHANNELS", "key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Pending Channels", "name": "Closing Contacts",
}, },
], ],
"loadingChannelPubkeys": Array [],
"openingChannel": false, "openingChannel": false,
"pendingChannels": Array [ "pendingChannels": Array [
3, 3,
@ -166,33 +184,39 @@ Object {
"channels": Array [], "channels": Array [],
"channelsLoading": false, "channelsLoading": false,
"closingChannel": false, "closingChannel": false,
"closingChannelIds": Array [],
"contactModal": Object {
"channel": null,
"isOpen": false,
},
"filter": Object { "filter": Object {
"key": "ALL_CHANNELS", "key": "ALL_CHANNELS",
"name": "All Channels", "name": "All Contacts",
}, },
"filterPulldown": false, "filterPulldown": false,
"filters": Array [ "filters": Array [
Object { Object {
"key": "ALL_CHANNELS", "key": "ALL_CHANNELS",
"name": "All Channels", "name": "All Contacts",
}, },
Object { Object {
"key": "ACTIVE_CHANNELS", "key": "ACTIVE_CHANNELS",
"name": "Active Channels", "name": "Online Contacts",
}, },
Object { Object {
"key": "OPEN_CHANNELS", "key": "NON_ACTIVE_CHANNELS",
"name": "Open Channels", "name": "Offline Contacts",
}, },
Object { Object {
"key": "OPEN_PENDING_CHANNELS", "key": "OPEN_PENDING_CHANNELS",
"name": "Open Pending Channels", "name": "Pending Contacts",
}, },
Object { Object {
"key": "CLOSING_PENDING_CHANNELS", "key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Pending Channels", "name": "Closing Contacts",
}, },
], ],
"loadingChannelPubkeys": Array [],
"openingChannel": false, "openingChannel": false,
"pendingChannels": Object { "pendingChannels": Object {
"pending_closing_channels": Array [], "pending_closing_channels": Array [],
@ -217,33 +241,39 @@ Object {
"channels": Array [], "channels": Array [],
"channelsLoading": false, "channelsLoading": false,
"closingChannel": false, "closingChannel": false,
"closingChannelIds": Array [],
"contactModal": Object {
"channel": null,
"isOpen": false,
},
"filter": Object { "filter": Object {
"key": "ALL_CHANNELS", "key": "ALL_CHANNELS",
"name": "All Channels", "name": "All Contacts",
}, },
"filterPulldown": false, "filterPulldown": false,
"filters": Array [ "filters": Array [
Object { Object {
"key": "ALL_CHANNELS", "key": "ALL_CHANNELS",
"name": "All Channels", "name": "All Contacts",
}, },
Object { Object {
"key": "ACTIVE_CHANNELS", "key": "ACTIVE_CHANNELS",
"name": "Active Channels", "name": "Online Contacts",
}, },
Object { Object {
"key": "OPEN_CHANNELS", "key": "NON_ACTIVE_CHANNELS",
"name": "Open Channels", "name": "Offline Contacts",
}, },
Object { Object {
"key": "OPEN_PENDING_CHANNELS", "key": "OPEN_PENDING_CHANNELS",
"name": "Open Pending Channels", "name": "Pending Contacts",
}, },
Object { Object {
"key": "CLOSING_PENDING_CHANNELS", "key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Pending Channels", "name": "Closing Contacts",
}, },
], ],
"loadingChannelPubkeys": Array [],
"openingChannel": false, "openingChannel": false,
"pendingChannels": Object { "pendingChannels": Object {
"pending_closing_channels": Array [], "pending_closing_channels": Array [],
@ -268,33 +298,39 @@ Object {
"channels": Array [], "channels": Array [],
"channelsLoading": false, "channelsLoading": false,
"closingChannel": false, "closingChannel": false,
"closingChannelIds": Array [],
"contactModal": Object {
"channel": null,
"isOpen": false,
},
"filter": Object { "filter": Object {
"key": "ALL_CHANNELS", "key": "ALL_CHANNELS",
"name": "All Channels", "name": "All Contacts",
}, },
"filterPulldown": false, "filterPulldown": false,
"filters": Array [ "filters": Array [
Object { Object {
"key": "ALL_CHANNELS", "key": "ALL_CHANNELS",
"name": "All Channels", "name": "All Contacts",
}, },
Object { Object {
"key": "ACTIVE_CHANNELS", "key": "ACTIVE_CHANNELS",
"name": "Active Channels", "name": "Online Contacts",
}, },
Object { Object {
"key": "OPEN_CHANNELS", "key": "NON_ACTIVE_CHANNELS",
"name": "Open Channels", "name": "Offline Contacts",
}, },
Object { Object {
"key": "OPEN_PENDING_CHANNELS", "key": "OPEN_PENDING_CHANNELS",
"name": "Open Pending Channels", "name": "Pending Contacts",
}, },
Object { Object {
"key": "CLOSING_PENDING_CHANNELS", "key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Pending Channels", "name": "Closing Contacts",
}, },
], ],
"loadingChannelPubkeys": Array [],
"openingChannel": false, "openingChannel": false,
"pendingChannels": Object { "pendingChannels": Object {
"pending_closing_channels": Array [], "pending_closing_channels": Array [],

Loading…
Cancel
Save