Browse Source

Merge pull request #67 from LN-Zap/feature/channels-redesign

Feature/channels redesign
renovate/lint-staged-8.x
JimmyMow 7 years ago
committed by GitHub
parent
commit
d0d814c387
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 92
      app/app.global.scss
  2. 3
      app/components/AnimatedCheckmark/AnimatedCheckmark.js
  3. 90
      app/components/ChannelForm/ChannelForm.js
  4. 58
      app/components/ChannelForm/ChannelForm.scss
  5. 44
      app/components/ChannelForm/Footer.js
  6. 17
      app/components/ChannelForm/Footer.scss
  7. 37
      app/components/ChannelForm/StepFour.js
  8. 36
      app/components/ChannelForm/StepFour.scss
  9. 75
      app/components/ChannelForm/StepOne.js
  10. 74
      app/components/ChannelForm/StepOne.scss
  11. 50
      app/components/ChannelForm/StepThree.js
  12. 58
      app/components/ChannelForm/StepThree.scss
  13. 49
      app/components/ChannelForm/StepTwo.js
  14. 58
      app/components/ChannelForm/StepTwo.scss
  15. 3
      app/components/ChannelForm/index.js
  16. 113
      app/components/Channels/Channel.js
  17. 71
      app/components/Channels/Channel.scss
  18. 2
      app/components/Channels/ClosedPendingChannel.js
  19. 133
      app/components/Channels/NetworkChannels.js
  20. 114
      app/components/Channels/NetworkChannels.scss
  21. 2
      app/components/Channels/OpenPendingChannel.js
  22. 4
      app/components/Channels/OpenPendingChannel.scss
  23. 9
      app/components/CryptoIcon/CryptoIcon.js
  24. 1
      app/components/Form/PayForm.js
  25. 3
      app/components/Form/PayForm.scss
  26. 1
      app/components/Form/RequestForm.scss
  27. 6
      app/components/LoadingBolt/LoadingBolt.js
  28. 19
      app/components/Nav/Nav.js
  29. 7
      app/components/Nav/Nav.scss
  30. 5
      app/components/Wallet/Wallet.js
  31. 6
      app/components/Wallet/Wallet.scss
  32. 0
      app/icons/1024x1024.png
  33. 0
      app/icons/128x128.png
  34. 0
      app/icons/16x16.png
  35. 0
      app/icons/24x24.png
  36. 0
      app/icons/256x256.png
  37. 0
      app/icons/32x32.png
  38. 0
      app/icons/48x48.png
  39. 0
      app/icons/512x512.png
  40. 0
      app/icons/64x64.png
  41. 0
      app/icons/96x96.png
  42. 0
      app/icons/channels.svg
  43. 47
      app/icons/cloudbolt.svg
  44. 0
      app/icons/litecoin.svg
  45. 0
      app/icons/peers.svg
  46. 0
      app/icons/settings.svg
  47. 0
      app/icons/skinny_bitcoin.svg
  48. 0
      app/icons/wallet.svg
  49. 11
      app/lnd/methods/index.js
  50. 130
      app/reducers/channelform.js
  51. 147
      app/reducers/channels.js
  52. 4
      app/reducers/index.js
  53. 7
      app/reducers/ipc.js
  54. 129
      app/reducers/network.js
  55. 2
      app/routes.js
  56. 3
      app/routes/app/components/App.js
  57. 207
      app/routes/channels/components/Channels.js
  58. 148
      app/routes/channels/components/Channels.scss
  59. 103
      app/routes/channels/containers/ChannelsContainer.js
  60. 3
      app/routes/channels/index.js
  61. 48
      app/routes/wallet/components/Wallet.js
  62. 4
      app/routes/wallet/containers/WalletContainer.js
  63. 12
      package.json
  64. BIN
      resources/bin/darwin/lnd
  65. BIN
      resources/bin/linux/lnd
  66. BIN
      resources/bin/win32/lnd.exe
  67. 6
      resources/check.svg
  68. 47
      resources/cloudbolt.svg
  69. 5
      resources/thunderstorm.svg
  70. 51
      resources/zap_1.svg
  71. BIN
      resources/zap_2.icns
  72. BIN
      resources/zap_2.ico
  73. BIN
      resources/zap_2.png
  74. 51
      resources/zap_3.svg
  75. 3
      test/components/AnimatedCheckmark.spec.js
  76. 8
      test/components/CryptoIcon.spec.js
  77. 3
      test/components/LoadingBolt.spec.js
  78. 3
      test/components/Nav.spec.js
  79. 174
      test/reducers/__snapshots__/channels.spec.js.snap
  80. 5
      webpack.config.renderer.dev.js
  81. 36
      yarn.lock

92
app/app.global.scss

@ -72,4 +72,94 @@ body {
100% {
transform: rotate(360deg);
}
}
}
// buttons
.buttonPrimary, .buttonSecondary {
-webkit-user-select: none;
cursor: pointer;
display: block;
padding-left: 30px;
padding-right: 30px;
padding-top: 18px;
padding-bottom: 15px;
border-radius: 2px;
text-align: center;
font-size: 18px;
transition: none;
position: relative;
color: white;
&:active {
transform: translate(0px, 3px);
outline: 0;
}
}
.buttonPrimary {
background-color: $main;
box-shadow: 0 3px 0 0 darken($main, 10%);
&:active {
box-shadow: inset 0 1px 1px 1px darken($main, 10%);
}
}
.buttonPrimary.inactive {
opacity: 0.5;
cursor: auto;
}
.buttonPrimary.inactive:active {
box-shadow: 0 3px 0 0 darken($main, 10%);
transform: none;
}
.buttonSecondary {
background-color: $secondary;
box-shadow: 0 3px 0 0 darken($secondary, 10%);
&:active {
box-shadow: inset 0 1px 1px 1px darken($secondary, 10%);
}
}
.buttonContainer.circleContainer {
display: inline-block;
width: auto;
min-width: 0;
background: none;
border: none;
box-shadow: none;
}
.buttonContainer .circle {
display: inline-block;
border-radius: 50px;
}
.buttonContainer .circle.small {
width: 50px;
padding: 10px 0;
}
.buttonContainer.small {
min-width: auto;
padding: 0;
border: none;
margin: 0 auto;
width: 80%;
}
.buttonContainer .small.active {
box-shadow: inset 0 1px 1px 1px #1f4b2e;
transform: translate(0px, 3px);
outline: 0;
background: #002280;
}
.buttonContainer.small .buttonPrimary {
padding: 10px 5px;
font-size: 15px;
}

3
app/components/AnimatedCheckmark/AnimatedCheckmark.js

@ -1,6 +1,7 @@
import React from 'react'
import Isvg from 'react-inlinesvg'
import checkmarkIcon from 'components/AnimatedCheckmark/checkmark.svg'
const AnimatedCheckmark = () => <Isvg src={'./components/AnimatedCheckmark/checkmark.svg'} />
const AnimatedCheckmark = () => <Isvg src={checkmarkIcon} />
export default AnimatedCheckmark

90
app/components/ChannelForm/ChannelForm.js

@ -0,0 +1,90 @@
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 className={styles.close} />
</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

58
app/components/ChannelForm/ChannelForm.scss

@ -0,0 +1,58 @@
@import '../../variables.scss';
.modal {
position: relative;
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

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

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

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

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

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

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

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

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

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

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

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

113
app/components/Channels/Channel.js

@ -1,56 +1,83 @@
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, setChannel, currentTicker }) => (
<li className={styles.channel} onClick={() => setChannel(channel)}>
<h1 className={styles.status}>Status: Open</h1>
<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>
<h4>
{
ticker.currency === 'btc' ?
btc.satoshisToBtc(channel.local_balance)
:
btc.satoshisToUsd(channel.local_balance, currentTicker.price_usd)
}
</h4>
<span>Local</span>
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>
<h4>
<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.remote_balance)
btc.satoshisToBtc(channel.capacity)
:
btc.satoshisToUsd(channel.remote_balance, currentTicker.price_usd)
btc.satoshisToUsd(channel.capacity, currentTicker.price_usd)
}
</h4>
<span>Remote</span>
</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>
@ -59,7 +86,7 @@ const Channel = ({ ticker, channel, setChannel, currentTicker }) => (
Channel.propTypes = {
ticker: PropTypes.object.isRequired,
channel: PropTypes.object.isRequired,
setChannel: PropTypes.func.isRequired,
closeChannel: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired
}

71
app/components/Channels/Channel.scss

@ -2,12 +2,10 @@
.channel {
position: relative;
background: $white;
background: $lightgrey;
margin: 5px 0;
padding: 10px;
display: flex;
flex-direction: row;
justify-content: space-between;
border-top: 1px solid $grey;
border-top: 1px solid $white;
cursor: pointer;
transition: all 0.25s;
@ -19,21 +17,58 @@
border: none;
}
.status {
color: $main;
position: absolute;
top: 0;
left: 10px;
padding: 10px;
text-transform: uppercase;
font-weight: bold;
font-size: 10px;
.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;
margin-top: 25px;
section {
margin-bottom: 20px;
@ -60,7 +95,6 @@
.left {
flex: 7;
border-right: 1px solid $grey;
}
.right {
@ -68,7 +102,6 @@
.capacity {
text-align: center;
border-bottom: 1px solid $grey;
margin-bottom: 10px;
}
@ -84,10 +117,6 @@
color: $main;
font-size: 16px;
}
&:first-child {
border-right: 1px solid $grey;
}
}
}
}

2
app/components/Channels/ClosedPendingChannel.js

@ -6,7 +6,7 @@ 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}>Status: Closing</h1>
<h1 className={styles.closing}>Closing Channel...</h1>
<div className={styles.left}>
<section className={styles.remotePubkey}>
<span>Remote Pubkey</span>

133
app/components/Channels/NetworkChannels.js

@ -0,0 +1,133 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { ForceGraph, ForceGraphNode, ForceGraphLink } from 'react-vis-force'
import { FaCircle } from 'react-icons/lib/fa'
import styles from './NetworkChannels.scss'
class NetworkChannels extends Component {
constructor(props) {
super(props)
this.state = {
ready: false
}
}
componentWillMount() {
setTimeout(() => {
this.setState({ ready: true })
}, 1000)
this.props.setCurrentChannel(this.props.channels[0])
}
render() {
const { ready } = this.state
const {
channels,
network: { nodes, edges, selectedChannel, networkLoading },
identity_pubkey,
setCurrentChannel
} = this.props
if (!ready || networkLoading) {
return (
<div className={styles.networkLoading}>
<h1>loading network graph</h1>
</div>
)
}
return (
<div className={styles.networkchannels}>
<div className={styles.network}>
<ForceGraph
simulationOptions={
{
width: 1000,
height: 1000,
strength: {
charge: -750
}
}
}
labelAttr='label'
opacityFactor={1}
zoomOptions={{ minScale: 0.1, maxScale: 5 }}
zoom
animate
highlightDependencies
>
{
nodes.map(node => (
<ForceGraphNode
r={15}
label={node.pub_key}
key={node.pub_key}
node={{ id: node.pub_key }}
className={`${styles.node} ${identity_pubkey === node.pub_key && styles.active}`}
fill={identity_pubkey === node.pub_key ? 'green' : 'silver'}
/>
))
}
{
edges.map(edge => (
<ForceGraphLink
className={`${styles.line} ${selectedChannel.chan_id === edge.channel_id && styles.active}`}
key={edge.channel_id}
link={{ source: edge.node1_pub, target: edge.node2_pub }}
/>
))
}
</ForceGraph>
</div>
<div className={styles.channels}>
<ul>
{
channels.map((channel, index) => (
<li
key={index}
className={`${styles.channel} ${channel.chan_id === selectedChannel.chan_id && styles.active}`}
onClick={() => setCurrentChannel(channel)}
>
<header>
{
channel.active ?
<span className={styles.active}>
<FaCircle />
<i>active</i>
</span>
:
<span className={styles.notactive}>
<FaCircle />
<i>not active</i>
</span>
}
</header>
<div className={styles.content}>
<div>
<h4>Remote Pubkey</h4>
<h2>{`${channel.remote_pubkey.substring(30, channel.remote_pubkey.length)}...`}</h2>
</div>
<div>
<h4>Channel Point</h4>
<h2>{`${channel.channel_point.substring(30, channel.channel_point.length)}...`}</h2>
</div>
</div>
</li>
))
}
</ul>
</div>
</div>
)
}
}
NetworkChannels.propTypes = {
channels: PropTypes.array.isRequired,
network: PropTypes.object.isRequired,
identity_pubkey: PropTypes.string.isRequired,
setCurrentChannel: PropTypes.func.isRequired
}
export default NetworkChannels

114
app/components/Channels/NetworkChannels.scss

@ -0,0 +1,114 @@
@import '../../variables.scss';
@keyframes dash {
to {
stroke-dashoffset: 1000;
}
}
.networkLoading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background: $black;
h1 {
font-size: 22px;
color: $green;
font-family: 'courier';
text-align: center;
margin-top: 25%;
}
}
.networkchannels {
position: relative;
}
.network, .channels {
display: inline-block;
vertical-align: top;
}
.network {
width: 70%;
}
.channels {
width: calc(30% - 2px);
border: 1px solid $darkestgrey;
border-radius: 5px;
}
.node {
r: 15;
fill: $darkestgrey;
.active {
r: 25;
fill: $green;
stroke: $green;
}
}
.line.active {
stroke-width: 10;
opacity: 1;
stroke: $green;
stroke-dasharray: 100;
animation: dash 2.5s infinite linear;
}
.channel {
padding: 10px;
background: rgba(0, 0, 0, 0.7);
cursor: pointer;
transition: all 0.25s;
border-bottom: 1px solid $white;
&.active, &:hover {
background: rgba(255, 255, 255, 0.4);
}
header {
margin-bottom: 10px;
.active {
color: $green;
}
.notactive {
color: $green;
}
span svg {
font-size: 10px;
}
i {
text-transform: uppercase;
font-size: 10px;
margin-left: 5px;
}
}
.content {
div {
margin-bottom: 5px;
}
h4 {
text-transform: uppercase;
margin-bottom: 5px;
font-size: 10px;
color: $main;
}
h2 {
font-size: 14px;
color: $white;
}
}
}

2
app/components/Channels/OpenPendingChannel.js

@ -6,7 +6,7 @@ import styles from './OpenPendingChannel.scss'
const OpenPendingChannel = ({ ticker, channel: { channel }, currentTicker, explorerLinkBase }) => (
<li className={styles.channel} onClick={() => shell.openExternal(`${explorerLinkBase}/tx/${channel.channel_point.split(':')[0]}`)}>
<h1 className={styles.pending}>Status: Pending</h1>
<h1 className={styles.pending}>Opening Channel...</h1>
<div className={styles.left}>
<section className={styles.remotePubkey}>
<span>Remote Pubkey</span>

4
app/components/Channels/OpenPendingChannel.scss

@ -2,7 +2,7 @@
.channel {
position: relative;
background: $white;
background: $lightgrey;
padding: 10px;
display: flex;
flex-direction: row;
@ -13,7 +13,7 @@
opacity: 0.5;
.pending {
color: $green;
color: $main;
position: absolute;
top: 0;
left: 10px;

9
app/components/CryptoIcon/CryptoIcon.js

@ -1,15 +1,16 @@
import React from 'react'
import PropTypes from 'prop-types'
import path from 'path'
import { FaBitcoin } from 'react-icons/lib/fa'
import Isvg from 'react-inlinesvg'
import skinnyBitcoinIcon from 'icons/skinny_bitcoin.svg'
import litecoinIcon from 'icons/litecoin.svg'
const CryptoIcon = ({ currency, styles }) => {
switch (currency) {
case 'btc':
return <FaBitcoin style={styles} />
return <Isvg style={styles} src={skinnyBitcoinIcon} />
case 'ltc':
return <Isvg style={styles} src={path.join(__dirname, '..', 'resources/litecoin.svg')} />
return <Isvg style={styles} src={litecoinIcon} />
default:
return <span />
}

1
app/components/Form/PayForm.js

@ -13,6 +13,7 @@ class PayForm extends Component {
// If on-chain, focus on amount to let user know it's editable
if (isOnchain) { this.amountInput.focus() }
// If LN go retrieve invoice details
if ((prevProps.payform.payInput !== payInput) && isLn) {
fetchInvoice(payInput)

3
app/components/Form/PayForm.scss

@ -62,12 +62,13 @@
}
}
input[type=number],, input[type=text] {
input[type=number], input[type=text] {
width: 100px;
font-size: 180px;
border: none;
outline: 0;
-webkit-appearance: none;
font-weight: 200;
}
}

1
app/components/Form/RequestForm.scss

@ -44,6 +44,7 @@
border: none;
outline: 0;
-webkit-appearance: none;
font-weight: 200;
}
}

6
app/components/LoadingBolt/LoadingBolt.js

@ -1,12 +1,14 @@
import React from 'react'
import path from 'path'
import Isvg from 'react-inlinesvg'
import cloudboltIcon from 'icons/cloudbolt.svg'
import styles from './LoadingBolt.scss'
const LoadingBolt = () => (
<div className={styles.container}>
<div className={styles.content}>
<Isvg className={styles.bolt} src={path.join(__dirname, '..', 'resources/cloudbolt.svg')} />
<Isvg className={styles.bolt} src={cloudboltIcon} />
<h1>loading</h1>
</div>
</div>

19
app/components/Nav/Nav.js

@ -1,8 +1,13 @@
import path from 'path'
import React from 'react'
import PropTypes from 'prop-types'
import { NavLink } from 'react-router-dom'
import Isvg from 'react-inlinesvg'
import walletIcon from 'icons/wallet.svg'
import peersIcon from 'icons/peers.svg'
import channelsIcon from 'icons/channels.svg'
import settingsIcon from 'icons/settings.svg'
import styles from './Nav.scss'
const Nav = ({ openPayForm, openRequestForm }) => (
@ -15,37 +20,37 @@ const Nav = ({ openPayForm, openRequestForm }) => (
<NavLink exact to='/' activeClassName={styles.active} className={styles.link}>
<span className={styles.activeBorder} />
<li>
<Isvg styles={{ verticalAlign: 'middle' }} src={path.join(__dirname, '..', 'resources/icons/wallet.svg')} />
<Isvg styles={{ verticalAlign: 'middle' }} src={walletIcon} />
<span>Wallet</span>
</li>
</NavLink>
<NavLink exact to='/wallet' activeClassName={styles.active} className={styles.link}>
<span className={styles.activeBorder} />
<li>
<Isvg styles={{ verticalAlign: 'middle' }} src={path.join(__dirname, '..', 'resources/icons/peers.svg')} />
<Isvg styles={{ verticalAlign: 'middle' }} src={peersIcon} />
<span>Peers</span>
</li>
</NavLink>
<NavLink exact to='/channels' activeClassName={styles.active} className={styles.link}>
<span className={styles.activeBorder} />
<li>
<Isvg styles={{ verticalAlign: 'middle' }} src={path.join(__dirname, '..', 'resources/icons/channels.svg')} />
<Isvg styles={{ verticalAlign: 'middle' }} src={channelsIcon} />
<span>Channels</span>
</li>
</NavLink>
<NavLink exact to='/settings' activeClassName={styles.active} className={styles.link}>
<span className={styles.activeBorder} />
<li>
<Isvg styles={{ verticalAlign: 'middle' }} src={path.join(__dirname, '..', 'resources/icons/settings.svg')} />
<Isvg styles={{ verticalAlign: 'middle' }} src={settingsIcon} />
<span>Settings</span>
</li>
</NavLink>
</ul>
<div className={styles.buttons}>
<div className={styles.button} onClick={openPayForm}>
<div className={`buttonPrimary ${styles.button}`} onClick={openPayForm}>
<span>Pay</span>
</div>
<div className={styles.button} onClick={openRequestForm}>
<div className={`buttonPrimary ${styles.button}`} onClick={openRequestForm}>
<span>Request</span>
</div>
</div>

7
app/components/Nav/Nav.scss

@ -185,18 +185,11 @@
}
.button {
text-align: center;
background: $main;
margin-bottom: 20px;
padding: 20px 10px;
font-weight: bold;
cursor: pointer;
text-transform: uppercase;
letter-spacing: .2px;
&:hover {
background: lighten($main, 5%);
}
}
.content {

5
app/components/Wallet/Wallet.js

@ -1,10 +1,11 @@
import path from 'path'
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { FaQrcode } from 'react-icons/lib/fa'
import Isvg from 'react-inlinesvg'
import { btc } from 'utils'
import skinnyBitcoinIcon from 'icons/skinny_bitcoin.svg'
import ReceiveModal from './ReceiveModal'
import styles from './Wallet.scss'
class Wallet extends Component {
@ -39,7 +40,7 @@ class Wallet extends Component {
<div className={styles.content}>
<div className={styles.left}>
<div className={styles.leftContent}>
<Isvg className={styles.bitcoinLogo} src={path.join(__dirname, '..', 'resources/icons/skinny_bitcoin.svg')} />
<Isvg className={styles.bitcoinLogo} src={skinnyBitcoinIcon} />
<div className={styles.details}>
<h1>{btc.satoshisToBtc(parseFloat(balance.walletBalance) + parseFloat(balance.channelBalance))} BTC</h1>
<span>{btc.satoshisToBtc(balance.walletBalance)} available</span>

6
app/components/Wallet/Wallet.scss

@ -52,9 +52,9 @@
.rightContent {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-direction: row;
justify-content: flex-end;
align-items: right;
height: calc(100% - 50px);
div {

0
resources/icons/1024x1024.png → app/icons/1024x1024.png

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 156 KiB

0
resources/icons/128x128.png → app/icons/128x128.png

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

0
resources/icons/16x16.png → app/icons/16x16.png

Before

Width:  |  Height:  |  Size: 954 B

After

Width:  |  Height:  |  Size: 954 B

0
resources/icons/24x24.png → app/icons/24x24.png

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

0
resources/icons/256x256.png → app/icons/256x256.png

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

0
resources/icons/32x32.png → app/icons/32x32.png

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

0
resources/icons/48x48.png → app/icons/48x48.png

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

0
resources/icons/512x512.png → app/icons/512x512.png

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

0
resources/icons/64x64.png → app/icons/64x64.png

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

0
resources/icons/96x96.png → app/icons/96x96.png

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

0
resources/icons/channels.svg → app/icons/channels.svg

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

47
app/icons/cloudbolt.svg

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="79.536px" height="79.536px" viewBox="0 0 79.536 79.536" style="enable-background:new 0 0 79.536 79.536;"
xml:space="preserve">
<g>
<g>
<path class="load-draw-svg" style="fill:#010002;" d="M78.967,29.396c0,8.124-6.582,14.7-14.706,14.7c-0.58,0-6.131,0-13.489,0
c-4.464,0-9.574,0-14.685,0c-12.896,0-25.626,0-26.942,0c-4.735,0-8.575-1.799-8.575-6.54c0-4.735,3.84-8.575,8.575-8.575
c0.857,0,1.675,0.171,2.47,0.409c0-0.067-0.021-0.132-0.021-0.202c0-5.525,4.479-9.999,10.004-9.999
c0.228,0,0.456,0.052,0.687,0.067c-0.013-0.233-0.075-0.451-0.075-0.684C22.209,8.313,30.522,0,40.788,0
c9.264,0,16.896,6.814,18.289,15.689c1.61-0.616,3.351-0.991,5.184-0.991C72.385,14.698,78.967,21.279,78.967,29.396z
M49.177,47.618H34.504c-4.306,4.329-11.283,11.34-11.363,11.34h11.757L23.146,79.536l26.45-23.52h-8.818L49.177,47.618z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

0
resources/litecoin.svg → app/icons/litecoin.svg

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 263 B

0
resources/icons/peers.svg → app/icons/peers.svg

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

0
resources/icons/settings.svg → app/icons/settings.svg

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

0
resources/icons/skinny_bitcoin.svg → app/icons/skinny_bitcoin.svg

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

0
resources/icons/wallet.svg → app/icons/wallet.svg

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

11
app/lnd/methods/index.js

@ -33,6 +33,17 @@ export default function (lnd, event, msg, data) {
})
.catch(error => console.log('info error: ', error))
break
case 'describeNetwork':
networkController.describeGraph(lnd)
.then(networkData => event.sender.send('receiveDescribeNetwork', networkData))
.catch(error => console.log('describeGraph error: ', error))
break
case 'queryRoutes':
// Data looks like { pubkey: String, amount: Number }
networkController.queryRoutes(lnd, data)
.then(routes => event.sender.send('receiveQueryRoutes', routes))
.catch(error => console.log('queryRoutes error: ', error))
break
case 'newaddress':
// Data looks like { address: '' }
walletController.newAddress(lnd, data.type)

130
app/reducers/channelform.js

@ -0,0 +1,130 @@
import { createSelector } from 'reselect'
// Initial State
const initialState = {
isOpen: false,
node_key: '',
local_amt: 0,
push_amt: 0,
step: 1
}
// Constants
// ------------------------------------
export const OPEN_CHANNEL_FORM = 'OPEN_CHANNEL_FORM'
export const CLOSE_CHANNEL_FORM = 'CLOSE_CHANNEL_FORM'
export const SET_NODE_KEY = 'SET_NODE_KEY'
export const SET_LOCAL_AMOUNT = 'SET_LOCAL_AMOUNT'
export const SET_PUSH_AMOUNT = 'SET_PUSH_AMOUNT'
export const CHANGE_STEP = 'CHANGE_STEP'
export const RESET_CHANNEL_FORM = 'RESET_CHANNEL_FORM'
// ------------------------------------
// Actions
// ------------------------------------
export function openChannelForm() {
return {
type: OPEN_CHANNEL_FORM
}
}
export function closeChannelForm() {
return {
type: CLOSE_CHANNEL_FORM
}
}
export function setNodeKey(node_key) {
return {
type: SET_NODE_KEY,
node_key
}
}
export function setLocalAmount(local_amt) {
return {
type: SET_LOCAL_AMOUNT,
local_amt
}
}
export function setPushAmount(push_amt) {
return {
type: SET_PUSH_AMOUNT,
push_amt
}
}
export function changeStep(step) {
return {
type: CHANGE_STEP,
step
}
}
export function resetChannelForm() {
return {
type: RESET_CHANNEL_FORM
}
}
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[OPEN_CHANNEL_FORM]: state => ({ ...state, isOpen: true }),
[CLOSE_CHANNEL_FORM]: state => ({ ...state, isOpen: false }),
[SET_NODE_KEY]: (state, { node_key }) => ({ ...state, node_key }),
[SET_LOCAL_AMOUNT]: (state, { local_amt }) => ({ ...state, local_amt }),
[SET_PUSH_AMOUNT]: (state, { push_amt }) => ({ ...state, push_amt }),
[CHANGE_STEP]: (state, { step }) => ({ ...state, step }),
[RESET_CHANNEL_FORM]: () => (initialState)
}
const channelFormSelectors = {}
const channelFormStepSelector = state => state.channelform.step
const channelFormLocalAmountSelector = state => state.channelform.local_amt
channelFormSelectors.channelFormHeader = createSelector(
channelFormStepSelector,
(step) => {
switch (step) {
case 1:
return 'Step 1: Select a peer'
case 2:
return 'Step 2: Set your local amount'
case 3:
return 'Step 3: Set your push amount'
default:
return 'Step 4: Create your channel'
}
}
)
channelFormSelectors.channelFormProgress = createSelector(
channelFormStepSelector,
step => ((step - 1) / 3) * 100
)
channelFormSelectors.stepTwoIsValid = createSelector(
channelFormLocalAmountSelector,
local_amt => local_amt > 0
)
export { channelFormSelectors }
// ------------------------------------
// Reducer
// ------------------------------------
export default function channelFormReducer(state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}

147
app/reducers/channels.js

@ -1,5 +1,7 @@
import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron'
import { btc } from 'utils'
import { closeChannelForm } from './channelform'
import { setError } from './error'
// ------------------------------------
// Constants
@ -19,6 +21,13 @@ export const CLOSING_CHANNEL = 'CLOSING_CHANNEL'
export const CLOSING_SUCCESSFUL = 'CLOSING_SUCCESSFUL'
export const CLOSING_FAILURE = 'CLOSING_FAILURE'
export const UPDATE_SEARCH_QUERY = 'UPDATE_SEARCH_QUERY'
export const SET_VIEW_TYPE = 'SET_VIEW_TYPE'
export const TOGGLE_PULLDOWN = 'TOGGLE_PULLDOWN'
export const CHANGE_FILTER = 'CHANGE_FILTER'
// ------------------------------------
// Actions
// ------------------------------------
@ -67,6 +76,20 @@ export function openingFailure() {
}
}
export function updateChannelSearchQuery(searchQuery) {
return {
type: UPDATE_SEARCH_QUERY,
searchQuery
}
}
export function setViewType(viewType) {
return {
type: SET_VIEW_TYPE,
viewType
}
}
// Send IPC event for peers
export const fetchChannels = () => async (dispatch) => {
dispatch(getChannels())
@ -77,7 +100,10 @@ export const fetchChannels = () => async (dispatch) => {
export const receiveChannels = (event, { channels, pendingChannels }) => dispatch => dispatch({ type: RECEIVE_CHANNELS, channels, pendingChannels })
// Send IPC event for opening a channel
export const openChannel = ({ pubkey, localamt, pushamt }) => (dispatch) => {
export const openChannel = ({ pubkey, local_amt, push_amt }) => (dispatch) => {
const localamt = btc.btcToSatoshis(local_amt)
const pushamt = btc.btcToSatoshis(push_amt)
dispatch(openingChannel())
ipcRenderer.send('lnd', { msg: 'openChannel', data: { pubkey, localamt, pushamt } })
}
@ -86,6 +112,7 @@ export const openChannel = ({ pubkey, localamt, pushamt }) => (dispatch) => {
// Receive IPC event for openChannel
export const channelSuccessful = () => (dispatch) => {
dispatch(fetchChannels())
dispatch(closeChannelForm())
}
// Receive IPC event for updated channel
@ -94,8 +121,7 @@ export const pushchannelupdated = () => (dispatch) => {
}
// Receive IPC event for channel end
export const pushchannelend = (event, channelEndData) => (dispatch) => {
console.log('channelEndData: ', channelEndData)
export const pushchannelend = event => (dispatch) => { // eslint-disable-line
dispatch(fetchChannels())
}
@ -106,8 +132,7 @@ export const pushchannelerror = (event, { error }) => (dispatch) => {
}
// Receive IPC event for channel status
export const pushchannelstatus = (event, channelStatusData) => (dispatch) => {
console.log('channel Status data: ', channelStatusData)
export const pushchannelstatus = event => (dispatch) => { // eslint-disable-line
dispatch(fetchChannels())
}
@ -156,6 +181,19 @@ export const pushclosechannelstatus = () => (dispatch) => {
dispatch(fetchChannels())
}
export function toggleFilterPulldown() {
return {
type: TOGGLE_PULLDOWN
}
}
export function changeFilter(filter) {
return {
type: CHANGE_FILTER,
filter
}
}
// ------------------------------------
// Action Handlers
// ------------------------------------
@ -174,7 +212,14 @@ const ACTION_HANDLERS = {
[OPENING_CHANNEL]: state => ({ ...state, openingChannel: true }),
[OPENING_FAILURE]: state => ({ ...state, openingChannel: false }),
[CLOSING_CHANNEL]: state => ({ ...state, closingChannel: true })
[CLOSING_CHANNEL]: state => ({ ...state, closingChannel: true }),
[UPDATE_SEARCH_QUERY]: (state, { searchQuery }) => ({ ...state, searchQuery }),
[SET_VIEW_TYPE]: (state, { viewType }) => ({ ...state, viewType }),
[TOGGLE_PULLDOWN]: state => ({ ...state, filterPulldown: !state.filterPulldown }),
[CHANGE_FILTER]: (state, { filter }) => ({ ...state, filterPulldown: false, filter })
}
const channelsSelectors = {}
@ -183,20 +228,88 @@ const channelsSelector = state => state.channels.channels
const pendingOpenChannelsSelector = state => state.channels.pendingChannels.pending_open_channels
const pendingClosedChannelsSelector = state => state.channels.pendingChannels.pending_closing_channels
const pendingForceClosedChannelsSelector = state => state.channels.pendingChannels.pending_force_closing_channels
const channelSearchQuerySelector = state => state.channels.searchQuery
const filtersSelector = state => state.channels.filters
const filterSelector = state => state.channels.filter
channelsSelectors.channelModalOpen = createSelector(
channelSelector,
channel => (!!channel)
)
channelsSelectors.allChannels = createSelector(
const activeChannels = createSelector(
channelsSelector,
openChannels => openChannels.filter(channel => channel.active)
)
const closingPendingChannels = createSelector(
pendingClosedChannelsSelector,
pendingForceClosedChannelsSelector,
(pendingClosedChannels, pendingForcedClosedChannels) => [...pendingClosedChannels, ...pendingForcedClosedChannels]
)
const allChannels = createSelector(
channelsSelector,
pendingOpenChannelsSelector,
pendingClosedChannelsSelector,
pendingForceClosedChannelsSelector,
(channels, pendingOpenChannels, pendingClosedChannels, pendingForcedClosedChannels) => (
[...channels, ...pendingOpenChannels, ...pendingClosedChannels, ...pendingForcedClosedChannels]
)
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
return [...filteredChannels, ...filteredPendingOpenChannels, ...filteredPendingClosedChannels, ...filteredPendingForcedClosedChannels]
}
)
channelsSelectors.activeChanIds = createSelector(
channelsSelector,
channels => channels.map(channel => channel.chan_id)
)
channelsSelectors.nonActiveFilters = createSelector(
filtersSelector,
filterSelector,
(filters, filter) => filters.filter(f => f.key !== filter.key)
)
export const currentChannels = createSelector(
allChannels,
activeChannels,
channelsSelector,
pendingOpenChannelsSelector,
closingPendingChannels,
filterSelector,
channelSearchQuerySelector,
(allChannelsArr, activeChannelsArr, openChannels, pendingOpenChannels, pendingClosedChannels, filter, searchQuery) => {
// Helper function to deliver correct channel array based on filter
const filteredArray = (filterKey) => {
switch (filterKey) {
case 'ALL_CHANNELS':
return allChannelsArr
case 'ACTIVE_CHANNELS':
return activeChannelsArr
case 'OPEN_CHANNELS':
return openChannels
case 'OPEN_PENDING_CHANNELS':
return pendingOpenChannels
case 'CLOSING_PENDING_CHANNELS':
return pendingClosedChannels
default:
return []
}
}
const channelArray = filteredArray(filter.key)
return channelArray.filter(channel => (Object.prototype.hasOwnProperty.call(channel, 'channel') ?
channel.channel.remote_node_pub.includes(searchQuery) || channel.channel.channel_point.includes(searchQuery)
:
channel.remote_pubkey.includes(searchQuery) || channel.channel_point.includes(searchQuery)))
}
)
export { channelsSelectors }
@ -221,7 +334,19 @@ const initialState = {
push_amt: ''
},
openingChannel: false,
closingChannel: false
closingChannel: false,
searchQuery: '',
viewType: 0,
filterPulldown: false,
filter: { key: 'ALL_CHANNELS', name: 'All Channels' },
filters: [
{ key: 'ALL_CHANNELS', name: 'All Channels' },
{ key: 'ACTIVE_CHANNELS', name: 'Active Channels' },
{ key: 'OPEN_CHANNELS', name: 'Open Channels' },
{ key: 'OPEN_PENDING_CHANNELS', name: 'Open Pending Channels' },
{ key: 'CLOSING_PENDING_CHANNELS', name: 'Closing Pending Channels' }
]
}
export default function channelsReducer(state = initialState, action) {

4
app/reducers/index.js

@ -8,6 +8,7 @@ import balance from './balance'
import payment from './payment'
import peers from './peers'
import channels from './channels'
import channelform from './channelform'
import form from './form'
import payform from './payform'
@ -18,6 +19,7 @@ import modal from './modal'
import address from './address'
import transaction from './transaction'
import activity from './activity'
import network from './network'
import error from './error'
const rootReducer = combineReducers({
@ -29,6 +31,7 @@ const rootReducer = combineReducers({
payment,
peers,
channels,
channelform,
form,
payform,
@ -39,6 +42,7 @@ const rootReducer = combineReducers({
address,
transaction,
activity,
network,
error
})

7
app/reducers/ipc.js

@ -31,6 +31,8 @@ import {
newTransaction
} from './transaction'
import { receiveDescribeNetwork, receiveQueryRoutes } from './network'
// Import all receiving IPC event handlers and pass them into createIpc
const ipc = createIpc({
lndSyncing,
@ -79,7 +81,10 @@ const ipc = createIpc({
receiveTransactions,
transactionSuccessful,
transactionError,
newTransaction
newTransaction,
receiveDescribeNetwork,
receiveQueryRoutes
})
export default ipc

129
app/reducers/network.js

@ -0,0 +1,129 @@
import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron'
// ------------------------------------
// Constants
// ------------------------------------
export const GET_DESCRIBE_NETWORK = 'GET_DESCRIBE_NETWORK'
export const RECEIVE_DESCRIBE_NETWORK = 'RECEIVE_DESCRIBE_NETWORK'
export const GET_QUERY_ROUTES = 'GET_QUERY_ROUTES'
export const RECEIVE_QUERY_ROUTES = 'RECEIVE_QUERY_ROUTES'
export const SET_CURRENT_ROUTE = 'SET_CURRENT_ROUTE'
export const SET_CURRENT_CHANNEL = 'SET_CURRENT_CHANNEL'
// ------------------------------------
// Actions
// ------------------------------------
export function getDescribeNetwork() {
return {
type: GET_DESCRIBE_NETWORK
}
}
export function getQueryRoutes(pubkey) {
return {
type: GET_QUERY_ROUTES,
pubkey
}
}
export function setCurrentRoute(route) {
return {
type: SET_CURRENT_ROUTE,
route
}
}
export function setCurrentChannel(selectedChannel) {
return {
type: SET_CURRENT_CHANNEL,
selectedChannel
}
}
// Send IPC event for describeNetwork
export const fetchDescribeNetwork = () => (dispatch) => {
dispatch(getDescribeNetwork())
ipcRenderer.send('lnd', { msg: 'describeNetwork' })
}
// Receive IPC event for describeNetwork
export const receiveDescribeNetwork = (event, { nodes, edges }) => dispatch => dispatch({ type: RECEIVE_DESCRIBE_NETWORK, nodes, edges })
export const queryRoutes = (pubkey, amount) => (dispatch) => {
dispatch(getQueryRoutes(pubkey))
ipcRenderer.send('lnd', { msg: 'queryRoutes', data: { pubkey, amount } })
}
export const receiveQueryRoutes = (event, { routes }) => dispatch => dispatch({ type: RECEIVE_QUERY_ROUTES, routes })
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[GET_DESCRIBE_NETWORK]: state => ({ ...state, networkLoading: true }),
[RECEIVE_DESCRIBE_NETWORK]: (state, { nodes, edges }) => ({ ...state, networkLoading: false, nodes, edges }),
[GET_QUERY_ROUTES]: (state, { pubkey }) => ({ ...state, networkLoading: true, selectedNode: { pubkey, routes: [], currentRoute: {} } }),
[RECEIVE_QUERY_ROUTES]: (state, { routes }) => (
{
...state,
networkLoading: false,
selectedNode: { pubkey: state.selectedNode.pubkey, routes, currentRoute: routes[0] }
}
),
[SET_CURRENT_ROUTE]: (state, { route }) => (
{
...state,
selectedNode: { pubkey: state.selectedNode.pubkey, routes: state.selectedNode.routes, currentRoute: route }
}
),
[SET_CURRENT_CHANNEL]: (state, { selectedChannel }) => ({ ...state, selectedChannel })
}
// ------------------------------------
// Selectors
// ------------------------------------
const networkSelectors = {}
const currentRouteSelector = state => state.network.selectedNode.currentRoute
networkSelectors.currentRouteHopChanIds = createSelector(
currentRouteSelector,
(currentRoute) => {
if (!currentRoute.hops) { return [] }
return currentRoute.hops.map(hop => hop.chan_id)
}
)
export { networkSelectors }
// ------------------------------------
// Initial State
// ------------------------------------
const initialState = {
networkLoading: false,
nodes: [],
edges: [],
selectedNode: {
pubkey: '',
routes: [],
currentRoute: {}
},
selectedChannel: {}
}
// ------------------------------------
// Reducer
// ------------------------------------
export default function activityReducer(state = initialState, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}

2
app/routes.js

@ -4,11 +4,13 @@ import { Switch, Route } from 'react-router'
import App from './routes/app'
import Activity from './routes/activity'
import Wallet from './routes/wallet'
import Channels from './routes/channels'
export default () => (
<App>
<Switch>
<Route path='/wallet' component={Wallet} />
<Route path='/channels' component={Channels} />
<Route path='/' component={Activity} />
</Switch>
</App>

3
app/routes/app/components/App.js

@ -57,7 +57,8 @@ class App extends Component {
)
}
if (!currentTicker) { return <LoadingBolt /> }
if (!currentTicker || balance.balanceLoading) { return <LoadingBolt /> }
// return <LoadingBolt />
return (
<div>

207
app/routes/channels/components/Channels.js

@ -0,0 +1,207 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { FaAlignJustify, FaGlobe, FaAngleDown, FaRepeat } from 'react-icons/lib/fa'
import { MdSearch } from 'react-icons/lib/md'
import OpenPendingChannel from 'components/Channels/OpenPendingChannel'
import ClosedPendingChannel from 'components/Channels/ClosedPendingChannel'
import Channel from 'components/Channels/Channel'
import NetworkChannels from 'components/Channels/NetworkChannels'
import ChannelForm from 'components/ChannelForm'
import styles from './Channels.scss'
class Channels extends Component {
componentWillMount() {
const { fetchChannels, fetchPeers, fetchDescribeNetwork } = this.props
fetchChannels()
fetchPeers()
fetchDescribeNetwork()
}
render() {
const {
fetchChannels,
closeChannel,
channels: {
searchQuery,
filterPulldown,
filter,
viewType
},
nonActiveFilters,
toggleFilterPulldown,
changeFilter,
currentChannels,
openChannels,
updateChannelSearchQuery,
setViewType,
openChannelForm,
ticker,
currentTicker,
channelFormProps,
network,
identity_pubkey,
setCurrentChannel
} = this.props
const refreshClicked = (event) => {
// store event in icon so we dont get an error when react clears it
const icon = event.currentTarget
// fetch peers
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.container} ${viewType === 1 && styles.graphview}`}>
<ChannelForm {...channelFormProps} />
<div className={styles.search}>
<label className={`${styles.label} ${styles.input}`} htmlFor='channelSearch'>
<MdSearch />
</label>
<input
value={searchQuery}
onChange={event => updateChannelSearchQuery(event.target.value)}
className={`${styles.text} ${styles.input}`}
placeholder='Search channels by funding transaction or remote public key'
type='text'
id='channelSearch'
/>
</div>
<header className={styles.header}>
<div className={styles.layoutsContainer}>
<span className={viewType === 0 && styles.active} onClick={() => setViewType(0)}>
<FaAlignJustify />
</span>
<span className={viewType === 1 && styles.active} onClick={() => setViewType(1)}>
<FaGlobe />
</span>
</div>
<div className={styles.createChannelContainer}>
<div className={`buttonPrimary ${styles.newChannelButton}`} onClick={openChannelForm}>
Create new channel
</div>
</div>
</header>
<div className={styles.filtersContainer}>
<section>
<h2 onClick={toggleFilterPulldown} className={styles.filterTitle}>
{filter.name} <span className={filterPulldown && styles.pulldown}><FaAngleDown /></span>
</h2>
<ul className={`${styles.filters} ${filterPulldown && styles.active}`}>
{
nonActiveFilters.map(f => (
<li key={f.key} onClick={() => changeFilter(f)}>
{f.name}
</li>
))
}
</ul>
</section>
<section className={`${styles.refreshContainer} hint--left`} data-hint='Refresh your peers list'>
<FaRepeat
style={{ verticalAlign: 'baseline' }}
onClick={refreshClicked}
/>
</section>
</div>
<div className={`${styles.channels} ${filterPulldown && styles.fade}`}>
{
viewType === 0 &&
<ul className={viewType === 1 && styles.cardsContainer}>
{
currentChannels.map((channel, index) => {
if (Object.prototype.hasOwnProperty.call(channel, 'blocks_till_open')) {
return (
<OpenPendingChannel
key={index}
channel={channel}
ticker={ticker}
currentTicker={currentTicker}
explorerLinkBase={'https://testnet.smartbit.com.au/'}
/>
)
} else if (Object.prototype.hasOwnProperty.call(channel, 'closing_txid')) {
return (
<ClosedPendingChannel
key={index}
channel={channel}
ticker={ticker}
currentTicker={currentTicker}
explorerLinkBase={'https://testnet.smartbit.com.au/'}
/>
)
}
return (
<Channel
key={index}
ticker={ticker}
channel={channel}
closeChannel={closeChannel}
currentTicker={currentTicker}
/>
)
})
}
</ul>
}
{ viewType === 1 &&
<NetworkChannels
channels={openChannels}
network={network}
identity_pubkey={identity_pubkey}
setCurrentChannel={setCurrentChannel}
/>
}
</div>
</div>
)
}
}
Channels.propTypes = {
fetchChannels: PropTypes.func.isRequired,
fetchPeers: PropTypes.func.isRequired,
channels: PropTypes.object.isRequired,
currentChannels: PropTypes.array.isRequired,
openChannels: PropTypes.array.isRequired,
nonActiveFilters: PropTypes.array.isRequired,
updateChannelSearchQuery: PropTypes.func.isRequired,
setViewType: PropTypes.func.isRequired,
setCurrentChannel: PropTypes.func.isRequired,
openChannelForm: PropTypes.func.isRequired,
closeChannel: PropTypes.func.isRequired,
toggleFilterPulldown: PropTypes.func.isRequired,
changeFilter: PropTypes.func.isRequired,
ticker: PropTypes.object.isRequired,
currentTicker: PropTypes.object.isRequired,
channelFormProps: PropTypes.object.isRequired,
network: PropTypes.object.isRequired,
fetchDescribeNetwork: PropTypes.func.isRequired,
identity_pubkey: PropTypes.string.isRequired
}
export default Channels

148
app/routes/channels/components/Channels.scss

@ -0,0 +1,148 @@
@import '../../../variables.scss';
.container.graphview {
background: $black;
}
.search {
height: 75px;
padding: 2px 25px;
border-bottom: 1px solid $darkgrey;
background: $white;
.input {
display: inline-block;
vertical-align: top;
height: 100%;
}
.label {
width: 5%;
line-height: 70px;
font-size: 25px;
text-align: center;
cursor: pointer;
}
.text {
width: 95%;
outline: 0;
padding: 0;
border: 0;
border-radius: 0;
height: 68px;
font-size: 18px;
}
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.filtersContainer {
position: relative;
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0 40px;
h2, h2 span {
color: $bluegrey;
cursor: pointer;
transition: color 0.25s;
&:hover {
color: lighten($bluegrey, 10%);
}
}
h2, .filters li {
text-transform: uppercase;
letter-spacing: 1.5px;
color: $darkestgrey;
font-size: 14px;
font-weight: 400;
}
h2 span.pulldown {
color: $main;
}
.filters {
display: none;
&.active {
display: block;
position: absolute;
bottom: -100px;
z-index: 10;
li {
margin: 5px 0;
cursor: pointer;
&:hover {
color: $main;
}
}
}
}
.refreshContainer {
color: $bluegrey;
&:hover {
cursor: pointer;
color: lighten($bluegrey, 10%);
}
}
}
.layoutsContainer {
padding: 40px;
span {
font-size: 30px;
color: $grey;
cursor: pointer;
transition: all 0.25s;
&:nth-child(1) {
margin-right: 20px;
}
&:hover {
color: $darkestgrey;
}
&.active {
color: $darkestgrey;
}
}
}
.createChannelContainer {
padding: 40px;
.newChannelButton {
font-size: 14px;
}
}
.channels {
padding: 10px 40px 40px 40px;
transition: opacity 0.25s;
&.fade {
opacity: 0.05;
}
.cardsContainer {
display: flex;
justify-content: center;
flex-wrap: wrap;
box-sizing: border-box;
}
}

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

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

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

48
app/routes/wallet/components/Wallet.js

@ -1,37 +1,24 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Channels from 'components/Channels'
import Peers from 'components/Peers'
import styles from './Wallet.scss'
class Wallet extends Component {
componentWillMount() {
const { fetchPeers, fetchChannels } = this.props
const { fetchPeers } = this.props
fetchPeers()
fetchChannels()
}
render() {
const {
ticker,
peers: { peersLoading, peers, peer, peerForm },
channels: { channelsLoading, channels, channel, channelForm, pendingChannels },
fetchPeers,
fetchChannels,
setPeer,
setChannel,
peerModalOpen,
channelModalOpen,
setPeerForm,
setChannelForm,
connectRequest,
disconnectRequest,
allChannels,
openChannel,
closeChannel,
currentTicker,
explorerLinkBase
disconnectRequest
} = this.props
return (
@ -49,24 +36,6 @@ class Wallet extends Component {
connect={connectRequest}
disconnect={disconnectRequest}
/>
<Channels
fetchChannels={fetchChannels}
ticker={ticker}
peers={peers}
channelsLoading={channelsLoading}
allChannels={allChannels}
channels={channels}
pendingChannels={pendingChannels}
modalChannel={channel}
setChannel={setChannel}
channelModalOpen={channelModalOpen}
channelForm={channelForm}
setChannelForm={setChannelForm}
openChannel={openChannel}
closeChannel={closeChannel}
currentTicker={currentTicker}
explorerLinkBase={explorerLinkBase}
/>
</section>
</div>
)
@ -75,23 +44,12 @@ class Wallet extends Component {
Wallet.propTypes = {
fetchPeers: PropTypes.func.isRequired,
fetchChannels: PropTypes.func.isRequired,
ticker: PropTypes.object.isRequired,
peers: PropTypes.object.isRequired,
channels: PropTypes.object.isRequired,
setPeer: PropTypes.func.isRequired,
setChannel: PropTypes.func.isRequired,
peerModalOpen: PropTypes.bool.isRequired,
channelModalOpen: PropTypes.bool.isRequired,
setPeerForm: PropTypes.func.isRequired,
setChannelForm: PropTypes.func.isRequired,
connectRequest: PropTypes.func.isRequired,
disconnectRequest: PropTypes.func.isRequired,
allChannels: PropTypes.array.isRequired,
openChannel: PropTypes.func.isRequired,
closeChannel: PropTypes.func.isRequired,
currentTicker: PropTypes.object.isRequired,
explorerLinkBase: PropTypes.string.isRequired
disconnectRequest: PropTypes.func.isRequired
}

4
app/routes/wallet/containers/WalletContainer.js

@ -12,7 +12,6 @@ import {
} from 'reducers/peers'
import {
fetchChannels,
fetchPendingChannels,
setChannel,
channelsSelectors,
setChannelForm,
@ -30,7 +29,6 @@ const mapDispatchToProps = {
disconnectRequest,
fetchChannels,
fetchPendingChannels,
setChannel,
openChannel,
closeChannel,
@ -47,8 +45,6 @@ const mapStateToProps = state => ({
peers: state.peers,
channels: state.channels,
allChannels: channelsSelectors.allChannels(state),
peerModalOpen: peersSelectors.peerModalOpen(state),
channelModalOpen: channelsSelectors.channelModalOpen(state),

12
package.json

@ -37,6 +37,8 @@
"files": [
"dist/",
"resources/",
"resources/icons/wallet.svg",
"resources/icons/peers.svg",
"node_modules/",
"app.html",
"main.prod.js",
@ -45,7 +47,6 @@
"rpc.proto"
],
"dmg": {
"icon": "./resources/zap_2.icns",
"contents": [
{
"x": 130,
@ -60,7 +61,6 @@
]
},
"win": {
"icon": "./resources/zap_2.ico",
"target": [
"nsis"
]
@ -77,11 +77,8 @@
},
"extraResources": [
"**/resources/bin/darwin/lnd",
"**/resources/bin/linux/lnd",
"**/resources/bin/win32/lnd",
"**/resources/scripts/*",
"**/resources/zap_2.svg",
"**/resources/cloudbolt.svg"
"**/resources/scripts/darwin_generate_certs.sh",
"**/resources/icons/*"
]
},
"repository": {
@ -221,6 +218,7 @@
"react-router-redux": "^5.0.0-alpha.6",
"react-svg": "^2.1.21",
"react-svg-morph": "^0.1.10",
"react-vis-force": "^0.3.1",
"react-websocket": "^1.1.7",
"redux": "^3.7.1",
"redux-electron-ipc": "^1.1.10",

BIN
resources/bin/darwin/lnd

Binary file not shown.

BIN
resources/bin/linux/lnd

Binary file not shown.

BIN
resources/bin/win32/lnd.exe

Binary file not shown.

6
resources/check.svg

@ -1,6 +0,0 @@
<svg viewBox="0 0 300 300">
<g>
<path d="M 150, 150 m -0, -150 a -150,-150 0 1,0 0,300 a -150,-150 0 1,0 0,-300" fill="none" stroke-width="19" class="circle animatable reverse" stroke-dashoffset="942.6107177734375" stroke-dasharray="942.6107177734375"></path>
<path fill="none" d="M 90,160 l 45,45 l 90,-95" stroke-width="19" class="check animatable" stroke-dashoffset="194.50213623046875" stroke-dasharray="194.50213623046875"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 459 B

47
resources/cloudbolt.svg

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="79.536px" height="79.536px" viewBox="0 0 79.536 79.536" style="enable-background:new 0 0 79.536 79.536;"
xml:space="preserve">
<g>
<g>
<path class="load-draw-svg" style="fill:#010002;" d="M78.967,29.396c0,8.124-6.582,14.7-14.706,14.7c-0.58,0-6.131,0-13.489,0
c-4.464,0-9.574,0-14.685,0c-12.896,0-25.626,0-26.942,0c-4.735,0-8.575-1.799-8.575-6.54c0-4.735,3.84-8.575,8.575-8.575
c0.857,0,1.675,0.171,2.47,0.409c0-0.067-0.021-0.132-0.021-0.202c0-5.525,4.479-9.999,10.004-9.999
c0.228,0,0.456,0.052,0.687,0.067c-0.013-0.233-0.075-0.451-0.075-0.684C22.209,8.313,30.522,0,40.788,0
c9.264,0,16.896,6.814,18.289,15.689c1.61-0.616,3.351-0.991,5.184-0.991C72.385,14.698,78.967,21.279,78.967,29.396z
M49.177,47.618H34.504c-4.306,4.329-11.283,11.34-11.363,11.34h11.757L23.146,79.536l26.45-23.52h-8.818L49.177,47.618z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

5
resources/thunderstorm.svg

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 50 50" version="1.1" width="100" height="100">
<g id="surface1">
<path style="" class="thunderstorm-path" d="M 26 5 C 21.890625 5 18.4375 7.535156 16.90625 11.09375 C 16.605469 11.0625 16.3125 11 16 11 C 11.792969 11 8.320313 13.925781 7.34375 17.84375 C 4.210938 19.253906 2 22.351563 2 26 C 2 30.957031 6.042969 35 11 35 L 16.125 35 L 15.0625 37.625 L 14.53125 39 L 20.5625 39 L 18.0625 45.65625 L 16.9375 48.625 L 19.5625 46.8125 L 32.5625 37.90625 L 33 37.59375 L 33 36 L 28.71875 36 L 29.28125 35 L 39 35 C 43.957031 35 48 30.957031 48 26 C 48 22.417969 45.851563 19.382813 42.8125 17.9375 C 42.292969 13.710938 38.910156 10.433594 34.625 10.125 C 32.90625 7.097656 29.726563 5 26 5 Z M 26 7 C 29.148438 7 31.847656 8.804688 33.15625 11.4375 L 33.4375 12 L 34.0625 12 C 37.792969 12.023438 40.777344 14.941406 40.96875 18.625 L 41 19.28125 L 41.59375 19.5 C 44.164063 20.535156 46 23.046875 46 26 C 46 29.878906 42.878906 33 39 33 L 30.375 33 L 32.875 28.5 L 33.6875 27 L 19.3125 27 L 19.0625 27.625 L 16.90625 33 L 11 33 C 7.121094 33 4 29.878906 4 26 C 4 23.007813 5.871094 20.476563 8.5 19.46875 L 9.03125 19.28125 L 9.125 18.71875 C 9.726563 15.464844 12.5625 13 16 13 C 16.433594 13 16.855469 13.046875 17.28125 13.125 L 18.15625 13.28125 L 18.40625 12.46875 C 19.46875 9.300781 22.460938 7 26 7 Z M 20.6875 29 L 30.28125 29 L 26.125 36.5 L 25.3125 38 L 28.90625 38 L 21.03125 43.40625 L 22.9375 38.34375 L 23.4375 37 L 17.5 37 Z "/>
</g>
</svg>

51
resources/zap_1.svg

@ -1,51 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="128" height="128" viewBox="0 0 100 100">
<defs>
<g id="background">
<path fill="url(#background-gradient)" stroke="none" d=" M 90.75 23.05 Q 89.5 23.05 88.85 24.15 88.15 25.25 88.85 26.35 97.25 40.2 95.15 56.15 93.05 71.65 81.75 82.6 70.35 93.7 54.9 95.25 32.95 97.55 17.6 81.85 2.2 66.2 4.85 44.2 6.6 30.2 16.15 19.7 25.65 9.1 39.25 5.8 50.55 3 61.7 5.95 62.85 6.3 63.7 5.65 64.6 5 64.6 3.85 64.6 2.25 62.95 1.7 52.55 -1.1 41.6 0.65 25.55 3.3 14.4 14.85 3.1 26.25 0.65 42.4 -2.95 66.9 14 84.5 30.85 102.1 55.2 99.7 72.6 97.9 85.2 85.4 97.95 72.8 99.7 55.35 101.45 38.55 92.75 24.05 92.05 23.05 90.75 23.05 Z"/>
</g><mask id="mask">
<path fill="#FFFFFF" stroke="none" d=" M 82.2 82.15 Q 95.5 68.85 95.5 49.95 95.5 31.1 82.2 17.8 68.85 4.5 50 4.5 31.15 4.5 17.8 17.8 4.5 31.1 4.5 49.95 4.5 68.85 17.8 82.15 31.15 95.45 50 95.45 68.85 95.45 82.2 82.15 Z"/>
</mask>
<linearGradient id="background-gradient" x1="0" y1="0" y2="1" x2="0" >
<stop stop-color="#ebb864" offset="0%"/>
<stop stop-color="#1d1d1d" offset="100%"/>
</linearGradient>
<g transform="scale(2.0833333333333335)" id="picture"><path d="M24 0C10.7 0 0 10.7 0 24s10.7 24 24 24 24-10.7 24-24S37.3 0 24 0zm-7.3 42.7h-.2l6.4-15.9h-8.2L31.9 5.3l-6.7 16.6h8.1L16.7 42.7z"/></g>
<linearGradient id="picture-gradient" x1="0" y1="0" y2="1" x2="0" >
<stop stop-color="#ebb864" offset="0%"/>
<stop stop-color="#1d1d1d" offset="100%"/>
</linearGradient>
</defs>
<use xlink:href="#background" fill="url(#background-gradient)" />
<g mask="url(#mask)">
<g transform="
translate(
50 50
)
translate(0 0) scale(0.5)
translate(
-50 -50
)">
<use xlink:href="#picture" fill="url(#picture-gradient)" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

BIN
resources/zap_2.icns

Binary file not shown.

BIN
resources/zap_2.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

BIN
resources/zap_2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

51
resources/zap_3.svg

@ -1,51 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="128" height="128" viewBox="0 0 100 100">
<defs>
<g id="background">
<path fill="url(#background-gradient)" stroke="none" d=" M 90.75 23.05 Q 89.5 23.05 88.85 24.15 88.15 25.25 88.85 26.35 97.25 40.2 95.15 56.15 93.05 71.65 81.75 82.6 70.35 93.7 54.9 95.25 32.95 97.55 17.6 81.85 2.2 66.2 4.85 44.2 6.6 30.2 16.15 19.7 25.65 9.1 39.25 5.8 50.55 3 61.7 5.95 62.85 6.3 63.7 5.65 64.6 5 64.6 3.85 64.6 2.25 62.95 1.7 52.55 -1.1 41.6 0.65 25.55 3.3 14.4 14.85 3.1 26.25 0.65 42.4 -2.95 66.9 14 84.5 30.85 102.1 55.2 99.7 72.6 97.9 85.2 85.4 97.95 72.8 99.7 55.35 101.45 38.55 92.75 24.05 92.05 23.05 90.75 23.05 Z"/>
</g><mask id="mask">
<path fill="#FFFFFF" stroke="none" d=" M 82.2 82.15 Q 95.5 68.85 95.5 49.95 95.5 31.1 82.2 17.8 68.85 4.5 50 4.5 31.15 4.5 17.8 17.8 4.5 31.1 4.5 49.95 4.5 68.85 17.8 82.15 31.15 95.45 50 95.45 68.85 95.45 82.2 82.15 Z"/>
</mask>
<linearGradient id="background-gradient" x1="0" y1="0" y2="1" x2="0" >
<stop stop-color="#ebb864" offset="0%"/>
<stop stop-color="#1d1d1d" offset="100%"/>
</linearGradient>
<g transform="scale(2.0833333333333335)" id="picture"><path d="M24 0C10.7 0 0 10.7 0 24s10.7 24 24 24 24-10.7 24-24S37.3 0 24 0zM4 24C4 13 13 4 24 4c2.5 0 4.8.5 7 1.3L13.5 27.2h9.3l-6.2 15.3C9.2 39.6 4 32.4 4 24zm20 20c-2.3 0-4.5-.4-6.5-1.1l17-21.2h-9.1l6.5-16C39 8.7 44 15.8 44 24c0 11-9 20-20 20z"/></g>
<linearGradient id="picture-gradient" x1="0" y1="0" y2="1" x2="0" >
<stop stop-color="#ebb864" offset="0%"/>
<stop stop-color="#1d1d1d" offset="100%"/>
</linearGradient>
</defs>
<use xlink:href="#background" fill="url(#background-gradient)" />
<g mask="url(#mask)">
<g transform="
translate(
50 50
)
translate(0 0) scale(0.55)
translate(
-50 -50
)">
<use xlink:href="#picture" fill="url(#picture-gradient)" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

3
test/components/AnimatedCheckmark.spec.js

@ -3,12 +3,13 @@ import { shallow } from 'enzyme'
import Isvg from 'react-inlinesvg'
import AnimatedCheckmark from '../../app/components/AnimatedCheckmark'
import checkmarkIcon from '../../app/components/AnimatedCheckmark/checkmark.svg'
describe('component.AnimatedCheckmark', () => {
describe('default', () => {
it('should render default component', () => {
const el = shallow(<AnimatedCheckmark />)
expect(el.find(Isvg).props().src).toContain('checkmark.svg')
expect(el.find(Isvg).props().src).toContain(checkmarkIcon)
})
})
})

8
test/components/CryptoIcon.spec.js

@ -1,9 +1,11 @@
import React from 'react'
import { shallow } from 'enzyme'
import { FaBitcoin } from 'react-icons/lib/fa'
import Isvg from 'react-inlinesvg'
import CryptoIcon from '../../app/components/CryptoIcon'
import skinnyBitcoinIcon from '../../app/icons/skinny_bitcoin.svg'
import litecoinIcon from '../../app/icons/litecoin.svg'
const defaultProps = {
currency: 'bch',
styles: {}
@ -22,7 +24,7 @@ describe('component.CryptoIcon', () => {
const props = { ...defaultProps, currency: 'btc' }
const el = shallow(<CryptoIcon {...props} />)
it('should show btc symbol', () => {
expect(el.find(FaBitcoin)).toHaveLength(1)
expect(el.find(Isvg).props().src).toContain(skinnyBitcoinIcon)
})
})
@ -30,7 +32,7 @@ describe('component.CryptoIcon', () => {
const props = { ...defaultProps, currency: 'ltc' }
const el = shallow(<CryptoIcon {...props} />)
it('should show ltc symbol', () => {
expect(el.find(Isvg).props().src).toContain('litecoin.svg')
expect(el.find(Isvg).props().src).toContain(litecoinIcon)
})
})
})

3
test/components/LoadingBolt.spec.js

@ -2,11 +2,12 @@ import React from 'react'
import { shallow } from 'enzyme'
import Isvg from 'react-inlinesvg'
import LoadingBolt from '../../app/components/LoadingBolt'
import cloudboltIcon from '../../app/icons/cloudbolt.svg'
describe('component.LoadingBolt', () => {
const el = shallow(<LoadingBolt />)
it('should show defaults', () => {
expect(el.find(Isvg).props().src).toContain('cloudbolt.svg')
expect(el.find(Isvg).props().src).toContain(cloudboltIcon)
expect(el.text()).toContain('loading')
})
})

3
test/components/Nav.spec.js

@ -1,9 +1,6 @@
import React from 'react'
import { shallow } from 'enzyme'
import ReactSVG from 'react-svg'
import { NavLink } from 'react-router-dom'
import { MdAccountBalanceWallet } from 'react-icons/lib/md'
import { FaClockO, FaDollar } from 'react-icons/lib/fa'
import Nav from 'components/Nav'
const defaultProps = {

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

@ -12,6 +12,33 @@ Object {
"channels": Array [],
"channelsLoading": true,
"closingChannel": false,
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Channels",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Channels",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Active Channels",
},
Object {
"key": "OPEN_CHANNELS",
"name": "Open Channels",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Open Pending Channels",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Pending Channels",
},
],
"openingChannel": false,
"pendingChannels": Object {
"pending_closing_channels": Array [],
@ -19,6 +46,8 @@ Object {
"pending_open_channels": Array [],
"total_limbo_balance": "",
},
"searchQuery": "",
"viewType": 0,
}
`;
@ -34,6 +63,33 @@ Object {
"channels": Array [],
"channelsLoading": false,
"closingChannel": false,
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Channels",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Channels",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Active Channels",
},
Object {
"key": "OPEN_CHANNELS",
"name": "Open Channels",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Open Pending Channels",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Pending Channels",
},
],
"openingChannel": true,
"pendingChannels": Object {
"pending_closing_channels": Array [],
@ -41,6 +97,8 @@ Object {
"pending_open_channels": Array [],
"total_limbo_balance": "",
},
"searchQuery": "",
"viewType": 0,
}
`;
@ -59,11 +117,40 @@ Object {
],
"channelsLoading": false,
"closingChannel": false,
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Channels",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Channels",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Active Channels",
},
Object {
"key": "OPEN_CHANNELS",
"name": "Open Channels",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Open Pending Channels",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Pending Channels",
},
],
"openingChannel": false,
"pendingChannels": Array [
3,
4,
],
"searchQuery": "",
"viewType": 0,
}
`;
@ -79,6 +166,33 @@ Object {
"channels": Array [],
"channelsLoading": false,
"closingChannel": false,
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Channels",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Channels",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Active Channels",
},
Object {
"key": "OPEN_CHANNELS",
"name": "Open Channels",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Open Pending Channels",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Pending Channels",
},
],
"openingChannel": false,
"pendingChannels": Object {
"pending_closing_channels": Array [],
@ -86,6 +200,8 @@ Object {
"pending_open_channels": Array [],
"total_limbo_balance": "",
},
"searchQuery": "",
"viewType": 0,
}
`;
@ -101,6 +217,33 @@ Object {
"channels": Array [],
"channelsLoading": false,
"closingChannel": false,
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Channels",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Channels",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Active Channels",
},
Object {
"key": "OPEN_CHANNELS",
"name": "Open Channels",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Open Pending Channels",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Pending Channels",
},
],
"openingChannel": false,
"pendingChannels": Object {
"pending_closing_channels": Array [],
@ -108,6 +251,8 @@ Object {
"pending_open_channels": Array [],
"total_limbo_balance": "",
},
"searchQuery": "",
"viewType": 0,
}
`;
@ -123,6 +268,33 @@ Object {
"channels": Array [],
"channelsLoading": false,
"closingChannel": false,
"filter": Object {
"key": "ALL_CHANNELS",
"name": "All Channels",
},
"filterPulldown": false,
"filters": Array [
Object {
"key": "ALL_CHANNELS",
"name": "All Channels",
},
Object {
"key": "ACTIVE_CHANNELS",
"name": "Active Channels",
},
Object {
"key": "OPEN_CHANNELS",
"name": "Open Channels",
},
Object {
"key": "OPEN_PENDING_CHANNELS",
"name": "Open Pending Channels",
},
Object {
"key": "CLOSING_PENDING_CHANNELS",
"name": "Closing Pending Channels",
},
],
"openingChannel": false,
"pendingChannels": Object {
"pending_closing_channels": Array [],
@ -130,5 +302,7 @@ Object {
"pending_open_channels": Array [],
"total_limbo_balance": "",
},
"searchQuery": "",
"viewType": 0,
}
`;

5
webpack.config.renderer.dev.js

@ -25,7 +25,6 @@ const publicPath = `http://localhost:${port}/dist`;
const dll = path.resolve(process.cwd(), 'dll');
const manifest = path.resolve(dll, 'renderer.json');
console.log('one')
/**
* Warn if the DLL is not built
*/
@ -36,8 +35,6 @@ if (!(fs.existsSync(dll) && fs.existsSync(manifest))) {
execSync('npm run build-dll');
}
console.log('two')
export default merge.smart(baseConfig, {
devtool: 'inline-source-map',
@ -282,5 +279,3 @@ export default merge.smart(baseConfig, {
}
}
});
console.log('three')

36
yarn.lock

@ -2532,6 +2532,31 @@ currently-unhandled@^0.4.1:
dependencies:
array-find-index "^1.0.1"
d3-collection@1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
d3-dispatch@1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
d3-force@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.1.0.tgz#cebf3c694f1078fcc3d4daf8e567b2fbd70d4ea3"
dependencies:
d3-collection "1"
d3-dispatch "1"
d3-quadtree "1"
d3-timer "1"
d3-quadtree@1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.3.tgz#ac7987e3e23fe805a990f28e1b50d38fcb822438"
d3-timer@1:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531"
d@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
@ -5578,7 +5603,7 @@ lodash.range@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/lodash.range/-/lodash.range-3.2.0.tgz#f461e588f66683f7eadeade513e38a69a565a15d"
lodash.reduce@^4.4.0:
lodash.reduce@^4.4.0, lodash.reduce@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b"
@ -7231,6 +7256,15 @@ react-transition-group@^1.2.0:
prop-types "^15.5.6"
warning "^3.0.0"
react-vis-force@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/react-vis-force/-/react-vis-force-0.3.1.tgz#c7bc96a4e872409f5d4c0fa93fe89c94554d47b7"
dependencies:
d3-force "^1.0.2"
global "^4.3.0"
lodash.reduce "^4.6.0"
prop-types "^15.5.10"
react-websocket@^1.1.7:
version "1.1.7"
resolved "https://registry.yarnpkg.com/react-websocket/-/react-websocket-1.1.7.tgz#0a761f3de354d4731f55343456e03b1f6005b492"

Loading…
Cancel
Save