Browse Source

Merge pull request #104 from LN-Zap/feature/network

Feature/network
renovate/lint-staged-8.x
JimmyMow 7 years ago
committed by GitHub
parent
commit
bb07f326ec
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .gitignore
  2. 30
      app/app.global.scss
  3. 4
      app/components/GlobalError/GlobalError.scss
  4. 8
      app/components/Nav/Nav.js
  5. 292
      app/components/Network/CanvasNetworkGraph.js
  6. 156
      app/components/Network/CanvasNetworkGraph.scss
  7. 46
      app/components/Network/ChannelsList.js
  8. 78
      app/components/Network/ChannelsList.scss
  9. 25
      app/components/Network/PeersList.js
  10. 46
      app/components/Network/PeersList.scss
  11. 61
      app/components/Network/TransactionForm.js
  12. 125
      app/components/Network/TransactionForm.scss
  13. 4
      app/components/Wallet/ReceiveModal.js
  14. 4
      app/components/Wallet/Wallet.js
  15. 147
      app/lnd/lib/rpc.proto
  16. 23
      app/lnd/methods/index.js
  17. 2
      app/lnd/methods/paymentsController.js
  18. 42
      app/lnd/utils/index.js
  19. 3
      app/reducers/channels.js
  20. 5
      app/reducers/ipc.js
  21. 171
      app/reducers/network.js
  22. 2
      app/routes.js
  23. 7
      app/routes/activity/components/Activity.js
  24. 1
      app/routes/activity/components/components/Modal/Invoice/Invoice.js
  25. 3
      app/routes/activity/components/components/Modal/Modal.js
  26. 1
      app/routes/activity/components/components/Modal/Payment/Payment.js
  27. 9
      app/routes/activity/components/components/Modal/Transaction/Transaction.js
  28. 4
      app/routes/activity/containers/ActivityContainer.js
  29. 8
      app/routes/app/components/App.js
  30. 3
      app/routes/app/containers/AppContainer.js
  31. 4
      app/routes/channels/components/Channels.js
  32. 155
      app/routes/network/components/Network.js
  33. 81
      app/routes/network/components/Network.scss
  34. 62
      app/routes/network/containers/NetworkContainer.js
  35. 3
      app/routes/network/index.js
  36. 4
      app/routes/peers/components/Peers.js
  37. 147
      app/rpc.proto
  38. 3
      package.json
  39. 71
      yarn.lock

2
.gitignore

@ -52,4 +52,4 @@ main.js.map
npm-debug.log.*
# lnd binary
resources/bin/*
resources/bin/

30
app/app.global.scss

@ -164,3 +164,33 @@ body {
padding: 10px 5px;
font-size: 15px;
}
// network
@keyframes dash {
to {
stroke-dashoffset: 1000;
}
}
// each node in the map
.network-node {
}
// each channel in the map
.network-link {
opacity: 0.6;
}
.active-peer {
fill: #5589F3;
}
.active-channel {
opacity: 1;
stroke: #88D4A2;
stroke-width: 15;
stroke-dasharray: 100;
animation: dash 2.5s infinite linear;
}

4
app/components/GlobalError/GlobalError.scss

@ -13,6 +13,10 @@
&.closed {
max-height: 0;
padding: 0;
.content .close {
opacity: 0;
}
}
.content {

8
app/components/Nav/Nav.js

@ -6,6 +6,7 @@ import Isvg from 'react-inlinesvg'
import walletIcon from 'icons/wallet.svg'
import peersIcon from 'icons/peers.svg'
import channelsIcon from 'icons/channels.svg'
import networkIcon from 'icons/globe.svg'
import styles from './Nav.scss'
@ -37,6 +38,13 @@ const Nav = ({ openPayForm, openRequestForm }) => (
<span>Channels</span>
</li>
</NavLink>
<NavLink exact to='/network' activeClassName={styles.active} className={styles.link}>
<span className={styles.activeBorder} />
<li>
<Isvg styles={{ verticalAlign: 'middle' }} src={networkIcon} />
<span>Network</span>
</li>
</NavLink>
</ul>
<div className={styles.buttons}>
<div className={`buttonPrimary ${styles.button}`} onClick={openPayForm}>

292
app/components/Network/CanvasNetworkGraph.js

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

156
app/components/Network/CanvasNetworkGraph.scss

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

46
app/components/Network/ChannelsList.js

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

78
app/components/Network/ChannelsList.scss

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

25
app/components/Network/PeersList.js

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

46
app/components/Network/PeersList.scss

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

61
app/components/Network/TransactionForm.js

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

125
app/components/Network/TransactionForm.scss

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

4
app/components/Wallet/ReceiveModal.js

@ -93,7 +93,9 @@ ReceiveModal.propTypes = {
hideActivityModal: PropTypes.func.isRequired,
pubkey: PropTypes.string.isRequired,
address: PropTypes.string.isRequired,
newAddress: PropTypes.func.isRequired
newAddress: PropTypes.func.isRequired,
changeQrCode: PropTypes.func.isRequired,
qrCodeType: PropTypes.number.isRequired
}
export default ReceiveModal

4
app/components/Wallet/Wallet.js

@ -29,9 +29,9 @@ class Wallet extends Component {
const { modalOpen, qrCodeType } = this.state
const changeQrCode = () => {
const qrCodeType = this.state.qrCodeType === 1 ? 2 : 1
const qrCodeNum = this.state.qrCodeType === 1 ? 2 : 1
this.setState({ qrCodeType })
this.setState({ qrCodeType: qrCodeNum })
}
return (

147
app/lnd/lib/rpc.proto

@ -25,9 +25,46 @@ package lnrpc;
* https://github.com/MaxFangX/lightning-api
*/
// The WalletUnlocker service is used to set up a wallet password for
// lnd at first startup, and unlock a previously set up wallet.
service WalletUnlocker {
/** lncli: `create`
CreateWallet is used at lnd startup to set the encryption password for
the wallet database.
*/
rpc CreateWallet(CreateWalletRequest) returns (CreateWalletResponse) {
option (google.api.http) = {
post: "/v1/createwallet"
body: "*"
};
}
/** lncli: `unlock`
UnlockWallet is used at startup of lnd to provide a password to unlock
the wallet database.
*/
rpc UnlockWallet(UnlockWalletRequest) returns (UnlockWalletResponse) {
option (google.api.http) = {
post: "/v1/unlockwallet"
body: "*"
};
}
}
message CreateWalletRequest {
bytes password = 1;
}
message CreateWalletResponse {}
message UnlockWalletRequest {
bytes password = 1;
}
message UnlockWalletResponse {}
service Lightning {
/** lncli: `walletbalance`
WalletBalance returns the sum of all confirmed unspent outputs under control
WalletBalance returns total unspent outputs(confirmed and unconfirmed), all confirmed unspent outputs and all unconfirmed unspent outputs under control
by the wallet. This method can be modified by having the request specify
only witness outputs should be factored into the final output sum.
*/
@ -59,7 +96,10 @@ service Lightning {
/** lncli: `sendcoins`
SendCoins executes a request to send coins to a particular address. Unlike
SendMany, this RPC call only allows creating a single output at a time.
SendMany, this RPC call only allows creating a single output at a time. If
neither target_conf, or sat_per_byte are set, then the internal wallet will
consult its fee model to determine a fee for the default confirmation
target.
*/
rpc SendCoins (SendCoinsRequest) returns (SendCoinsResponse) {
option (google.api.http) = {
@ -77,7 +117,9 @@ service Lightning {
/** lncli: `sendmany`
SendMany handles a request for a transaction that creates multiple specified
outputs in parallel.
outputs in parallel. If neither target_conf, or sat_per_byte are set, then
the internal wallet will consult its fee model to determine a fee for the
default confirmation target.
*/
rpc SendMany (SendManyRequest) returns (SendManyResponse);
@ -191,7 +233,10 @@ service Lightning {
/** lncli: `openchannel`
OpenChannel attempts to open a singly funded channel specified in the
request to a remote peer.
request to a remote peer. Users are able to specify a target number of
blocks that the funding transaction should be confirmed in, or a manual fee
rate to us for the funding transaction. If neither are specified, then a
lax block confirmation target is used.
*/
rpc OpenChannel (OpenChannelRequest) returns (stream OpenStatusUpdate);
@ -199,7 +244,10 @@ service Lightning {
CloseChannel attempts to close an active channel identified by its channel
outpoint (ChannelPoint). The actions of this method can additionally be
augmented to attempt a force close after a timeout period in the case of an
inactive peer.
inactive peer. If a non-force close (cooperative closure) is requested,
then the user can specify either a target number of blocks until the
closure transaction is confirmed, or a manual fee rate. If neither are
specified, then a default lax, block confirmation target is used.
*/
rpc CloseChannel (CloseChannelRequest) returns (stream CloseStatusUpdate) {
option (google.api.http) = {
@ -431,6 +479,9 @@ message Transaction {
/// Fees paid for this transaction
int64 total_fees = 7 [ json_name = "total_fees" ];
/// Addresses that received funds for this transaction
repeated string dest_addresses = 8 [ json_name = "dest_addresses" ];
}
message GetTransactionsRequest {
}
@ -492,6 +543,12 @@ message LightningAddress {
message SendManyRequest {
/// The map from addresses to amounts
map<string, int64> AddrToAmount = 1;
/// The target number of blocks that this transaction should be confirmed by.
int32 target_conf = 3;
/// A manual fee rate set in sat/byte that should be used when crafting the transaction.
int64 sat_per_byte = 5;
}
message SendManyResponse {
/// The id of the transaction
@ -504,6 +561,12 @@ message SendCoinsRequest {
/// The amount in satoshis to send
int64 amount = 2;
/// The target number of blocks that this transaction should be confirmed by.
int32 target_conf = 3;
/// A manual fee rate set in sat/byte that should be used when crafting the transaction.
int64 sat_per_byte = 5;
}
message SendCoinsResponse {
/// The transaction ID of the transaction
@ -657,6 +720,13 @@ message ActiveChannel {
The list of active, uncleared HTLCs currently pending within the channel.
*/
repeated HTLC pending_htlcs = 15 [json_name = "pending_htlcs"];
/**
The CSV delay expressed in relative blocks. If the channel is force
closed, we'll need to wait for this many blocks before we can regain our
funds.
*/
uint32 csv_delay = 16 [ json_name = "csv_delay" ];
}
message ListChannelsRequest {
@ -764,6 +834,12 @@ message CloseChannelRequest {
/// If true, then the channel will be closed forcibly. This means the current commitment transaction will be signed and broadcast.
bool force = 2;
/// The target number of blocks that the closure transaction should be confirmed by.
int32 target_conf = 3;
/// A manual fee rate set in sat/byte that should be used when crafting the closure transaction.
int64 sat_per_byte = 5;
}
message CloseStatusUpdate {
oneof update {
@ -794,6 +870,12 @@ message OpenChannelRequest {
/// The number of satoshis to push to the remote side as part of the initial commitment state
int64 push_sat = 5 [json_name = "push_sat"];
/// The target number of blocks that the closure transaction should be confirmed by.
int32 target_conf = 6;
/// A manual fee rate set in sat/byte that should be used when crafting the closure transaction.
int64 sat_per_byte = 7;
}
message OpenStatusUpdate {
oneof update {
@ -803,6 +885,31 @@ message OpenStatusUpdate {
}
}
message PendingHTLC {
/// The direction within the channel that the htlc was sent
bool incoming = 1 [ json_name = "incoming" ];
/// The total value of the htlc
int64 amount = 2 [ json_name = "amount" ];
/// The final output to be swept back to the user's wallet
string outpoint = 3 [ json_name = "outpoint" ];
/// The next block height at which we can spend the current stage
uint32 maturity_height = 4 [ json_name = "maturity_height" ];
/**
The number of blocks remaining until the current stage can be swept.
Negative values indicate how many blocks have passed since becoming
mature.
*/
int32 blocks_til_maturity = 5 [ json_name = "blocks_til_maturity" ];
/// Indicates whether the htlc is in its first or second stage of recovery
uint32 stage = 6 [ json_name = "stage" ];
}
message PendingChannelRequest {}
message PendingChannelResponse {
message PendingChannel {
@ -823,7 +930,7 @@ message PendingChannelResponse {
uint32 confirmation_height = 2 [ json_name = "confirmation_height" ];
/// The number of blocks until this channel is open
uint32 blocks_till_open = 3 [ json_name = "blocks_till_open" ];
int32 blocks_till_open = 3 [ json_name = "blocks_till_open" ];
/**
The amount calculated to be paid in fees for the current set of
@ -857,8 +964,6 @@ message PendingChannelResponse {
/// The pending channel to be force closed
PendingChannel channel = 1 [ json_name = "channel" ];
// TODO(roasbeef): HTLC's as well?
/// The transaction id of the closing transaction
string closing_txid = 2 [ json_name = "closing_txid" ];
@ -868,8 +973,17 @@ message PendingChannelResponse {
/// The height at which funds can be sweeped into the wallet
uint32 maturity_height = 4 [ json_name = "maturity_height" ];
/// Remaining # of blocks until funds can be sweeped into the wallet
uint32 blocks_til_maturity = 5 [ json_name = "blocks_til_maturity" ];
/*
Remaining # of blocks until the commitment output can be swept.
Negative values indicate how many blocks have passed since becoming
mature.
*/
int32 blocks_til_maturity = 5 [ json_name = "blocks_til_maturity" ];
/// The total value of funds successfully recovered from this channel
int64 recovered_balance = 6 [ json_name = "recovered_balance" ];
repeated PendingHTLC pending_htlcs = 8 [ json_name = "pending_htlcs" ];
}
/// The balance in satoshis encumbered in pending channels
@ -891,7 +1005,13 @@ message WalletBalanceRequest {
}
message WalletBalanceResponse {
/// The balance of the wallet
int64 balance = 1 [json_name = "balance"];
int64 total_balance = 1 [json_name = "total_balance"];
/// The confirmed balance of a wallet(with >= 1 confirmations)
int64 confirmed_balance = 2 [json_name = "confirmed_balance"];
/// The unconfirmed balance of a wallet(with 0 confirmations)
int64 unconfirmed_balance = 3 [json_name = "unconfirmed_balance"];
}
message ChannelBalanceRequest {
@ -994,6 +1114,7 @@ message LightningNode {
string pub_key = 2 [ json_name = "pub_key" ];
string alias = 3 [ json_name = "alias" ];
repeated NodeAddress addresses = 4 [ json_name = "addresses" ];
string color = 5 [ json_name = "color" ];
}
message NodeAddress {
@ -1179,6 +1300,9 @@ message Invoice {
/// Fallback on-chain address.
string fallback_addr = 12 [json_name = "fallback_addr"];
/// Delta to use for the time-lock of the CLTV extended to the final hop.
uint64 cltv_expiry = 13 [json_name = "cltv_expiry"];
}
message AddInvoiceResponse {
bytes r_hash = 1 [json_name = "r_hash"];
@ -1263,6 +1387,7 @@ message PayReq {
string description = 6 [json_name = "description"];
string description_hash = 7 [json_name = "description_hash"];
string fallback_addr = 8 [json_name = "fallback_addr"];
int64 cltv_expiry = 9 [json_name = "cltv_expiry"];
}
message FeeReportRequest {}

23
app/lnd/methods/index.js

@ -44,8 +44,20 @@ export default function (lnd, event, msg, data) {
.then(routes => event.sender.send('receiveQueryRoutes', routes))
.catch(error => console.log('queryRoutes error: ', error))
break
case 'getInvoiceAndQueryRoutes':
// Data looks like { pubkey: String, amount: Number }
invoicesController.getInvoice(lnd, { pay_req: data.payreq })
.then((invoiceData) => {
networkController.queryRoutes(lnd, { pubkey: invoiceData.destination, amount: invoiceData.num_satoshis })
.then((routes) => {
event.sender.send('receiveInvoiceAndQueryRoutes', routes)
})
.catch(error => console.log('getInvoiceAndQueryRoutes queryRoutes error: ', error))
})
.catch(error => console.log('getInvoiceAndQueryRoutes invoice error: ', error))
break
case 'newaddress':
// Data looks like { address: '' }
// Data looks like { address: '' }
walletController.newAddress(lnd, data.type)
.then(({ address }) => event.sender.send('receiveAddress', address))
.catch(error => console.log('newaddress error: ', error))
@ -98,7 +110,9 @@ export default function (lnd, event, msg, data) {
case 'balance':
// Balance looks like [ { balance: '129477456' }, { balance: '243914' } ]
Promise.all([walletController.walletBalance, channelController.channelBalance].map(func => func(lnd)))
.then(balance => event.sender.send('receiveBalance', { walletBalance: balance[0].balance, channelBalance: balance[1].balance }))
.then((balance) => {
event.sender.send('receiveBalance', { walletBalance: balance[0].total_balance, channelBalance: balance[1].balance })
})
.catch(error => console.log('balance error: ', error))
break
case 'createInvoice':
@ -129,7 +143,10 @@ export default function (lnd, event, msg, data) {
console.log('payinvoice success: ', payment_route)
event.sender.send('paymentSuccessful', Object.assign(data, { payment_route }))
})
.catch(({ error }) => event.sender.send('paymentFailed', { error: error.toString() }))
.catch(({ error }) => {
console.log('error: ', error)
event.sender.send('paymentFailed', { error: error.toString() })
})
break
case 'sendCoins':
// Transaction looks like { txid: String }

2
app/lnd/methods/paymentsController.js

@ -7,7 +7,7 @@
export function sendPaymentSync(lnd, { paymentRequest }) {
return new Promise((resolve, reject) => {
lnd.sendPaymentSync({ payment_request: paymentRequest }, (error, data) => {
if (error) {
if (error) {
reject({ error })
return
}

42
app/lnd/utils/index.js

@ -1,42 +0,0 @@
import zbase32 from 'zbase32'
function convertBigEndianBufferToLong(longBuffer) {
let longValue = 0
const byteArray = Buffer.from(longBuffer).swap64()
for (let i = byteArray.length - 1; i >= 0; i -= 1) {
longValue = (longValue * 256) + byteArray[i]
}
return longValue
}
export function decodeInvoice(payreq) {
const payreqBase32 = zbase32.decode(payreq)
const bufferHexRotated = Buffer.from(payreqBase32).toString('hex')
const bufferHex = bufferHexRotated.substr(bufferHexRotated.length - 1, bufferHexRotated.length)
+ bufferHexRotated.substr(0, bufferHexRotated.length - 1)
const buffer = Buffer.from(bufferHex, 'hex')
const pubkeyBuffer = buffer.slice(0, 33)
const pubkey = pubkeyBuffer.toString('hex')
const paymentHashBuffer = buffer.slice(33, 65)
const paymentHashHex = paymentHashBuffer.toString('hex')
const valueBuffer = buffer.slice(65, 73)
const amount = convertBigEndianBufferToLong(valueBuffer)
return {
payreq,
pubkey,
amount,
r_hash: paymentHashHex
}
}
export default {
decodeInvoice
}

3
app/reducers/channels.js

@ -202,7 +202,8 @@ export const channelGraphData = (event, data) => (dispatch, getState) => {
// if there are any new channel updates
if (channel_updates.length) {
// The network has updated, so fetch a new result
dispatch(fetchDescribeNetwork())
// TODO: can't do this now because of the SVG performance issues, after we fix this we can uncomment the line below
// dispatch(fetchDescribeNetwork())
// loop through the channel updates
for (let i = 0; i < channel_updates.length; i += 1) {

5
app/reducers/ipc.js

@ -33,7 +33,7 @@ import {
newTransaction
} from './transaction'
import { receiveDescribeNetwork, receiveQueryRoutes } from './network'
import { receiveDescribeNetwork, receiveQueryRoutes, receiveInvoiceAndQueryRoutes } from './network'
// Import all receiving IPC event handlers and pass them into createIpc
const ipc = createIpc({
@ -90,7 +90,8 @@ const ipc = createIpc({
newTransaction,
receiveDescribeNetwork,
receiveQueryRoutes
receiveQueryRoutes,
receiveInvoiceAndQueryRoutes
})
export default ipc

171
app/reducers/network.js

@ -1,5 +1,6 @@
import { createSelector } from 'reselect'
import { ipcRenderer } from 'electron'
import { bech32 } from '../utils'
// ------------------------------------
// Constants
@ -19,6 +20,17 @@ export const SET_CURRENT_TAB = 'SET_CURRENT_TAB'
export const SET_CURRENT_PEER = 'SET_CURRENT_PEER'
export const UPDATE_PAY_REQ = 'UPDATE_PAY_REQ'
export const RESET_PAY_REQ = 'RESET_PAY_REQ'
export const UPDATE_SELECTED_PEERS = 'UPDATE_SELECTED_PEERS'
export const CLEAR_SELECTED_PEERS = 'CLEAR_SELECTED_PEERS'
export const UPDATE_SELECTED_CHANNELS = 'UPDATE_SELECTED_CHANNELS'
export const CLEAR_SELECTED_CHANNELS = 'CLEAR_SELECTED_CHANNELS'
export const GET_INFO_AND_QUERY_ROUTES = 'GET_INFO_AND_QUERY_ROUTES'
export const RECEIVE_INFO_AND_QUERY_ROUTES = 'RECEIVE_INFO_AND_QUERY_ROUTES'
export const CLEAR_QUERY_ROUTES = 'CLEAR_QUERY_ROUTES'
// ------------------------------------
// Actions
@ -71,6 +83,50 @@ export function updatePayReq(pay_req) {
}
}
export function resetPayReq() {
return {
type: RESET_PAY_REQ
}
}
export function updateSelectedPeers(peer) {
return {
type: UPDATE_SELECTED_PEERS,
peer
}
}
export function updateSelectedChannels(channel) {
return {
type: UPDATE_SELECTED_CHANNELS,
channel
}
}
export function getInvoiceAndQueryRoutes() {
return {
type: GET_INFO_AND_QUERY_ROUTES
}
}
export function clearQueryRoutes() {
return {
type: CLEAR_QUERY_ROUTES
}
}
export function clearSelectedPeers() {
return {
type: CLEAR_SELECTED_PEERS
}
}
export function clearSelectedChannels() {
return {
type: CLEAR_SELECTED_CHANNELS
}
}
// Send IPC event for describeNetwork
export const fetchDescribeNetwork = () => (dispatch) => {
dispatch(getDescribeNetwork())
@ -87,6 +143,15 @@ export const queryRoutes = (pubkey, amount) => (dispatch) => {
export const receiveQueryRoutes = (event, { routes }) => dispatch => dispatch({ type: RECEIVE_QUERY_ROUTES, routes })
// take a payreq and query routes for it
export const fetchInvoiceAndQueryRoutes = payreq => (dispatch) => {
dispatch(getInvoiceAndQueryRoutes())
ipcRenderer.send('lnd', { msg: 'getInvoiceAndQueryRoutes', data: { payreq } })
}
export const receiveInvoiceAndQueryRoutes = (event, { routes }) => dispatch => {
dispatch({ type: RECEIVE_INFO_AND_QUERY_ROUTES, routes })
}
// ------------------------------------
// Action Handlers
// ------------------------------------
@ -103,12 +168,7 @@ const ACTION_HANDLERS = {
}
),
[SET_CURRENT_ROUTE]: (state, { route }) => (
{
...state,
selectedNode: { pubkey: state.selectedNode.pubkey, routes: state.selectedNode.routes, currentRoute: route }
}
),
[SET_CURRENT_ROUTE]: (state, { route }) => ({ ...state, currentRoute: route }),
[SET_CURRENT_CHANNEL]: (state, { selectedChannel }) => ({ ...state, selectedChannel }),
@ -116,21 +176,96 @@ const ACTION_HANDLERS = {
[SET_CURRENT_PEER]: (state, { currentPeer }) => ({ ...state, currentPeer }),
[UPDATE_PAY_REQ]: (state, { pay_req }) => ({ ...state, pay_req })
[UPDATE_PAY_REQ]: (state, { pay_req }) => ({ ...state, pay_req }),
[RESET_PAY_REQ]: state => ({ ...state, pay_req: '' }),
[GET_INFO_AND_QUERY_ROUTES]: state => ({ ...state, fetchingInvoiceAndQueryingRoutes: true }),
[RECEIVE_INFO_AND_QUERY_ROUTES]: (state, { routes }) => ({ ...state, fetchingInvoiceAndQueryingRoutes: false, payReqRoutes: routes }),
[CLEAR_QUERY_ROUTES]: state => ({ ...state, payReqRoutes: [], currentRoute: {} }),
[UPDATE_SELECTED_PEERS]: (state, { peer }) => {
let selectedPeers
if (state.selectedPeers.includes(peer)) {
selectedPeers = state.selectedPeers.filter(selectedPeer => selectedPeer.pub_key !== peer.pub_key)
}
if (!state.selectedPeers.includes(peer)) {
selectedPeers = [...state.selectedPeers, peer]
}
return {
...state, selectedPeers
}
},
[CLEAR_SELECTED_PEERS]: state => ({ ...state, selectedPeers: [] }),
[UPDATE_SELECTED_CHANNELS]: (state, { channel }) => {
let selectedChannels
if (state.selectedChannels.includes(channel)) {
selectedChannels = state.selectedChannels.filter(selectedChannel => selectedChannel.chan_id !== channel.chan_id)
}
if (!state.selectedChannels.includes(channel)) {
selectedChannels = [...state.selectedChannels, channel]
}
return {
...state, selectedChannels
}
},
[CLEAR_SELECTED_CHANNELS]: state => ({ ...state, selectedChannels: [] })
}
// ------------------------------------
// Selectors
// ------------------------------------
const networkSelectors = {}
const currentRouteSelector = state => state.network.selectedNode.currentRoute
const selectedPeersSelector = state => state.network.selectedPeers
const selectedChannelsSelector = state => state.network.selectedChannels
const payReqSelector = state => state.network.pay_req
const currentRouteSelector = state => state.network.currentRoute
// networkSelectors.currentRouteHopChanIds = createSelector(
// currentRouteSelector,
// (currentRoute) => {
// if (!currentRoute.hops) { return [] }
// return currentRoute.hops.map(hop => hop.chan_id)
// }
// )
networkSelectors.selectedPeerPubkeys = createSelector(
selectedPeersSelector,
peers => peers.map(peer => peer.pub_key)
)
networkSelectors.currentRouteHopChanIds = createSelector(
networkSelectors.selectedChannelIds = createSelector(
selectedChannelsSelector,
channels => channels.map(channel => channel.chan_id)
)
networkSelectors.payReqIsLn = createSelector(
payReqSelector,
(input) => {
if (!input.startsWith('ln')) { return false }
try {
bech32.decode(input)
return true
} catch (e) {
return false
}
}
)
networkSelectors.currentRouteChanIds = createSelector(
currentRouteSelector,
(currentRoute) => {
if (!currentRoute.hops) { return [] }
(route) => {
if (!route.hops || !route.hops.length) { return [] }
return currentRoute.hops.map(hop => hop.chan_id)
return route.hops.map(hop => hop.chan_id)
}
)
@ -143,18 +278,18 @@ const initialState = {
networkLoading: false,
nodes: [],
edges: [],
selectedNode: {
pubkey: '',
routes: [],
currentRoute: {}
},
selectedChannel: {},
currentTab: 1,
currentPeer: {},
currentRoute: {},
pay_req: ''
fetchingInvoiceAndQueryingRoutes: false,
pay_req: '',
payReqRoutes: [],
selectedPeers: [],
selectedChannels: []
}

2
app/routes.js

@ -5,12 +5,14 @@ import App from './routes/app'
import Activity from './routes/activity'
import Peers from './routes/peers'
import Channels from './routes/channels'
import Network from './routes/network'
export default () => (
<App>
<Switch>
<Route path='/peers' component={Peers} />
<Route path='/channels' component={Channels} />
<Route path='/network' component={Network} />
<Route path='/' component={Activity} />
</Switch>
</App>

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

@ -4,6 +4,7 @@ import { MdSearch } from 'react-icons/lib/md'
import { FaAngleDown } from 'react-icons/lib/fa'
import Wallet from 'components/Wallet'
import LoadingBolt from 'components/LoadingBolt'
import Invoice from './components/Invoice'
import Payment from './components/Payment'
import Transaction from './components/Transaction'
@ -19,8 +20,9 @@ class Activity extends Component {
}
componentWillMount() {
const { fetchPayments, fetchInvoices, fetchTransactions } = this.props
const { fetchPayments, fetchInvoices, fetchTransactions, fetchBalance } = this.props
fetchBalance()
fetchPayments()
fetchInvoices()
fetchTransactions()
@ -60,6 +62,8 @@ class Activity extends Component {
} = this.props
if (invoiceLoading || paymentLoading) { return <div>Loading...</div> }
if (balance.balanceLoading) { return <LoadingBolt /> }
if (!balance.channelBalance || !balance.walletBalance) { return <LoadingBolt /> }
return (
<div>
@ -123,6 +127,7 @@ Activity.propTypes = {
fetchPayments: PropTypes.func.isRequired,
fetchInvoices: PropTypes.func.isRequired,
fetchTransactions: PropTypes.func.isRequired,
fetchBalance: PropTypes.func.isRequired,
ticker: PropTypes.object.isRequired,
searchInvoices: PropTypes.func.isRequired,

1
app/routes/activity/components/components/Modal/Invoice/Invoice.js

@ -8,7 +8,6 @@ import QRCode from 'qrcode.react'
import { FaCircle } from 'react-icons/lib/fa'
import CurrencyIcon from 'components/CurrencyIcon'
import { btc } from 'utils'
import styles from './Invoice.scss'

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

@ -1,13 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
import ReactModal from 'react-modal'
import { MdClose } from 'react-icons/lib/md'
import Transaction from './Transaction'
import Payment from './Payment'
import Invoice from './Invoice'
import { MdClose } from 'react-icons/lib/md'
import styles from './Modal.scss'
const Modal = ({ modalType, modalProps, hideActivityModal, ticker, currentTicker }) => {

1
app/routes/activity/components/components/Modal/Payment/Payment.js

@ -4,7 +4,6 @@ import PropTypes from 'prop-types'
import Moment from 'react-moment'
import 'moment-timezone'
import CurrencyIcon from 'components/CurrencyIcon'
import { btc } from 'utils'
import styles from './Payment.scss'

9
app/routes/activity/components/components/Modal/Transaction/Transaction.js

@ -5,7 +5,6 @@ import PropTypes from 'prop-types'
import Moment from 'react-moment'
import 'moment-timezone'
import CurrencyIcon from 'components/CurrencyIcon'
import { btc } from 'utils'
import styles from './Transaction.scss'
@ -43,10 +42,10 @@ const Transaction = ({ transaction, ticker, currentTicker }) => (
<dt>Fee</dt>
<dd>
{
ticker.currency === 'usd' ?
btc.satoshisToUsd(transaction.total_fees)
:
btc.satoshisToBtc(transaction.total_fees)
ticker.currency === 'usd' ?
btc.satoshisToUsd(transaction.total_fees)
:
btc.satoshisToBtc(transaction.total_fees)
}
</dd>
<dt>Date</dt>

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

@ -1,5 +1,6 @@
import { connect } from 'react-redux'
import { tickerSelectors } from 'reducers/ticker'
import { fetchBalance } from 'reducers/balance'
import {
fetchInvoices,
searchInvoices,
@ -34,7 +35,8 @@ const mapDispatchToProps = {
hideActivityModal,
changeFilter,
toggleFilterPulldown,
newAddress
newAddress,
fetchBalance
}
const mapStateToProps = state => ({

8
app/routes/app/components/App.js

@ -9,10 +9,9 @@ import styles from './App.scss'
class App extends Component {
componentWillMount() {
const { fetchTicker, fetchBalance, fetchInfo, newAddress } = this.props
const { fetchTicker, fetchInfo, newAddress } = this.props
fetchTicker()
fetchBalance()
fetchInfo()
newAddress('np2wkh')
}
@ -23,7 +22,6 @@ class App extends Component {
hideModal,
ticker,
currentTicker,
balance,
form,
openPayForm,
@ -37,7 +35,7 @@ class App extends Component {
children
} = this.props
if (!currentTicker || balance.balanceLoading) { return <LoadingBolt /> }
if (!currentTicker) { return <LoadingBolt /> }
return (
<div>
@ -68,7 +66,6 @@ class App extends Component {
App.propTypes = {
modal: PropTypes.object.isRequired,
ticker: PropTypes.object.isRequired,
balance: PropTypes.object.isRequired,
form: PropTypes.object.isRequired,
formProps: PropTypes.object.isRequired,
closeForm: PropTypes.func.isRequired,
@ -78,7 +75,6 @@ App.propTypes = {
fetchInfo: PropTypes.func.isRequired,
hideModal: PropTypes.func.isRequired,
fetchTicker: PropTypes.func.isRequired,
fetchBalance: PropTypes.func.isRequired,
openPayForm: PropTypes.func.isRequired,
openRequestForm: PropTypes.func.isRequired,
clearError: PropTypes.func.isRequired,

3
app/routes/app/containers/AppContainer.js

@ -2,7 +2,6 @@ import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import { fetchTicker, setCurrency, tickerSelectors } from 'reducers/ticker'
import { newAddress } from 'reducers/address'
import { fetchBalance } from 'reducers/balance'
import { fetchInfo } from 'reducers/info'
@ -27,7 +26,6 @@ const mapDispatchToProps = {
fetchTicker,
setCurrency,
newAddress,
fetchBalance,
fetchInfo,
@ -58,7 +56,6 @@ const mapStateToProps = state => ({
ticker: state.ticker,
address: state.address,
balance: state.balance,
info: state.info,
payment: state.payment,
transaction: state.transaction,

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

@ -58,7 +58,7 @@ class Channels extends Component {
this.setState({ refreshing: true })
// store event in icon so we dont get an error when react clears it
const icon = this.refs.repeat.childNodes
const icon = this.repeat.childNodes
// fetch peers
fetchChannels()
@ -127,7 +127,7 @@ class Channels extends Component {
</ul>
</section>
<section className={styles.refreshContainer}>
<span className={styles.refresh} onClick={refreshClicked} ref='repeat'>
<span className={styles.refresh} onClick={refreshClicked} ref={(ref) => (this.repeat = ref)}>
{
this.state.refreshing ?
<FaRepeat />

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

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

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

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

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

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

3
app/routes/network/index.js

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

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

@ -42,7 +42,7 @@ class Peers extends Component {
this.setState({ refreshing: true })
// store event in icon so we dont get an error when react clears it
const icon = this.refs.repeat.childNodes
const icon = this.repeat.childNodes
// fetch peers
fetchPeers()
@ -98,7 +98,7 @@ class Peers extends Component {
</div>
<div className={styles.refreshContainer}>
<span className={styles.refresh} onClick={refreshClicked} ref='repeat'>
<span className={styles.refresh} onClick={refreshClicked} ref={(ref) => (this.repeat = ref)}>
{
this.state.refreshing ?
<FaRepeat />

147
app/rpc.proto

@ -25,9 +25,46 @@ package lnrpc;
* https://github.com/MaxFangX/lightning-api
*/
// The WalletUnlocker service is used to set up a wallet password for
// lnd at first startup, and unlock a previously set up wallet.
service WalletUnlocker {
/** lncli: `create`
CreateWallet is used at lnd startup to set the encryption password for
the wallet database.
*/
rpc CreateWallet(CreateWalletRequest) returns (CreateWalletResponse) {
option (google.api.http) = {
post: "/v1/createwallet"
body: "*"
};
}
/** lncli: `unlock`
UnlockWallet is used at startup of lnd to provide a password to unlock
the wallet database.
*/
rpc UnlockWallet(UnlockWalletRequest) returns (UnlockWalletResponse) {
option (google.api.http) = {
post: "/v1/unlockwallet"
body: "*"
};
}
}
message CreateWalletRequest {
bytes password = 1;
}
message CreateWalletResponse {}
message UnlockWalletRequest {
bytes password = 1;
}
message UnlockWalletResponse {}
service Lightning {
/** lncli: `walletbalance`
WalletBalance returns the sum of all confirmed unspent outputs under control
WalletBalance returns total unspent outputs(confirmed and unconfirmed), all confirmed unspent outputs and all unconfirmed unspent outputs under control
by the wallet. This method can be modified by having the request specify
only witness outputs should be factored into the final output sum.
*/
@ -59,7 +96,10 @@ service Lightning {
/** lncli: `sendcoins`
SendCoins executes a request to send coins to a particular address. Unlike
SendMany, this RPC call only allows creating a single output at a time.
SendMany, this RPC call only allows creating a single output at a time. If
neither target_conf, or sat_per_byte are set, then the internal wallet will
consult its fee model to determine a fee for the default confirmation
target.
*/
rpc SendCoins (SendCoinsRequest) returns (SendCoinsResponse) {
option (google.api.http) = {
@ -77,7 +117,9 @@ service Lightning {
/** lncli: `sendmany`
SendMany handles a request for a transaction that creates multiple specified
outputs in parallel.
outputs in parallel. If neither target_conf, or sat_per_byte are set, then
the internal wallet will consult its fee model to determine a fee for the
default confirmation target.
*/
rpc SendMany (SendManyRequest) returns (SendManyResponse);
@ -191,7 +233,10 @@ service Lightning {
/** lncli: `openchannel`
OpenChannel attempts to open a singly funded channel specified in the
request to a remote peer.
request to a remote peer. Users are able to specify a target number of
blocks that the funding transaction should be confirmed in, or a manual fee
rate to us for the funding transaction. If neither are specified, then a
lax block confirmation target is used.
*/
rpc OpenChannel (OpenChannelRequest) returns (stream OpenStatusUpdate);
@ -199,7 +244,10 @@ service Lightning {
CloseChannel attempts to close an active channel identified by its channel
outpoint (ChannelPoint). The actions of this method can additionally be
augmented to attempt a force close after a timeout period in the case of an
inactive peer.
inactive peer. If a non-force close (cooperative closure) is requested,
then the user can specify either a target number of blocks until the
closure transaction is confirmed, or a manual fee rate. If neither are
specified, then a default lax, block confirmation target is used.
*/
rpc CloseChannel (CloseChannelRequest) returns (stream CloseStatusUpdate) {
option (google.api.http) = {
@ -431,6 +479,9 @@ message Transaction {
/// Fees paid for this transaction
int64 total_fees = 7 [ json_name = "total_fees" ];
/// Addresses that received funds for this transaction
repeated string dest_addresses = 8 [ json_name = "dest_addresses" ];
}
message GetTransactionsRequest {
}
@ -492,6 +543,12 @@ message LightningAddress {
message SendManyRequest {
/// The map from addresses to amounts
map<string, int64> AddrToAmount = 1;
/// The target number of blocks that this transaction should be confirmed by.
int32 target_conf = 3;
/// A manual fee rate set in sat/byte that should be used when crafting the transaction.
int64 sat_per_byte = 5;
}
message SendManyResponse {
/// The id of the transaction
@ -504,6 +561,12 @@ message SendCoinsRequest {
/// The amount in satoshis to send
int64 amount = 2;
/// The target number of blocks that this transaction should be confirmed by.
int32 target_conf = 3;
/// A manual fee rate set in sat/byte that should be used when crafting the transaction.
int64 sat_per_byte = 5;
}
message SendCoinsResponse {
/// The transaction ID of the transaction
@ -657,6 +720,13 @@ message ActiveChannel {
The list of active, uncleared HTLCs currently pending within the channel.
*/
repeated HTLC pending_htlcs = 15 [json_name = "pending_htlcs"];
/**
The CSV delay expressed in relative blocks. If the channel is force
closed, we'll need to wait for this many blocks before we can regain our
funds.
*/
uint32 csv_delay = 16 [ json_name = "csv_delay" ];
}
message ListChannelsRequest {
@ -764,6 +834,12 @@ message CloseChannelRequest {
/// If true, then the channel will be closed forcibly. This means the current commitment transaction will be signed and broadcast.
bool force = 2;
/// The target number of blocks that the closure transaction should be confirmed by.
int32 target_conf = 3;
/// A manual fee rate set in sat/byte that should be used when crafting the closure transaction.
int64 sat_per_byte = 5;
}
message CloseStatusUpdate {
oneof update {
@ -794,6 +870,12 @@ message OpenChannelRequest {
/// The number of satoshis to push to the remote side as part of the initial commitment state
int64 push_sat = 5 [json_name = "push_sat"];
/// The target number of blocks that the closure transaction should be confirmed by.
int32 target_conf = 6;
/// A manual fee rate set in sat/byte that should be used when crafting the closure transaction.
int64 sat_per_byte = 7;
}
message OpenStatusUpdate {
oneof update {
@ -803,6 +885,31 @@ message OpenStatusUpdate {
}
}
message PendingHTLC {
/// The direction within the channel that the htlc was sent
bool incoming = 1 [ json_name = "incoming" ];
/// The total value of the htlc
int64 amount = 2 [ json_name = "amount" ];
/// The final output to be swept back to the user's wallet
string outpoint = 3 [ json_name = "outpoint" ];
/// The next block height at which we can spend the current stage
uint32 maturity_height = 4 [ json_name = "maturity_height" ];
/**
The number of blocks remaining until the current stage can be swept.
Negative values indicate how many blocks have passed since becoming
mature.
*/
int32 blocks_til_maturity = 5 [ json_name = "blocks_til_maturity" ];
/// Indicates whether the htlc is in its first or second stage of recovery
uint32 stage = 6 [ json_name = "stage" ];
}
message PendingChannelRequest {}
message PendingChannelResponse {
message PendingChannel {
@ -823,7 +930,7 @@ message PendingChannelResponse {
uint32 confirmation_height = 2 [ json_name = "confirmation_height" ];
/// The number of blocks until this channel is open
uint32 blocks_till_open = 3 [ json_name = "blocks_till_open" ];
int32 blocks_till_open = 3 [ json_name = "blocks_till_open" ];
/**
The amount calculated to be paid in fees for the current set of
@ -857,8 +964,6 @@ message PendingChannelResponse {
/// The pending channel to be force closed
PendingChannel channel = 1 [ json_name = "channel" ];
// TODO(roasbeef): HTLC's as well?
/// The transaction id of the closing transaction
string closing_txid = 2 [ json_name = "closing_txid" ];
@ -868,8 +973,17 @@ message PendingChannelResponse {
/// The height at which funds can be sweeped into the wallet
uint32 maturity_height = 4 [ json_name = "maturity_height" ];
/// Remaining # of blocks until funds can be sweeped into the wallet
uint32 blocks_til_maturity = 5 [ json_name = "blocks_til_maturity" ];
/*
Remaining # of blocks until the commitment output can be swept.
Negative values indicate how many blocks have passed since becoming
mature.
*/
int32 blocks_til_maturity = 5 [ json_name = "blocks_til_maturity" ];
/// The total value of funds successfully recovered from this channel
int64 recovered_balance = 6 [ json_name = "recovered_balance" ];
repeated PendingHTLC pending_htlcs = 8 [ json_name = "pending_htlcs" ];
}
/// The balance in satoshis encumbered in pending channels
@ -891,7 +1005,13 @@ message WalletBalanceRequest {
}
message WalletBalanceResponse {
/// The balance of the wallet
int64 balance = 1 [json_name = "balance"];
int64 total_balance = 1 [json_name = "total_balance"];
/// The confirmed balance of a wallet(with >= 1 confirmations)
int64 confirmed_balance = 2 [json_name = "confirmed_balance"];
/// The unconfirmed balance of a wallet(with 0 confirmations)
int64 unconfirmed_balance = 3 [json_name = "unconfirmed_balance"];
}
message ChannelBalanceRequest {
@ -994,6 +1114,7 @@ message LightningNode {
string pub_key = 2 [ json_name = "pub_key" ];
string alias = 3 [ json_name = "alias" ];
repeated NodeAddress addresses = 4 [ json_name = "addresses" ];
string color = 5 [ json_name = "color" ];
}
message NodeAddress {
@ -1179,6 +1300,9 @@ message Invoice {
/// Fallback on-chain address.
string fallback_addr = 12 [json_name = "fallback_addr"];
/// Delta to use for the time-lock of the CLTV extended to the final hop.
uint64 cltv_expiry = 13 [json_name = "cltv_expiry"];
}
message AddInvoiceResponse {
bytes r_hash = 1 [json_name = "r_hash"];
@ -1263,6 +1387,7 @@ message PayReq {
string description = 6 [json_name = "description"];
string description_hash = 7 [json_name = "description_hash"];
string fallback_addr = 8 [json_name = "fallback_addr"];
int64 cltv_expiry = 9 [json_name = "cltv_expiry"];
}
message FeeReportRequest {}

3
package.json

@ -193,6 +193,9 @@
"bitcoinjs-lib": "^3.2.0",
"bitcore-lib": "^0.14.0",
"copy-to-clipboard": "^3.0.8",
"d3-force": "^1.1.0",
"d3-selection": "^1.2.0",
"d3-zoom": "^1.7.1",
"devtron": "^1.4.0",
"electron-debug": "^1.2.0",
"font-awesome": "^4.7.0",

71
yarn.lock

@ -2528,6 +2528,77 @@ 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-color@1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b"
d3-dispatch@1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
d3-drag@1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.1.tgz#df8dd4c502fb490fc7462046a8ad98a5c479282d"
dependencies:
d3-dispatch "1"
d3-selection "1"
d3-ease@1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
d3-force@^1.1.0:
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-interpolate@1:
version "1.1.6"
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.1.6.tgz#2cf395ae2381804df08aa1bf766b7f97b5f68fb6"
dependencies:
d3-color "1"
d3-quadtree@1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.3.tgz#ac7987e3e23fe805a990f28e1b50d38fcb822438"
d3-selection@1, d3-selection@^1.1.0, d3-selection@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.2.0.tgz#1b8ec1c7cedadfb691f2ba20a4a3cfbeb71bbc88"
d3-timer@1:
version "1.0.7"
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531"
d3-transition@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039"
dependencies:
d3-color "1"
d3-dispatch "1"
d3-ease "1"
d3-interpolate "1"
d3-selection "^1.1.0"
d3-timer "1"
d3-zoom@^1.7.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.7.1.tgz#02f43b3c3e2db54f364582d7e4a236ccc5506b63"
dependencies:
d3-dispatch "1"
d3-drag "1"
d3-interpolate "1"
d3-selection "1"
d3-transition "1"
d@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"

Loading…
Cancel
Save