JimmyMow
7 years ago
committed by
GitHub
39 changed files with 1725 additions and 117 deletions
@ -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 |
@ -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;} |
|||
} |
@ -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 |
@ -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; |
|||
} |
|||
} |
@ -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 |
@ -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; |
|||
} |
|||
} |
@ -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 |
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
@ -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 |
|||
} |
@ -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 |
@ -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; |
|||
} |
|||
} |
|||
} |
@ -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)) |
@ -0,0 +1,3 @@ |
|||
import NetworkContainer from './containers/NetworkContainer' |
|||
|
|||
export default NetworkContainer |
Loading…
Reference in new issue