diff --git a/app/components/Network/NetworkGraph.js b/app/components/Network/NetworkGraph.js index b182d267..9b50440f 100644 --- a/app/components/Network/NetworkGraph.js +++ b/app/components/Network/NetworkGraph.js @@ -1,7 +1,10 @@ +import { findDOMNode } from 'react-dom' import React, { Component } from 'react' import PropTypes from 'prop-types' import { ForceGraph, ForceGraphNode, ForceGraphLink } from 'react-vis-force' import { FaCircle } from 'react-icons/lib/fa' +import Isvg from 'react-inlinesvg' +import bitcoinIcon from 'icons/skinny_bitcoin.svg' import styles from './NetworkGraph.scss' class NetworkGraph extends Component { @@ -15,7 +18,7 @@ class NetworkGraph extends Component { componentWillMount() { setTimeout(() => { this.setState({ ready: true }) - }, 1000) + }, 10000) } render() { @@ -24,10 +27,12 @@ class NetworkGraph extends Component { network: { nodes, edges, selectedChannel, networkLoading }, selectedPeerPubkeys, selectedChannelIds, - identity_pubkey, + currentRouteChanIds, + identity_pubkey } = this.props if (!ready || networkLoading) { + console.log('hi!') return (
@@ -40,6 +45,7 @@ class NetworkGraph extends Component { return (
) } { - edges.map(edge => - - ) + edges.map(edge => { + return ( + + ) + }) + } + + { + currentRouteChanIds.map((chan_id) => { + const line = document.getElementById(chan_id) + + if (!line) { return } + const x1 = line.x1.baseVal.value + const x2 = line.x2.baseVal.value + const y1 = line.y1.baseVal.value + const y2 = line.y2.baseVal.value + + return ( + + ) + }) + } + + { + currentRouteChanIds.map((chan_id) => { + const line = document.getElementById(chan_id) + + if (!line) { return } + const x1 = line.x1.baseVal.value + const x2 = line.x2.baseVal.value + const y1 = line.y1.baseVal.value + const y2 = line.y2.baseVal.value + + return ( + + ) + }) + } + { + currentRouteChanIds.map((chan_id) => { + const line = document.getElementById(chan_id) + + if (!line) { return } + const x1 = line.x1.baseVal.value + const x2 = line.x2.baseVal.value + const y1 = line.y1.baseVal.value + const y2 = line.y2.baseVal.value + + return ( + + ) + }) }
diff --git a/app/components/Network/NetworkGraph.scss b/app/components/Network/NetworkGraph.scss index 6a60cf98..4ae59d0b 100644 --- a/app/components/Network/NetworkGraph.scss +++ b/app/components/Network/NetworkGraph.scss @@ -18,6 +18,10 @@ left: 0; width: 70%; height: 100vh; + animation: fadein 0.5s; + animation-timing-function:linear; + animation-fill-mode: forwards; + animation-iteration-count: infinite; h1 { font-size: 22px; @@ -32,13 +36,22 @@ height: 100vh; animation: fadein 0.5s; animation-timing-function:linear; - animation-fill-mode:forwards; + animation-fill-mode: forwards; animation-iteration-count: 1; } .node { stroke-width: 2 !important; animation: color-change 1s infinite; + fill: #353535; + + &.owner { + fill: #FFFFFF; + } + + &.peer { + fill: #5589F3; + } } .activeEdge { diff --git a/app/components/Network/TransactionForm.js b/app/components/Network/TransactionForm.js index 8c305215..6266e6c8 100644 --- a/app/components/Network/TransactionForm.js +++ b/app/components/Network/TransactionForm.js @@ -1,14 +1,60 @@ import React from 'react' import PropTypes from 'prop-types' +import { btc } from 'utils' import styles from './TransactionForm.scss' -const TransactionForm = ({}) => ( +const TransactionForm = ({ updatePayReq, pay_req, loadingRoutes, payReqRoutes, setCurrentRoute, currentRoute }) => (
- tx form +
+ updatePayReq(event.target.value)} + /> +
+ + { + loadingRoutes && +
+
+

calculating all routes...

+
+ } + +
    + { + payReqRoutes.map((route, index) => +
  • setCurrentRoute(route)}> +
    +

    Route #{index + 1}

    + Hops: {route.hops.length} +
    + +
    +
    +

    Amount

    + {btc.satoshisToBtc(route.total_amt)} BTC +
    + +
    +

    Fees

    + {btc.satoshisToBtc(route.total_fees)} BTC +
    +
    +
  • + ) + } +
) TransactionForm.propTypes = { + updatePayReq: PropTypes.func.isRequired, + pay_req: PropTypes.string.isRequired, + loadingRoutes: PropTypes.bool.isRequired, + payReqRoutes: PropTypes.array.isRequired, + setCurrentRoute: PropTypes.func.isRequired } export default TransactionForm diff --git a/app/components/Network/TransactionForm.scss b/app/components/Network/TransactionForm.scss index e69de29b..f4c35cf7 100644 --- a/app/components/Network/TransactionForm.scss +++ b/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; + } + } + } +} \ No newline at end of file diff --git a/app/lnd/methods/index.js b/app/lnd/methods/index.js index 7f4a4132..6425f7f7 100644 --- a/app/lnd/methods/index.js +++ b/app/lnd/methods/index.js @@ -44,6 +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 => { + console.log('invoiceData: ', invoiceData) + networkController.queryRoutes(lnd, { pubkey: invoiceData.destination , amount: invoiceData.num_satoshis }) + .then(routes => { + console.log('routes: ', 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: '' } walletController.newAddress(lnd, data.type) @@ -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 } diff --git a/app/lnd/methods/paymentsController.js b/app/lnd/methods/paymentsController.js index f0f36fab..139486c7 100644 --- a/app/lnd/methods/paymentsController.js +++ b/app/lnd/methods/paymentsController.js @@ -6,8 +6,11 @@ */ export function sendPaymentSync(lnd, { paymentRequest }) { return new Promise((resolve, reject) => { - lnd.sendPaymentSync({ payment_request: paymentRequest }, (err, data) => { - if (err) { reject(err) } + lnd.sendPaymentSync({ payment_request: paymentRequest }, (error, data) => { + if (error) { + reject({ error }) + return + } if (!data.payment_route) { reject({ error: data.payment_error }) } diff --git a/app/reducers/channels.js b/app/reducers/channels.js index 0a919805..cc7f6c0d 100644 --- a/app/reducers/channels.js +++ b/app/reducers/channels.js @@ -197,7 +197,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++) { diff --git a/app/reducers/ipc.js b/app/reducers/ipc.js index 557555e5..8ea03f28 100644 --- a/app/reducers/ipc.js +++ b/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 diff --git a/app/reducers/network.js b/app/reducers/network.js index 888f1b47..04f5118e 100644 --- a/app/reducers/network.js +++ b/app/reducers/network.js @@ -1,5 +1,6 @@ import { createSelector } from 'reselect' import { ipcRenderer } from 'electron' +import { bech32 } from '../utils' // ------------------------------------ // Constants @@ -19,11 +20,16 @@ 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 UPDATE_SELECTED_CHANNELS = 'UPDATE_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 // ------------------------------------ @@ -75,6 +81,12 @@ export function updatePayReq(pay_req) { } } +export function resetPayReq() { + return { + type: RESET_PAY_REQ + } +} + export function updateSelectedPeers(peer) { return { type: UPDATE_SELECTED_PEERS, @@ -89,6 +101,18 @@ export function updateSelectedChannels(channel) { } } +export function getInvoiceAndQueryRoutes() { + return { + type: GET_INFO_AND_QUERY_ROUTES + } +} + +export function clearQueryRoutes() { + return { + type: CLEAR_QUERY_ROUTES + } +} + // Send IPC event for describeNetwork export const fetchDescribeNetwork = () => (dispatch) => { dispatch(getDescribeNetwork()) @@ -105,6 +129,16 @@ 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 => { + console.log('routes: ', routes) + dispatch({ type: RECEIVE_INFO_AND_QUERY_ROUTES, routes }) +} // ------------------------------------ // Action Handlers // ------------------------------------ @@ -121,12 +155,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 }), @@ -135,6 +164,11 @@ const ACTION_HANDLERS = { [SET_CURRENT_PEER]: (state, { currentPeer }) => ({ ...state, currentPeer }), [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 @@ -173,29 +207,53 @@ const ACTION_HANDLERS = { // Selectors // ------------------------------------ const networkSelectors = {} -const currentRouteSelector = state => state.network.selectedNode.currentRoute -const selectedPeers = state => state.network.selectedPeers -const selectedChannels = state => state.network.selectedChannels +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 [] } +// networkSelectors.currentRouteHopChanIds = createSelector( +// currentRouteSelector, +// (currentRoute) => { +// if (!currentRoute.hops) { return [] } - return currentRoute.hops.map(hop => hop.chan_id) - } -) +// return currentRoute.hops.map(hop => hop.chan_id) +// } +// ) networkSelectors.selectedPeerPubkeys = createSelector( - selectedPeers, + selectedPeersSelector, peers => peers.map(peer => peer.pub_key) ) networkSelectors.selectedChannelIds = createSelector( - selectedChannels, + 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, + (route) => { + if (!route.hops || !route.hops.length) { return [] } + + return route.hops.map(hop => hop.chan_id) + } +) + export { networkSelectors } // ------------------------------------ @@ -205,19 +263,16 @@ const initialState = { networkLoading: false, nodes: [], edges: [], - selectedNode: { - pubkey: '', - routes: [], - currentRoute: {} - }, selectedChannel: {}, currentTab: 1, currentPeer: {}, + currentRoute: {}, + fetchingInvoiceAndQueryingRoutes: false, pay_req: '', - + payReqRoutes: [], selectedPeers: [], selectedChannels: [] } diff --git a/app/routes/activity/components/Activity.js b/app/routes/activity/components/Activity.js index 7326f26a..23eb32d1 100644 --- a/app/routes/activity/components/Activity.js +++ b/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,7 @@ class Activity extends Component { } = this.props if (invoiceLoading || paymentLoading) { return
Loading...
} + if (balance.balanceLoading) { return } return (
diff --git a/app/routes/activity/containers/ActivityContainer.js b/app/routes/activity/containers/ActivityContainer.js index 650faf24..3f0df128 100644 --- a/app/routes/activity/containers/ActivityContainer.js +++ b/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 => ({ diff --git a/app/routes/app/components/App.js b/app/routes/app/components/App.js index f0caab1d..e11c1202 100644 --- a/app/routes/app/components/App.js +++ b/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('p2pkh') } @@ -37,7 +36,7 @@ class App extends Component { children } = this.props - if (!currentTicker || balance.balanceLoading) { return } + if (!currentTicker) { return } return (
diff --git a/app/routes/network/components/Network.js b/app/routes/network/components/Network.js index 4df80ba9..d5bf8481 100644 --- a/app/routes/network/components/Network.js +++ b/app/routes/network/components/Network.js @@ -19,13 +19,35 @@ class Network extends Component { 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 } = this.props + + clearQueryRoutes() + resetPayReq() + } + render() { const { setCurrentTab, updateSelectedPeers, + setCurrentRoute, network, selectedPeerPubkeys, + currentRouteChanIds, peers: { peers }, @@ -33,6 +55,8 @@ class Network extends Component { selectedChannelIds, updateSelectedChannels, + updatePayReq, + identity_pubkey } = this.props @@ -45,7 +69,16 @@ class Network extends Component { return break case 3: - return + return ( + + ) break } } @@ -58,6 +91,7 @@ class Network extends Component { identity_pubkey={identity_pubkey} selectedPeerPubkeys={selectedPeerPubkeys} selectedChannelIds={selectedChannelIds} + currentRouteChanIds={currentRouteChanIds} />
diff --git a/app/routes/network/components/Network.scss b/app/routes/network/components/Network.scss index 4d49bffe..185adb85 100644 --- a/app/routes/network/components/Network.scss +++ b/app/routes/network/components/Network.scss @@ -47,6 +47,7 @@ width: 30%; height: 100%; background: #353535; + overflow-y: scroll; } .tabs { diff --git a/app/routes/network/containers/NetworkContainer.js b/app/routes/network/containers/NetworkContainer.js index 41f85a16..85428b70 100644 --- a/app/routes/network/containers/NetworkContainer.js +++ b/app/routes/network/containers/NetworkContainer.js @@ -7,6 +7,13 @@ import { setCurrentTab, updateSelectedPeers, updateSelectedChannels, + setCurrentRoute, + + updatePayReq, + resetPayReq, + + fetchInvoiceAndQueryRoutes, + clearQueryRoutes, networkSelectors } from '../../../reducers/network' @@ -19,6 +26,11 @@ const mapDispatchToProps = { fetchDescribeNetwork, setCurrentTab, updateSelectedPeers, + updatePayReq, + fetchInvoiceAndQueryRoutes, + setCurrentRoute, + clearQueryRoutes, + resetPayReq, fetchPeers, @@ -33,6 +45,8 @@ const mapStateToProps = state => ({ selectedPeerPubkeys: networkSelectors.selectedPeerPubkeys(state), selectedChannelIds: networkSelectors.selectedChannelIds(state), + payReqIsLn: networkSelectors.payReqIsLn(state), + currentRouteChanIds: networkSelectors.currentRouteChanIds(state), activeChannels: channelsSelectors.activeChannels(state) })